Přejít k navigační liště

Zdroják » Různé » SOLID – (S)ingle Responsibility Principle

SOLID – (S)ingle Responsibility Principle

Články JavaScript, Různé

V tomto článku představím Single Responsibility Princip ze SOLIDu. Ukážeme si, jak tento princip dodržovat na konkrétním příkladu, namalujeme si nějaké UML diagramy, a nakonec přiložím celý příklad k dispozici v Typescriptu.

Seriál: SOLID (první díl)

  1. SOLID – (S)ingle Responsibility Principle 23. 4. 2018

Co je to SOLID

Jedná se o sadu principů (konkrétně 5) v rámci OOP, které pomáhájí k tvorbě softwarového designu, který je flexibilní, srozumitelný a udržovatelný.

SRP – Single Responsibility Principle

V tomto článku se vrhneme na ten první. Pro tento princip existuje mnoho definic. Já jsem si vybral tu od Uncle Boba (Robert C. Martin), který SRP definuje následovně:

Třída by měla dělat pouze jednu věc a dělat ji správně

To znamená, že by měl existovat jediný důvod pro její změnu!

Spousta lidí tak nějak intuitivně chápe, co se tím myslí. Problém nastává až tehdy, kdy se snažíme udělat vhodnou dekompozici tříd a určit jejich zodpovědnost. Uncle Bob v jeho knize The Clean Coder uvedl vhodnou poznámku, že tento princip je snadný k pochopení, ale těžký k dodržení. A to minimálně v mém případě určitě platí.

Pojďme si ho vysvělit na příkladu.

Příklad

Řekněme, že vytváříme nějaký systém, do kterého se uživatel musí zaregistrovat. Po registraci do systému pošleme uživateli uvítací zprávu na email. Pojďme si tedy navrhnout třídu, která bude řešit odesílání uvítacího emailu. Ta by mohla vypadat následovně.

EmailWelcomeMessage je třída, jejiž veřejné rozhranní tvoří metoda send()která odesílá uvítací zprávu uživateli na email, který se nastaví pomocí metody setRecipient(). Privátní metody buildMessageSubject a buildMessageBody slouží pro vytvoření předmětu a těla emailu. Pravděpodobně obsahují nějaký HTML snippet. Metoda configureConnection() slouží pro konfiguraci SMTP serveru a ostatních záležitostí pro přepravu pošty. 

Teď si představte, že za vámi přijde Mařenka z obchodního oddělení a řekne vám, že chce změnit text emailu, který se zasílá uživatelům. Abychom změnili text, tak musíme sáhnout do třídy, která se stará i o konfiguraci a zasílání emailu uživateli. Kód přidáváme, přesouváme, mažeme, až nakonec máme krásný cool text s obrázky v těle emailu. Kód se dostane do produkce a druhý den za vámi přijde naštvaná Mařenka, že se vůbec emaily neposílají, protože jsme nevědomky změnili i kód, který se staral o konfiguraci a přepravu pošty.

Jak by se vám líbilo, kdybyste si zavolali technika na opravu televize, u které nejde obraz, a ten vám ji vrátil „opravenou“ s funkčním obrazem, ale pro změnu by nešel zvuk?

Kde je v této konkrétní implementaci problém?

Problém je ten, že třída řeší spoustu věcí a existuje více než jeden důvod pro její změnu. Třídy, které řeší více než jeden problém jsou nejenom zbytečně velké, ale v případě změny jedné funkcionality se může stát, že rozbijete druhou. Takové třídy se i špatně testují a snadno v nich vznikají chyby. Už jen z toho důvodu, že je tam spoustu testovacích případů, na které můžete snadno zapomenout. O tom, že se veškeré změny v takové třídě dělají těžko, už snad ani nemusím psát (a stejně jsem napsal).

Podívejte se na schéma třídy a schválně si položte otázku: „Jaké jsou důvody pro změnu této třídy?“. Já vidím tyto:

  1. Změna textu nebo formátování emailu
  2. Změna konfigurace pro připojení na emailový server

Dekompozice: Změna textu nebo formátování emailu

A co třeba takto?

Třída EmailWelcomeMessage závisí na abstrakci EmailMessageBuilder. Ve třídě EmailWelcomeMessageBuilder pak najdeme kód, který řeší formátování emailu pro nově registrovaného uživatele. Pokud za námi přijde Mařenka s požadavkem na změnu tohoto textu, budeme zasahovat pouze do patřičné třídy, která opravdu řeší jen a pouze formátování textu!

Dekompozice: Změna konfigurace pro připojení na emailový server

První problém vyřešen. A co to ještě vylepšit?

EmailWelcomeMessage je jenom fasáda nad třídami, které řeší přepravu emailu a jeho formátování. To jakým způsobem se konfiguruje a přepravuje pošta ji nezajímá. Všechno je to skryté v nějaké implementaci EmailTransporteru. Pokud za námi přijde system admin Pepa, že se nám změnila autentifikace pro SMTP server, tak tuto konfiguraci změníme na jednom místě, a to v SomeConcreteEmailTransporteru! Nemůže se nám tedy stát, že změnou emailové konfigurace se nám rozbije formátování emailu a naopak.

Zdrojový kód příkladu

Celý příklad napsaný v typescriptu je k dispozici na jsfiddle.

Shrnutí

Dodržovat Single Responsibility Principle je těžké. Chce to zkušenosti a hlavně pevné nervy. V mém případě si dokonce pomáhám i tak, že si v hlavě říkám, co ta třída vlastně řeší za problém a dávám si pozor na spojky, protože pokud věta obsahuje nějaké spojky, tak je poměrně vysoká pravděpodobnost, že třída řeší víc věcí, než by měla. Například pro první verzi třídy EmailWelcomeMessage to vypadalo takhle: „Třída posílá email uživateli a nastavuje se v ní konfigurace pro přepravu pošty a umí naformátovat tělo emailu“. Všimněte si, že věty, které následují po spojce „a“, jsou přesně ty problémy, které jsme řešili nahoře. Nakonec bych ještě rád upozornil, že tento příklad byl vytvořen konkrétně pro ukázku tohoto principu, takže by skutečná implementace mohla vypadat trochu jinak.

Příště se podíváme na další princip, a to konkrétně Open Closed Principle.

Komentáře

Subscribe
Upozornit na
guest
16 Komentářů
Nejstarší
Nejnovější Most Voted
Inline Feedbacks
View all comments
dzejkob

Co na to říct? Principiálně hezký učebnicový příklad a bylo by fajn, kdyby se to tak dělalo. V realitě se ovšem počká, až Mařenka přijde – opraví se bugy a potom se to překlopí do režimu „to funguje – na to se nesahá“.

lenoch

Já mám šablonu mailu, kde je text mailu, subject a další parametry zprávy a pak třídu Mailer, která tu zprávu pošle.

Tomáš Votruba

Ahoj, moc se mi líbí jak jsi to pojal.
Když vidím někoho tento pojem vysvětlovat, tak vezme nejsložitější příklad, který v poslední době potkal.

Email sender a mail factory je krásné, krátké a k věci.

Jsem z PHP komunitu, koukal jsem na příklad a napadlo mě, máte tam nějaký Dependency Injection Container? Bez něj ten kód spíš spoustu lidí odradí svou náročnou udržitelností.

Tomáš Votruba

Přijde mi, že k tomu abych si injectnul třídu tak s použitím DI containeru musím pořádně zaplevelit kód, viz jsfiddle pro typescript example s DI container. Ale je to jen můj názor, možná to je v PHP vyřešené jiným způsobem.

To je pravda. Psát vlastní kontejner vše zkomplikuje a sám to dělám jen na školení, když vysvětluji co DI kontejner je (díky za tip Davide Grudle). V PHP si ho sám nepíšu, ale používám hotové řešení (Symfony\DependencyInjection nebo Nette\DI).

Co DI kontejeru očekávám?

  1. Nahážu tam třídy, které chci jako služby
  2. Když jednu vytáhnu, tak má v sobě všechny potřebné závislosti (nejlíp přes constructor)

A díky moc za ten příklad v jsfiddle. Takový kontejner vypadá složitě, znečišťuje a znepřehledňuje kód, takže chápu proč preferuješ factories.

Jaroslav Týc

Podle titulku a úvodu jsem si říkal „a jéje, někdo byl včera na přednášce a dneska nám píše, co jim pan profesor vtloukal do hlavy“, ale hned jak přišla věta „já jsem si vybral…“, tak jsem věděl, že to bude napsané z osobní zkušenosti a má cenu čít dál.
Díky za kvalitní popis z osobním pohledem na věc a za příklad, který dává smysl od začátku do konce.

Vít Heřman

Zdravím,

nedávno jsem o SRP psal takové vnitrofiremní pojednání o převažujícím podivném chápání pojmu SRP. Zjistil jsem, že programátoři usilující o dodržení SRP mají snahu dekomponovat daleko více, než je únosné. Výsledkem bývá hrozivý moloch malých interfaců a tříd, které reprezentují často velmi umělé abstrakce. Často pod chybně chápanou záminkou testovatelnosti.

Výsledkem je dramaticky větší množství entit v systému a také daleko větší množství infrastrukturního kódu (třídy nebo rozhranní, které v podstatě jen uměle lepí jiná rozhraní a třídy, apod.). To vše přináší neúnosnou komplexitu snižující pochopitelnost systému na základě studia kódu. A to považuji za nesrovnatelně větší zlo, než-li prohřešky vůči SOLID principům.

Módní koncepty jako interface všude, DI a IoC tomu nasazují korunu. Tyto koncepty mají skutečně jen velmi málo exklusivních přínosů. Třeba v případě šířených veřejných knihoven nebo frameworků, od kterých je požadována vysoká extensibilita ty přínosy vidět jsou. Pochopitelnost sice trpí jak ďas, ale rozšiřitelnost je vynikající. Jde sice o trade-off, ale přínos je zřejmý. Ale třeba v zákaznických aplikacích je to čirá zbytečnost, protože potřeba znovupoužitelnosti na úrovni binárního kódu je skoro nulová. Není problém provést změnu ve zdrojovém kódu aplikace a během pár minut nasadit (zjednodušeně řečeno).

V tomto příkladu jistou nadbytečností zavánějí třídy (rozhranní) jako (I)EmailTransporter, (I)EmailBuilder. Obyčejné dvě metody (a třeba fluent API) poslouží lépe:

 mailing
    .createEmail(sender, recipient, subject, body)
    .send(configuration.smtpOptions)

Proto doporučuji být k SOLID principům skeptický. Nedoporučuji slepě následovat jejich tisíckrát prezentované výhody. Hlavní je zamyslet se do hloubky, zda jde o skutečnou výhodu pro mou aplikaci a uvědomit si zamlčované náklady, o kterých žádná kniha nebo prezentéři moc nemluví…

Radek

Chápu, že můžete mít problém se zneužitím DI, SRP a dalších. Aby to lidem něco řeklo, o jak velkých projektech se bavíme? Vytváří je tým se sdílenými znalostmi nebo se vývojáři střídají? Řeší váš software problémy se známým řešením nebo prochází kontinuálními proměnami?

Vít Heřman

Bavíme se o projektech velikosti řádově desetitisíce LOC, tým usiluje o sdílené znalosti, ale přesto dochází ke střídání. Jedná se o víceméně standardní zákaznické projekty s SQL databází na pozadí, formuláři nebo webovkama na popředí a business logikou uprostřed.

Zásadní je ale u těchto typů aplikací to, že skoro vždy se jedná o dost specifická zákaznická řešení na míru, kde je snadná změna zdrojového kódu a není tedy v zásadě problém měnit kontrakty tříd.

Chtělo by to hlouběji rozebrat, ale obecně mi dávají univerzální smysl pouze „L“ a „I“ ze SOLIDu. „S“ je těžko uchopitelné (snažil jsem se naznačit), „O“ je výhodné pouze pro frameworky. A nakonec výhody „D“ jakožto důsledné závislosti výhradně na abstrakcích jsou sporadické, protože nutí abstrahovat i to, co v systému abstraktní být nemusí. Raději opakuji, že mluvím o zákaznických aplikacích se snadnou realizací změn na úrovni zdrojového kódu. A díky tomu nejen, že lze, ale i je výhodné abstrahovat méně. Na čitelnost kódu a udržitelnost to má vliv spíše pozitivní, protože v systému potom existuje daleko méně „entit“.

Zjednodušeně řečeno podle mne platí, že systém se snáze spravuje, pokud minimalizujeme počet entit (např. tříd a rozhraní) a ty mají pochopitelné a dobře zvolené kontrakty. Interní implementace metod je důležitá také, ale druhořadá.

A díky tomu se nemůžu zbavit pocitu hraničícím s jistotou, že SOLID přímo navádí k nadměrnému množství entit ve zdrojovém kódu a právě tím znesnadňuje jeho údržbu. To je jeho náklad, který musí být vyvážen nějakou podstatnou výhodou. A tou výhodou je znovupoužitelnost a rozšiřitelnost nejrůznějších částí formou rozšiřování a nikoli změn interních implementací. Ale nějak racionálně obhájit to lze podle mého jen u knihoven a frameworků.

Vasek

Je dobry napad ukazovat principy OOP sveta na spise funkcionalnim jazyku? Jako chapu ze Typescriptu je vyrazny krok k OOP v JS, ale presto si myslim, ze ukazkove implementace by byly lepsi v jinem nez jazyce.

Nechci tim nijak zlehcovat praci, kterou jsi udelal, a UMLka ktera jsi vytvoril. To je super a pochopi to z toho mnoho novych lidi, to je uzasne.. Ale ten zvoleny jazyk mi prijde spis ku neprospechu veci.

Radek

Jsem rád za tenhle seriál. Podle mě je SOLID zásadní při OO vývoji.

Navržená dekompozice by však podle mě měla pokračovat. Zpráva nemá zodpovědnost za odesílání a ani nemá vědět, jak bude odeslána. Metodu send a atribut transport bych z ní smazal. Šel bych i dál a zrušil vazbu na message builder, naopak bych přidal metodu ve smyslu public string serialize (SerializerInterface serializer). Pak
už s minimální prací můžeme zprávu poslat jak e-mailem, tak třeba jako notifikaci na mobil.

lenoch

To send() ve zprávě mi taky zavání. Osobně si zatím myslím, že by měla stačit třída MailSender a Message, eventuálně s odpovídajícími interface. Možná, že je potřeba někdy dekomponovat ještě na další třídy, ale zatím na to nevidím valný důvod.

mirek

Naprostou souhlasim. Dokonce bych si troufl rict, ze uvedena „dekompozice“ je pro me az trochu silena. Uz kdyz jsem cetl prvni odstavec s odkazem na knihu Cisty kod, cekal jsem neco podobneho. Uz z navrhu autora musi preci kazdeho trknout zavislost zpravy na dalsich sluzbach. Sama zprava se dokaze odeslat? V serialu o SOLID? EmailWelcomeMessage bude asi abstraktni (nejaka spolecna logika pro vsechny zpravy), protoze zprav bude vic „druhu“ … vlastne to je jen EmailMessage … nepotrebuje mit odpovedost send() atd, ne?

d@rkWolf

Hmm, to je krásná ukázka toho, jak vyrobit luxusní, nepřehledný systém, v kterém se vyzná tak akorát autor, kdokoliv další to bude louskat týden a pak doufat.

lightWolf

A proc je to podle vas neprehledne? Jak by to tedy melo vypadat?

Enum a statická analýza kódu

Mám jednu univerzální radu pro začínající programátorty. V učení sice neexistují rychlé zkratky, ovšem tuhle radu můžete snadno začít používat a zrychlit tak tempo učení. Tou tajemnou ingrediencí je statická analýza kódu. Ukážeme si to na příkladu enum.

Pocta C64

Za prvopočátek své programátorské kariéry vděčím počítači Commodore 64. Tehdy jsem genialitu návrhu nemohl docenit. Dnes dokážu lehce nahlédnout pod pokličku. Chtěl bych se o to s vámi podělit a vzdát mu hold.