Vydělávej až 160.000 Kč měsíčně! Akreditované rekvalifikační kurzy s garancí práce od 0 Kč. Více informací.
Hledáme nové posily do ITnetwork týmu. Podívej se na volné pozice a přidej se do nejagilnější firmy na trhu - Více informací.

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:

Vzor Controller z GRASPO - Návrhové vzory

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ť.

Vzor Creator z GRASPO - Návrhové vzory

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.

Vzor Creator z GRASPO a závislosti - Návrhové vzory

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ú.

Vzor Creator z GRASPO a informácie - Návrhové vzory

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.

Vzor Creator z GRASPO a vnorená trieda - Návrhové vzory

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.

Low coupling vzor z GRASPO - Návrhové vzory

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:

Zvieracie zvuky ako ukážka polymorfizmu v OOP - Návrhové vzory

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ú.

Vzor Polymorphism z GRASPO - Návrhové vzory

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.

Vzor Protected Variations z GRASPO - Návrhové vzory

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.


 

Všetky články v sekcii
Návrhové vzory
Článok pre vás napísal David Hartinger
Avatar
Užívateľské hodnotenie:
Ešte nikto nehodnotil, buď prvý!
David je zakladatelem ITnetwork a programování se profesionálně věnuje 15 let. Má rád Nirvanu, nemovitosti a svobodu podnikání.
Unicorn university David sa informačné technológie naučil na Unicorn University - prestížnej súkromnej vysokej škole IT a ekonómie.
Aktivity