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

Zdroják » Různé » Návrhové principy: SOLID

Návrhové principy: SOLID

Články Různé

V současné době jsou stále populárnější návrhové vzory, které nám ukazují, jak řešit typické problémy při návrhu software v objektově orientovaných jazycích. Návrhové vzory jsou však jen konkrétní aplikace hlubších principů, na kterých by měl být objektově orientovaný návrh založen. Tento článek má za úkol čtenáře stručně seznámit s návrhovými principy SOLID, které formuloval Robert „Uncle Bob“ C. Martin.

Principy objektově orientovaného návrhu nám poskytují užitečná vodítka k rozhodování při návrhu software. Jejich cílem je pomoci vytvořit kvalitní návrh, který bude předcházet vzniku symptomů špatného designu, jakými jsou především:

  • Ztuhlost – Jakákoliv změna v software je velmi obtížná a vyžaduje řadu (nepředvídatelných) úprav na mnoha místech.
  • Křehkost – Jakákoliv provedená úprava způsobí problémy v jiných, často nesouvisejících, částech software.
  • Špatná znovupoužitelnost – Vyčlenění určité části software pro znovupoužití je složitější než tuto část vytvořit znovu.

Hlavním smyslem těchto principů je omezování závislostí mezi jednotlivými částmi software a omezování potřeb sekundárních úprav po každé změně. Závislosti, a to především ty skryté, jsou hlavní příčinou, proč software přestává být po určité době udržovatelný a vyžaduje zásadní změny návrhu, nebo rovnou vytvoření nové verze od začátku.

Těmto problémům se lze ve velké míře vyhnout, pokud se při návrhu řídíme dále uvedenými principy a používáme ověřené návrhové vzory.

Návrhové principy SOLID

Autorem této sady návrhových principů je Robert „Uncle Bob“ C. Martin. Název SOLID je akronym počátečních písmen pěti principů, které definoval.

1. Princip jedné odpovědnosti (Single Responsibility Principe – SRP)

„Třídy by měly mít jedinou zodpovědnost / jediný důvod ke změně.“

Jinými slovy, každá třída by měla mít zodpovědnost za právě jednu věc, která by měla být jasně vystižena jejím názvem. Z toho důvodu by i názvy tříd měly být pokud možno konkrétní.

Tento princip je považován za jeden z nejjednodušších, přesto je často porušován. Jeho aplikace vede k software složenému z většího množství jednoduchých tříd s jasně danou zodpovědností. Díky tomu je pro vývojáře jednodušší při úpravách software najít místa vyžadující změnu.

Častým argumentem proti SRP je, že systém složený z více tříd je složitější. Ve výsledku však takovýto systém neobsahuje více metod a vlastností než systém složený z několika velkých tříd – jsou pouze rozděleny do menších celků. Samozřejmě je však důležité, jako vždy, nic nepřehánět.

Existuje jednoduché vodítko ukazující na porušení tohoto principu: Pokud nelze zodpovědnost třídy popsat jednou jednoduchou větou bez použití spojky „a“, pravděpodobně porušuje princip jedné zodpovědnosti.

2. Princip otevřenosti a uzavřenosti (Open-Closed Principle – OCP)

„Třídy by měly být otevřené pro rozšiřování, ale uzavřené pro změny.“

Třídy by měly být psány tak, aby jejich funkčnost šla rozšířit bez nutnosti je modifikovat. Rozšíření funkčnosti by mělo být možné pouze tím, že přidáme nový kód bez nutnosti zasahovat do kódu existujícího. Díky tomu minimalizujeme možnost, že při úpravách neúmyslně poškodíme jiné části aplikace spoléhající na původní kód.

Klíčem k aplikaci tohoto principu je především abstrakce a polymorfismus. Třídy by tedy měly být vytvářeny tak, že z nich abstrahujeme společnou funkčnost a konkrétní implementační detaily, které se mohou měnit, definujeme až v odvozených třídách. Při potřebě vytvořit nový typ třídy s jinou funkčností pak můžeme pouze vytvořit nového potomka abstraktní třídy s vlastní implementací a nemusíme zasahovat do implementace dalších konkrétních tříd.

Podle Roberta C. Martina je tento princip nejdůležitějším ze všech SOLID principů. Nikdy jej však nelze dodržet dokonale. Vždy budou existovat změny vyžadující zásah do existujícího kódu. Už samotná snaha tyto případy minimalizovat ale vede k výraznému zkvalitnění návrhu.

3. Liskovové princip zaměnitelnosti (Liskov Substitution Principle – LSP)

„Podtřídy by měly být zaměnitelné s jejich bázovými třídami.“

Pokud nějaký kód používá bázovou třídu, musí fungovat i v případě, že místo bázové třídy použijeme jakoukoliv z jejích podtříd. Kódu používajícímu bázovou třídu tedy musí být jedno, zda dostane bázovou třídu nebo její podtřídu. Všechny podtřídy musí být z pohledu uživatele bázové třídy vzájemně zaměnitelné. Uživatel bázové třídy také nesmí být nucen měnit svoje chování podle typu obdržené podtřídy.

Toto pravidlo se může zdát samozřejmé, ale v praxi není tak složité jej porušit. Všechny odvozené třídy musí dodržovat tzv. kontrakt bázové třídy. Tento pojem vychází z metodiky Design By Contract. Ve stručnosti jde o to, že každá třída má nějaký kontrakt, který musí její uživatelé dodržet. Kontrakt je pro každou metodu třídy představován sadou podmínek, které musí být splněny před (tzv. precondition) a po (tzv. postcondition) jejím volání. Například určitá metoda může vyžadovat, že před jejím voláním byl pomocí jiných metod nastaven správný stav objektu. Nebo naopak metoda zaručuje, že po jejím volání bude objekt v určitém stavu.

Pokud má některá metoda odvozené třídy přísnější precondition než bázová třída, jde o porušení LSP. Kód používající bázovou třídu může splňovat podmínky vyžadované bázovou třídou, ale pro takovouto odvozenou třídu mohou být její přísnější podmínky porušeny. Stejně tak v případě postcondition, pokud odvozená třída nezaručí splnění všech podmínek, které splňovala bázová třída, může dojít k problémům, pokud kód používající bázovou třídu na splnění těchto podmínek spoléhal.

Jinými slovy: Odvozené třídy nesmí nikdy vyžadovat více a poskytovat méně než bázová třída.

4. Princip oddělení rozhraní (Interface Segregation Principle – ISP)

„Více specifických rozhraní je lepší než jedno univerzální rozhraní.“

Třídy by měly záviset pouze na těch rozhraních, která používají. Pokud definujeme pouze jedno velké rozhraní, ztrácíme přehled nad tím, které části rozhraní jsou používány kterými uživateli. Při změně v rozhraní třídy pak musíme vždy zkontrolovat a případně upravit všechny její uživatele.

Pokud tedy má třída více různých typů uživatelů, z nichž každý používá pouze definovanou část rozhraní, měli bychom pro každý typ uživatele vytvořit vlastní rozhraní. Uživatelé třídy by pak měli záviset pouze na těch rozhraních, která používají.

U tohoto principu je velmi důležité nezacházet do extrémů. Rozhraní by měla tvořit logická seskupení těch metod třídy, které se obvykle používají nebo mohou používat současně.

5. Princip obrácení závislostí (Dependency Inversion Principle – DIP)

„Závislosti by vždy měla být na abstraktním ne na konkrétním. Konkrétnější musí záviset na abstraktnějším ne naopak.“

Třídy vyšší úrovně abstrakce nesmí záviset na třídách nižší úrovně abstrakce. Všechny závislosti by tedy měly vést jedním směrem, a to od konkrétního k abstraktnímu. Všechny závislosti by měly být zaměřeny na rozhraní a abstraktní třídy, nikdy pouze na konkrétní implementaci.

Výhod, které přináší dodržování tohoto principu je hned několik. Jeho dodržování vede k výrazné redukci závislostí v kódu. Mnoho závislostí na konkrétních implementacích je nahrazeno závislostí na společném abstraktním rozhraní.

Konkrétní implementace se mění mnohem častěji než abstraktní rozhraní. Nikdy bychom neměli záviset na něčem, co se často mění, protože to pak může vyžadovat, aby se závislý kód také měnil.

Pokud třídy závisejí na abstraktních rozhraních a třídách, je velice snadné nahradit jednu konkrétní implementaci jinou. Kód je tedy velice flexibilní a mnoho změn lze provést pouze výměnou jedné konkrétní implementace za jinou. Tato výhoda je pak velmi výrazná v případě, že třídy testujeme a potřebujeme sledovat jejich chování. Můžeme jim pak jednoduše předat takové implementace, které budou jejich chování kontrolovat.

Pokud třída závisí pouze na abstraktních rozhraních, její znovupoužitelnost jinde je mnohem jednodušší. Třídu je možné bez úprav použít v prostředí tvořeném odlišnými implementacemi rozhraní, na kterých závisí.

Existují místa, kde se závislostí na konkrétní implementaci zbavuje těžko. Typickým případem je vytváření instancí. Pokud chceme vytvořit instalaci, nelze použít pouze abstraktní rozhraní, musíme sáhnout po konkrétní implementaci. Řešením je v tomto případě návrhový vzor Abstract Factory. Třída bude záviset pouze na abstraktní továrně vytvářející instance, aniž bychom museli znát jejich konkrétní implementaci.

Typickým příkladem porušení tohoto principu je výkonný kód, který závisí na prvcích uživatelského rozhraní. Časté změny uživatelského rozhraní si vynucují i změny ve výkonném kódu. Kód pak navíc nelze využít bez uživatelského rozhraní (například pokud bychom jej chtěli zpřístupnit jako webovou službu).

Závěr

Uvedené principy nám mohou posloužit jako vodítko při zvažování různých variant návrhu. Protože je ale vývoj software inženýrskou disciplínou, neexistuje v mnoha případech jedno správné řešení, ale je nutné volit mezi několika neideálními řešeními. V takovém případě přicházejí na řadu zkušenosti a intuice návrháře.

Zdroje a další informace

Podrobnější informace o návrhových principech SOLID lze nalézt na:

Komentáře

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

Dobry clanek
Pred rokem bych nerekl ze se budu na zdrojak chodit s tim ze se na nem dozvim neco uzitecneho. Jen tak dal

Martin Hassman

To jsme rádi. Děkujeme! 8-)

Rosret

Naprosto souhlasim :) Jen tak dal!

reflex

Dobry clanek, pouze bych u odbornych vyrazu zminil i slovicko v anglictine, ktere je rozhodne vice zazite nez jeho cesky prepis

ufak

Díky za připomenutí. Neustále zapomínám rozdělit odpovědnost do tříd a vyrábím hybridky a pak mám chaos i v hlavě a nakonec to stejně rozdělím.

Radek Miček

K 3): Je rozdíl mezi podtypem a podtřídou, a pokud vím, tak LSP definuje pojem podtyp. Problém s LSP je v tom, že se substituovatelnost těžko ověřuje (je ekvivalentní HP) a snadno poruší třeba při úpravě nadtřídy. Lze však formulovat omezení pro nadtřídy tak, že podtřídy pak budou automaticky podtypy. Tato omezení budou závislá na programovacím jazyce, kde pro běžně používané OO jazyky bude IMO dostačovat: žádné virtuální metody, žádná mutace dat, žádné veřejně přístupné členské funkce (tj. pouze private funkce) a zbytek kódu by samozřejmě neměl být schopen rozlišit typ pomocí mechanizmů jako je reflexe apod.

backup

Bingo!!

Ladislav Thon

LSP se dá formulovat i takhle: podtřídy musí být podtypy.

(V kontextu téhle diskuse je zajímavé, že zrovna Eiffel snadno umožňuje tohle pravidlo porušit, protože parametry metod jsou kovariantní :-) )

eif

Myslenka Design by Contract je nejlepe implementovana v Eiffelu, odkud pochazi a kde hraje klicovou ulohu.

Jenom skoda, ze Eiffel moc lidi nezna, vidim jej jako idealni prostredek pro vyuku OOP, byt treba pak nakonec clovek dela v Jave. Clovek si proste z Eiffelu odnese uzitecne navyky …

Ondřej Hanák

Matrixový příklad k LSP je na http://phpmaster.com/liskov-substitution-principle/.

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.