Lekce 2 - Návrhové vzory GRASP - Dokončení
V minulé lekci, Návrhové vzory GRASP, jsme si uvedli vzory přiřazení odpovědnosti. Začali jsme vzorem Controller, který odděluje logickou a prezentační část.
V dnešním tutoriálu se budeme zabývat dalšími vzory GRASP pro přiřazení odpovědnosti. Budou to například Creator, High cohesion, Indirection a další.
Seznam vzorů GRASP
Připomeňme si vzory GRASP:
- Controller,
- Creator,
- High cohesion,
- Indirection,
- Information expert,
- Low coupling,
- Polymorphism,
- Protected variations,
- Pure fabrication.
Vzor Controller jsme probrali v lekci Návrhové vzory GRASP. Dnes si projdeme zbývající vzory.
Creator
Vzor Creator
řeší, do které třídy bychom měli umístit
kód k vytvoření instance nějaké jiné třídy. Mějme třídu
A
a B
. Třída B
instanciuje třídu
A
pokud:
- je třída
A
její částí, - je třída
A
její závislostí, - má pro instanciaci třídy
A
dostatek informací, - třída
B
obsahuje tříduA
.
Třída A
je částí třídy
B
Příkladem pro situaci, kdy třída A
je částí třídy
B
by mohly být třídy Faktura
a
PolozkaFaktury
. Jednotlivé instance položek faktury dává smysl
vytvářet ve třídě Faktura
, jelikož je její součástí.
Třída Faktura
zde má za položky odpovědnost:
Třída A
je závislostí
třídy B
V tomto případě si třída B
vytvoří třídu
A
, pokud na ni závisí. Příkladem by mohla být například
databáze podpisů, jejíž instanci si vytvoří třída Faktura
,
aby mohla na vygenerované faktuře zobrazit podpis:
Pokud je daná závislost použita ještě jinde, je výhodnější nevytvářet stále nové instance závislosti, ale použít vzor Dependency Injection. Více v kurzu Dependency injection a softwarové architektury.
Třída
B
má dostatek informací pro instanciaci třídy
A
Zde se jedná o výběr takové třídy B
, do které zbytečně
nemusíme natahovat další data, když jsou již k dispozici někde jinde. Jako
příklad si uveďme rozhodování, zda třídu SeznamFaktur
s
fakturami zákazníka instanciujeme ve třídě SpravceFaktur
nebo
SpravceZakazniku
. Podíváme se, která z tříd má všechny
informace, které SeznamFaktur
potřebuje. Pokud zde budeme
potřebovat například všechny faktury a z nich vybrat faktury od určitého
zákazníka, instanciujeme SeznamFaktur
ve
SpravceFaktur
, jelikož v něm se faktury nacházejí:
Třída B
obsahuje třídu
A
Pokud je třída A
vnořená ve třídě B
, měla
by být také instance třídy A
vytvářena třídou
B
. Nicméně vnořené třídy se nestaly příliš
populárními.
UML diagram této situace vypadá následovně:
High cohesion
Vysoká soudržnost znamená, že se naše aplikace skládá z rozumně velkých kusů kódu. Každý tento kód se zaměřuje na jednu věc. To je i jeden ze základních principů samotného OOP. Vysoká soudržnost úzce souvisí s nízkou provázaností (viz dále). Když sdružujeme související kód na jedno místo, snižuje se nutnost vazeb do dalších částí aplikace. Dalším souvisejícím vzorem je Law of Deméter, který v podstatě říká, že objekt by neměl "hovořit" s cizími objekty.
Příkladem vysoké soudržnosti je například soustředění funkcionality
okolo uživatelů do třídy SpravceUzivatelu
. Když by se
přihlášení uživatele řešilo například ve třídě
SpravceFaktur
, kde je přihlášení třeba pro zobrazení faktur,
a zrušení uživatelského účtu by se řešilo ve třídě
Uklizec
, která promazává neaktivní účty, porušovali bychom
High cohesion (viz dále). Kód, který má být pospolu ve třídě
SpravceUzivatelu
, by byl rozházený různě po aplikaci, podle
toho, kde je zrovna potřeba. Proto sdružujeme související kód na jedno
místo, a to i když se tyto metody používají v aplikaci třeba jen
jednou.
Indirection
Indirection je velmi zajímavý princip, se kterým jsme se již setkali u controlleru. Říká, že když vytvoříme někde v aplikaci umělého prostředníka, tedy třídu "navíc", může naši aplikaci paradoxně výrazně zjednodušit. U kontroleru jasně vidíme, že sníží počet vazeb mezi objekty. Za cenu pár řádek kódu navíc podpoříme znovupoužitelnost a lepší čitelnost kódu. Indirection je jeden ze způsobů, jak dosáhnout Low coupling (viz dále). Příklad jsme si již ukazovali u vzoru Controller.
Information expert
Informační expert je další poučka, která nám pomáhá se rozhodnout do jaké třídy přidáme metodu, atribut a podobně. Odpovědnost má vždy ta třída, která má nejvíce informací. Takové třídě potom říkáme informační expert a právě do ní přidáváme další funkcionalitu a data. O podobném principu jsme již mluvili u vzoru Creator v této lekci.
Low coupling
Low coupling je v podstatě totéž jako High cohesion, ale z jiného pohledu. V aplikaci bychom měli vytvářet co nejmenší počet vazeb mezi objekty, čehož dosáhneme chytrým rozdělením zodpovědnosti.
Jako odstrašující příklad si uveďme třídu Manager
, ve
které je umístěna logika pro práci se zákazníky, s fakturami, s
logistikou, zkrátka se vším. Takovýmto objektům se někdy říká
"božské" (god objects), protože mají příliš velkou odpovědnost a tím
pádem vytvářejí příliš mnoho vazeb (takový Manager
bude
typicky používat velkou spoustu tříd, aby mohl fungovat takto obecně). Asi
vás již napadlo, že takový manažer by pravděpodobně nešlo znovu použít
v jiné aplikaci. V aplikaci není důležitý celkový počet vazeb, ale počet
vazeb mezi dvěma objekty. Vždy se snažíme, aby třída komunikovala s co
nejmenším počtem dalších tříd, proto bychom měli uvést třídy
SpravceUzivatelu
, SpravceFaktur
,
SpravceLogistiky
a podobně.
Zde je diagram UML:
Odstrašující příklad božského objektu při nedodržování Low coupling
A nemusíme zůstávat jen u tříd. Low coupling souvisí také například
s dalšími praktikami ohledně pojmenovávání metod. Metodu bychom měli
pojmenovávat co nejméně slovy a bez spojky "a". Metody delej()
nebo naparsujAZpracujAVypis()
signalizují, že toho dělají
příliš.
Když jsme již zmínili božské objekty, uveďme si i opačný problém, který je tak zvaný Yoyo problém (problém joja). Při příliš drobné struktuře programu, příliš vysoké granularitě, často i nadužívání dědičnosti, je v programu tolik tříd, že programátor se musí stále přepínat dovnitř nějaké třídy, zjistit jak pracuje a vrátit se zpět. Tato akce může připomínat vrhání joja dolů a nahoru, znovu a znovu. Před dědičností se proto často preferuje skládání objektů.
Co se týká vazeb mezi objekty, měli bychom se také vyvarovat
cyklickým vazbám, které jsou všeobecně považované za špatnou
praktiku. To jsou případy, kdy třída A
odkazuje na třídu
B
a ta odkazuje zpět na třídu A
. Zde je někde v
návrhu něco špatně. Cyklická vazba může být i přes více tříd.
Polymorphism
Ano, i polymorfismus je návrhovým vzorem. I když je nám v principu polymorfismus dobře známý, zopakujme pro úplnost, že se jedná nejčastěji o případ, kdy potomek upravuje funkcionalitu svého předka, ale zachovává jeho rozhraní.
Z programátorského hlediska se jedná o přepisování (override) metod
předka. S objekty poté můžeme pracovat pomocí obecného rozhraní, ale
každý objekt si funkcionalitu zděděnou od předka upravuje po svém.
Polymorfismus nemusí být omezený jen na dědičnost, ale obecně na práci s
objekty různých typů pomocí nějakého společného rozhraní, které
implementují. Ukažme si pověstný příklad se zvířaty.
Každé zvíře má metodu mluv()
, ale přepisuje si ji od předka
Zvire
, aby vydávalo specifický zvuk:
Další ukázkou se nabízí například předek pro formulářové
ovládací prvky. Každý prvek poté přepisuje metody předka jako
vykresli()
, vratVelikost()
a podobně podle toho, jak
konkrétní potomci fungují:
Protected variations
Protected variations bychom mohli přeložit jako chráněné změny. Praktika hovoří o vytvoření stabilního rozhraní na klíčových místech aplikace, kde by změna rozhraní způsobila nutnost přepsat větší část aplikace.
Uveďme si opět reálný příklad. V systému ITnetwork používáme
princip Protected variations, konkrétně pomocí návrhového vzoru Adapter.
Tím se bráníme proti změnám, které neustále provádí Facebook ve svém
API. Přihlašování přes Facebook a podobné další integrace mají za
následek zvýšení počtu a aktivity uživatelů, bohužel ovšem za cenu
přepisování aplikace každých několik měsíců. Pomocí rozhraní
FacebookManagerInterface
se systém již nemusí nikdy měnit.
Když vyjde nová verze, kdy Facebook zas vše předělá, pouze se toto
rozhraní implementuje v jiné třídě, například
FacebookManagerXX
, kde XX
je verze Facebook API. V
systému se poté změní instance, která toto rozhraní implementuje.
Rozhraní je samozřejmě možné definovat i pomocí polymorfismu a abstraktní
třídy:
Pure fabrication
O Pure fabrication jsme již dnes také mluvili. Volně přeloženo jako "čistý výmysl" se jedná právě o třídy, které slouží pouze pro zjednodušení systému z hlediska návrhu. Tak jako byl controller případ indirection, tak je indirection případem Pure fabrication.
Servisní třídy mimo funkcionalitu aplikace snižují závislosti a zvyšují soudržnost.
V další lekci, Servant (Služebník), si ukážeme návrhový vzor Služebník, který přidává skupině tříd určitou funkčnost bez její přímé implementace do těchto tříd.