SOLID – (S)ingle Responsibility Principle

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.
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:
- Změna textu nebo formátování emailu
- 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.
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á“.
Ahoj, díky za komentář.
Já se snažím vyvíjet podle TDD, aby bugů bylo co nejméně a design byl co nejvíce jednoduchý a flexibilní.
Jednoduchý a flexibilní design = dodržování SOLIDu. Aby se dobře testovalo tak jsou třídy malé a každá má jasnou responsibilitu (SRP). Jelikož jsou třídy malé tak jsou i solidně pokryté testy. Jelikož jsou solidně pokryté testy tak se do nich nebojím šáhnout :)
V realitě (teda aspoň v té mojí) se věci mění rychle, to je prostě vývoj. Bát se šáhnout do nějakého kódu je cesta do pekel a proto se snažím udělat vše proto abych se nebál = testy testy a zase testy.
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.
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í.
Ahoj, díky moc, to jsem rád.
Co se týče DI containeru tak jsem injectováni dependencies snažil řešit spíše jiným způsobem, např přes Factories a Abstract Factories. Záleží na velikosti projektu a na tom co od toho DI containeru vlastně očekáváš.
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?
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.
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.
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:
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í…
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?
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ů.
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.
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)
. Pakuž s minimální prací můžeme zprávu poslat jak e-mailem, tak třeba jako notifikaci na mobil.
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.
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?
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.
A proc je to podle vas neprehledne? Jak by to tedy melo vypadat?