Návrhové vzory GRASPO
Vitajte u komplexného článku, ktorý vám osvetlí návrhové vzory zo skupiny GRASPO. Tie zostavil Craig Larman, populárny autor a programátor zaoberajúca sa návrhom a procesom vývoja softvéru. GRASPO je akronym z General Responsibility Assignment Software Patterns, česky Všeobecné návrhové vzory priradenie zodpovednosti. Otázka pridelenie zodpovednosti je v OOP aplikáciách stále prítomným problémom a jedným z najdôležitejších pilierov kvalitnej architektúry. O význame pridelení zodpovednosti sme hovorili tiež v kurze Softwarové architektúry a depencency injection. Na rozdiel napr. Od návrhových vzorov zo skupiny GOF sa nejedná o konkrétnej vzory implementácie, ale skôr o dobré praktiky, teda poučky. Z tohto dôvodu môžeme bez problému všetky vzory z GRASPO popísať dnes v jedinej lekciu.
Controller
Pojem kontrolér by ste ako programátori so záujmom o návrh softvéru mali dobre poznať, minimálne v poňatí MVC architektúry. Slovensky by sme ho mohli preložiť ako "ovládač". Jedná sa o komponent, ktorej úlohou je komunikácia s užívateľom. Kontrolér nájdeme v určitej podobe v podstate vo všetkých dobre napísaných aplikáciách. Napr. vo formulároch v C# .NET sa mu hovorí Code Behind, ale stále sa jedná o kontrolér. Keď komunikáciu s užívateľom sprostredkováva oddelená riadiaca trieda, aplikácia sa razom rozdeľuje do vrstiev a logika je plne tienené od prezentácie. Takéto aplikácie sú prehľadné a dobre udržateľné.
Ukážme si jednoduchý príklad. Predpokladajme, že programujeme kalkulačku. Odstrašujúci príklad monolitické aplikácie by vyzeral asi takto (ako jazyk použime C #):
public int Secti() { Console.WriteLine("Zadej 1. číslo"); int a = int.Parse(Console.ReadLine()); Console.WriteLine("Zadej 2. číslo"); int b = int.Parse(Console.ReadLine()); return a + b; }
V metóde vyššie je zmiešaná komunikácia s užívateľom (výpis a čítanie z konzoly) s aplikačnou logikou (samotným výpočtom). Metóda by v praxi samozrejme počítala niečo zložitejšieho, aby sa ju oplatilo napísať, predstavte si, že miesto sčítanie je nejaká zložitejšie operácie. Niekedy tiež hovoríme, že metóda má side effects, nie je teda univerzálny a jej zavolanie vyvolá aj komunikáciu s konzolou, ktorá nie je na prvý pohľad zrejmá. Tento problém je tu možno ešte dobre viditeľný a aplikácii by vás nenapadlo takto napísať.
Menej viditeľný môže byť problém v prípade, keď logiku píšeme priamo do obslužných metód ovládacích prvkov formulára. Určite ste už niekedy programovali formulárovom aplikáciu. Možno ste videli aj takýto kód:
public void SectiTlacitko_Click(Object sender) { int a = int.Parse(cislo1.Text); int b = int.Parse(cislo2.Text); vysledekLabel.Text = (a + b).ToString(); }
Tu kontrolér, onú riadiace triedu, znečisťujeme logikou (naším výpočtom). Vo všetkých aplikáciách by vždy mala byť jedna vrstva, ktorá slúži len pre komunikáciu s užívateľom, či už ľudským alebo napríklad pomocou API. Táto vrstva by nemala chýbať (prvý chybný kód) alebo by nemala robiť niečo iné (druhý chybný kód).
Správna podoba kódu konzolové kalkulačky by bola napr. Táto:
public static function main() { Kalkulacka kalkulacka = new Kalkulacka(); Console.WriteLine("Zadej 1. číslo"); int a = int.Parse(Console.ReadLine()); Console.WriteLine("Zadej 2. číslo"); int b = int.Parse(Console.ReadLine()); Console.WriteLine(kalkulacka.Secti(a, b)); }
Metóda main()
je v tomto prípade súčasťou kontroleru,
ktorý iba komunikuje s užívateľom. Všetka logika je zapuzdrená v triedach
logickej vrstvy, tu v triede Kalkulacka
. Tá neobsahuje už žiadnu
prácu s konzolou.
Oprava druhého riešenie by vyzerala rovnako:
class KalkulackaKontroler { private Kalkulacka kalkulacka = new Kalkulacka(); public void SectiTlacitko_Click(sender: Object) { int a = int.Parse(cislo1.Text); int b = int.Parse(cislo2.Text); vysledekLabel.Text = (kalkulacka.Secti(a, b)).ToString(); } }
A UML diagram:
Vidíme, že parsování je stále rola kontroleru, pretože ide o
spracovanie vstupu. Rovnako tak aj zmena hodnoty labelu
vysledekLabel
, čo je zas výstup. Avšak samotný výpočet je
opäť v triede Kalkulacka
, ktorá o formulároch vôbec nevie.
Aby sme mali ukážky univerzálne, ukážme si ešte, ako sa napr. V PHP vypisuje stránka bez kontroleru:
<?php $databaze = new PDO('mysql:host=localhost;dbname=testdb;charset=utf8mb4', 'jmeno', 'heslo'); $auta = $databaze->query("SELECT * FROM auta")->fetchAll(); ?> <table> <?php foreach ($auta as $auto) : ?> <tr> <td><?= htmlspecialchars($auto['spz']) ?></td> <td><?= htmlspecialchars($auto['barva']) ?></td> </tr> <?php endforeach ?> </table>
A s kontrolerom:
class AutaKontroler { private $spravceAut; public function __construct() { $this->spravceAut = new SpravceAut(); } public function vsechna() { $auta = $this->spravceAut->vratAuta(); // Proměnná pro šablonu require('Sablony/auta.phtml'); // Načtení šablony } }
Šablóna "auta.phtml" by vyzerala napr. Nasledovne:
<table border="1"> <?php foreach ($auta as $auto) : ?> <tr> <td><?= htmlspecialchars($auto['spz']) ?></td> <td><?= htmlspecialchars($auto['barva']) ?></td> </tr> <?php endforeach ?> </table>
Kontrolerom sme oddelili logiku a prezentáciu do 2 súborov a znížili počet väzieb.
Creator
Vzor Creator
rieši do ktorej triedy by sme mali
umiestniť kód na vytvorenie inštancie nejakej inej triedy. Craig
hovorí, že trieda B
instanciuje triedu A
ak:
1. Je A jej častí
Príkladom by mohli byť triedy Faktura
a
PolozkaFaktury
. Jednotlivé inštancie položiek faktúry dáva
zmysel vytvárať v triede Faktura
, pretože je jej súčasťou.
Trieda Faktura
tu má za položky zodpovednosť.
2. Je A jej závislosťou
Trieda B
si vytvorí A
, pokiaľ na ňu závisí.
Príkladom by mohla byť napr. Databázy podpisov, ktorej inštanciu si vytvoria
trieda Faktura
, aby mohla na vygenerovanej faktúre zobraziť
podpis. Ak je daná závislosť použitá ešte inde, je výhodnejšie
nevytvárať stále nové inštancie závislosti, ale použiť vzor Dependency Injection.
3. Má pre instanciaci dostatok informácií
Typicky je viac možností, kam by vytvorenie inštancie triedy logicky patrilo. Mali by sme ho však umiestniť iba tam, kde sú už dostupné všetky informácie, teda premenné alebo inštancie, ktoré k vytvoreniu potrebujeme. Nedáva zmysel zbytočne naťahovať ďalšie dáta do triedy, keď je všetko potrebné už niekde k dispozícii.
Ako príklad si uveďme rozhodovaní, či triedu SeznamFaktur
s
faktúrami zákazníka instanciujeme v triede SpravceFaktur
alebo
SpravceZakazniku
. Pozrieme sa, ktorá z tried má všetky
informácie, ktoré SeznamFaktur
potrebuje. Pokiaľ tu budeme
potrebovať napr. Všetky faktúry az nich vybrať tie určitého zákazníka,
instanciujeme SeznamFaktur
vo SpravceFaktur
, pretože
v ňom sa faktúry nachádzajú.
4. B obsahuje A
Ak je A
vnorená trieda v triede B
, mala by byť aj
jej inštancie vytváraná triedou B
. Avšak vnorené triedy sa
nestali príliš populárnymi.
High cohesion
Vysoká súdržnosť znamená, že sa naše aplikácie skladá z rozumne veľkých kusov kódu, pričom sa každý tento kód zameriava na jednu vec. To je aj jeden zo základných princípov samotného OOP. Vysoká súdržnosť úzko súvisí s nízkou previazanosťou (pozri ďalej), pretože keď združujeme súvisiace kód na jedno miesto, znižuje sa nutnosť väzieb do ďalších častí aplikácie. Ďalším súvisiacim vzorom je Law of Demeter, ktorý v podstate hovorí, že objekt by nemal "hovoriť" s cudzími objektmi.
Príkladom vysokej súdržnosti je napr. Sústredenie funkcionality okolo
užívateľov do triedy SpravceUzivatelu
. Keď by sa prihlásenie
užívateľa riešilo napr. V triede SpravceFaktur
, kde je
prihlásenie potrebné pre zobrazenie faktúr, a zrušenie užívateľského
účtu by sa riešilo v triede Uklizec
, ktorá premazáva
neaktívne účty, porušovali by sme práve High cohesion. Kód, ktorý má
byť pospolu v triede SpravceUzivatelu
, by bol rozhádzaný rôzne
po aplikácii, podľa toho kde je práve potreba. Preto združujeme súvisiace
kód na jedno miesto a to aj keď sa tieto metódy používajú v aplikácii
napríklad len raz.
InDirection
InDirection je veľmi zaujímavý princíp, s ktorým sme sa už stretli u Controlleru. Hovorí, že keď vytvoríme niekde v aplikácii umelého prostredníka, teda triedu "naviac", môže našu aplikáciu paradoxne výrazne zjednodušiť. U kontroleru jasne vidíme, že zníži počet väzieb medzi objektmi a tak za cenu pár riadkov kódu navyše podporuje znovupoužitelnost a lepšiu čitateľnosť kódu. InDirection je jeden zo spôsobov, ako dosiahnuť Low coupling. Príklad sme si už ukazovali u vzore Controller.
Information expert
Informačný expert je ďalší poučka, ktoré nám pomáha sa rozhodnúť do akej triedy pridáme metódu, atribút a podobne. Zodpovednosť má vždy tá trieda, ktorá má najviac informácií. Takéto triede potom hovoríme informačný expert a práve do nej pridávame ďalšiu funkcionalitu a dáta. O podobnom princípe sme už hovorili u vzore Creator.
Low coupling
Low coupling popisu v podstate to isté ako High cohesion, ale z iného pohľadu. V aplikácii by sme mali vytvárať čo najmenší počet väzieb medzi objektmi, čo dosiahneme šikovným rozdelením zodpovednosti.
Ako odstrašujúci príklad si uveďme triedu Manager
, v ktorej
je umiestnená logika pre prácu so zákazníkmi, s faktúrami, s logistikou,
skrátka so všetkým. Takýmto objektom sa niekedy hovorí "božské" (god
objects), ktoré majú príliš veľkú zodpovednosť a tým pádom vytvárajú
príliš veľa väzieb (taký Manager
bude typicky používať
veľkú veľa tried, aby mohol fungovať takto všeobecne). V aplikácii nie je
dôležitý celkový počet väzieb, ale počet väzieb medzi dvoma objektmi.
Vždy sa snažíme, aby trieda komunikovala s čo najmenším počtom ďalších
tried, preto by sme mali uviesť triedy SpravceUzivatelu
,
SpravceFaktur
, SpravceLogistiky
a podobne. Asi vás
už napadlo, že takýto manažér by pravdepodobne nešlo znovupoužitie v inej
aplikácii.
Odstrašujúci príklad božského objektu pri nedodržiavaní Low coupling
A nemusíme zostávať len u tried. Low coupling súvisí tiež napr. S
ďalšími praktikami ohľadom pomenovávaní metód ( "Metódu by sme mali
pomenovávať čo najmenej slovami a bez spojky A"). Metódy
delej()
alebo naparsujAZpracujAVypis()
signalizujú,
že toho robia príliš.
Pozn .: Keď sme už spomenuli božské objekty, uveďme si aj opačný problém, ktorý je tzv. Yoyo problém (problém joja). Pri príliš drobné štruktúre programu, príliš vysoké granularitě, často i nadužívanie dedičnosti, je v programe toľko tried, že programátor sa musí stále prepínať dovnútra nejaké triedy, zistiť ako pracuje a vrátiť sa späť. Táto akcia môže pripomínať vrhanie joja dole a hore, znovu a znovu. Pred dedičnosťou sa preto často preferuje skladanie objektov.
Čo sa týka väzieb medzi objektmi, mali by sme sa tiež vyvarovať
cyklickým väzbám, ktoré sú všeobecne považované ako zlá
praktika. To sú prípady, keď trieda A
odkazuje na triedu
B
a tá odkazuje späť na triedu A
. Tu je niekde v
návrhu niečo zle. Cyklická väzba môže byť aj cez viac tried.
Polymorphism
Áno, aj polymorfizmus je návrhovým vzorom. Aj keď by vám mal byť
princíp polymorfizmu dobre známy, zopakujme pre úplnosť, že ide
najčastejšie o prípad, kedy potomok upravuje funkcionalitu svojho
predka, ale zachováva jeho rozhrania. Z programátorského hľadiska
ide o prepisovanie (override) metód predka. S objekty potom môžeme pracovať
pomocou všeobecného rozhrania, ale každý objekt si funkcionalitu zdedenú od
predka upravuje po svojom. Polymorfizmus nemusí byť obmedzený len na
dedičnosť, ale všeobecne na prácu s objektmi rôznych typov pomocou
nejakého spoločného rozhrania, ktoré implementujú. Ukážme si povestný
príklad so zvieratami, ktoré majú každé metódu mluv()
, ale
prepisujú si ju od predka Zvire
, aby vydávala ich špecifický
zvuk:
Pokiaľ chcete reálnejšie ukážku polymorfizmu, ponúka sa napr. Predok
pre formulárové ovládacie prvky, kedy každý prvok potom prepisuje metódy
predka ako vykresli()
, vratVelikost()
a podobne podľa
toho, ako konkrétne potomkovia fungujú.
Protected Variations
Protected variations by sme mohli preložiť ako chránené zmeny. Praktika
hovorí o vytvorenie stabilného rozhranie na kľúčových miestach
aplikácie, kde by zmena rozhrania spôsobila nutnosť prepísať väčšiu
časť aplikácie. Uveďme si opäť reálny príklad. V systéme
ITnetwork používame princíp Protected variations, konkrétne pomocou
návrhového vzoru Adapter
a tým sa bránime proti zmenám, ktoré neustále vykonáva Facebook vo svojom
API. Prihlasovanie cez Facebook a podobné ďalšie integrácie majú za
následok zvýšenie počtu a aktivity užívateľov, bohužiaľ avšak za cenu
prepisovanie aplikácie každých niekoľko mesiacov. Pomocou rozhrania
FacebookManagerInterface
sa systém už nemusí nikdy meniť. Keď
vyjde nová verzia, kedy Facebook zas všetko prerobí, len sa toto rozhranie
implementuje v inej triede (napr. FacebookManagerXX
, kde XX je
verzia Facebook API) a v systéme sa zmení inštancia, ktorá toto rozhranie
implementuje. Rozhranie je samozrejme možné definovať aj pomocou polymorfizmu
a abstraktné triedy.
Pure fabrication
O Pure fabrication sme už dnes tiež hovorili. Voľne preložené ako "čistý výmysel" sa jedná práve o triedy, ktoré slúžia len pre zjednodušenie systému z hľadiska návrhu. Tak ako bol controller prípad inDirection, tak je inDirection prípadom Pure fabrication. Servisné triedy mimo funkcionalitu aplikácie znižujú závislosti a zvyšujú súdržnosť.
To by bolo z GRASPO všetko a ja sa na vás budem tešiť u ďalších on-line kurzov na sieti ITnetwork.