IT rekvalifikace s garancí práce. Seniorní programátoři vydělávají až 160 000 Kč/měsíc a rekvalifikace je prvním krokem. Zjisti, jak na to!
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í.

Lekce 1 - Návrhové vzory GRASP

Vítejte u komplexního článku, který vám osvětlí návrhové vzory ze skupiny GRASP. Ty sestavil Craig Larman, populární autor a programátor zabývající se návrhem a procesem vývoje softwaru. GRASP je akronym z General Responsibility Assignment Software Patterns, česky Obecné návrhové vzory přiřazení odpovědnosti. Otázka přidělení odpovědnosti je v OOP aplikacích stále přítomným problémem a jedním z nejdůležitějších pilířů kvalitní architektury. O významu přidělení odpovědnosti jsme hovořili také v kurzu Softwarové architektury a depencency injection. Na rozdíl např. od návrhových vzorů ze skupiny GOF se nejedná o konkrétní vzory implementace, ale spíše o dobré praktiky, tedy poučky. Z tohoto důvodu můžeme bez problému všechny vzory z GRASP popsat dnes v jediné lekci.

Obsah

GRASP obsahuje následující vzory:

  • Controller
  • Creator
  • High cohesion
  • Indirection
  • Information expert
  • Low coupling
  • Polymorphism
  • Protected variations
  • Pure fabrication

Pojďme si je popsat.

Controller

Pojem kontroler byste jakožto programátoři se zájmem o návrh softwaru měli dobře znát, minimálně v pojetí MVC architektury. Česky bychom jej mohli přeložit jako "ovladač". Jedná se o komponentu, jejíž rolí je komunikace s uživatelem. Kontroler nalezneme v určité podobě v podstatě ve všech dobře napsaných aplikacích. Např. ve formulářích v C# .NET se mu říká Code Behind, ale stále se jedná o kontroler. Když komunikaci s uživatelem zprostředkovává oddělená řídící třída, aplikace se rázem rozděluje do vrstev a logika je plně odstíněna od prezentace. Takové aplikace jsou přehledné a dobře udržitelné.

Ukažme si jednoduchý příklad. Předpokládejme, že programujeme kalkulačku. Odstrašující příklad monolitické aplikace by vypadal asi takto (jako jazyk použijme 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 metodě výše je smíchána komunikace s uživatelem (výpis a čtení z konzole) s aplikační logikou (samotným výpočtem). Metoda by v praxi samozřejmě počítala něco složitějšího, aby se ji vyplatilo napsat, představte si, že místo sčítání je nějaká složitější operace. Někdy také říkáme, že metoda má side effects, není tedy univerzální a její zavolání vyvolá i komunikaci s konzolí, která není na první pohled patrná. Tento problém je zde možná ještě dobře viditelný a aplikaci by vás nenapadlo takto napsat.

Méně viditelný může být problém v případě, když logiku píšeme přímo do obslužných metod ovládacích prvků formuláře. Určitě jste již někdy programovali formulářovou aplikaci. Možná jste viděli i takový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();
}

Zde kontroler, onu řídící třídu, znečišťujeme logikou (naším výpočtem). Ve všech aplikacích by vždy měla být jedna vrstva, která slouží pouze pro komunikaci s uživatelem, ať již lidským nebo třeba pomocí API. Tato vrstva by neměla chybět (první chybný kód) nebo by neměla dělat něco jiného (druhý chybný kód).

Správná podoba kódu konzolové kalkulačky by byla např. tato:

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));
}

Metoda main() je v tomto případě součástí kontroleru, který pouze komunikuje s uživatelem. Veškerá logika je zapouzdřena v třídách logické vrstvy, zde ve třídě Kalkulacka. Ta neobsahuje již žádnou práci s konzolí.

Oprava druhého řešení by vypadala stejně:

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 GRASP - Ostatní návrhové vzory

Vidíme, že parsování je stále role kontroleru, protože jde o zpracování vstupu. Stejně tak i změna hodnoty labelu vysledekLabel, což je zas výstup. Nicméně samotný výpočet je opět ve třídě Kalkulacka, která o formulářích vůbec neví.

Abychom měli ukázky univerzální, ukažme si ještě, jak se např. 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 kontrolerem:

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
    }

}

Šablona "auta.phtml" by vypadala např. takto:

<table border="1">
    <?php foreach ($auta as $auto) : ?>
        <tr>
            <td><?= htmlspecialchars($auto['spz']) ?></td>
            <td><?= htmlspecialchars($auto['barva']) ?></td>
        </tr>
    <?php endforeach ?>
</table>

Kontrolerem jsme oddělili logiku a prezentaci do 2 souborů a snížili počet vazeb.

Creator

Vzor Creator řeší do které třídy bychom měli umístit kód k vytvoření instance nějaké jiné třídy. Craig říká, že třída B instanciuje třídu A pokud:

1. Je A její částí

Příkladem 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.

Vzor Creator z GRASP - Ostatní návrhové vzory

2. Je A její závislostí

Třída B si vytvoří A, pokud na ni závisí. Příkladem by mohla být např. 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.

Vzor Creator z GRASP a závislosti - Ostatní návrhové vzory

3. Má pro instanciaci dostatek informací

Typicky je více možností, kam by vytvoření instance třídy logicky patřilo. Měli bychom jej ovšem umístit pouze tam, kde jsou již dostupné všechny informace, tedy proměnné nebo instance, které k vytvoření potřebujeme. Nedává smysl zbytečně natahovat další data do třídy, když je vše potřebné již někde k dispozici.

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ř. všechny faktury a z nich vybrat ty určitého zákazníka, instanciujeme SeznamFaktur ve SpravceFaktur, jelikož v něm se faktury nacházejí.

Vzor Creator z GRASP a informace - Ostatní návrhové vzory

4. B obsahuje A

Pokud je A vnořená třída ve třídě B, měla by být také její instance vytvářena třídou B. Nicméně vnořené třídy se nestaly příliš populárními.

Vzor Creator z GRASP a vnořená třída - Ostatní návrhové vzory

High cohesion

Vysoká soudržnost znamená, že se naše aplikace skládá z rozumně velkých kusů kódu, přičemž se každý tento kód 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), jelikož 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ř. soustředění funkcionality okolo uživatelů do třídy SpravceUzivatelu. Když by se přihlášení uživatele řešilo např. 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 právě High cohesion. 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 a tak za cenu pár řádek kódu navíc podporuje znovupoužitelnost a lepší čitelnost kódu. Indirection je jeden ze způsobů, jak dosáhnout Low coupling. 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.

Low coupling

Low coupling popisu 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), které 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ě). 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ě. Asi vás již napadlo, že takový manažer by pravděpodobně nešlo znovupoužít v jiné aplikaci.

Low coupling vzor z GRASP - Ostatní návrhové vzory

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ř. 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š.

Pozn.: Když jsme již zmínili božské objekty, uveďme si i opačný problém, který je tzv. 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é jako špatná praktika. 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ž by vám měl být princip polymorfismu 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, které mají každé metodu mluv(), ale přepisují si ji od předka Zvire, aby vydávala jejich specifický zvuk:

Zvířecí zvuky jako ukázka polymorfismu v OOP - Ostatní návrhové vzory

Pokud chcete reálnější ukázku polymorfismu, nabízí se např. předek pro formulářové ovládací prvky, kdy každý prvek poté přepisuje metody předka jako vykresli(), vratVelikost() a podobně podle toho, jak konkrétní potomci fungují.

Vzor Polymorphism z GRASP - Ostatní návrhové vzory

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 a 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ř. FacebookManagerXX, kde XX je verze Facebook API) a v systému se změní instance, která toto rozhraní implementuje. Rozhraní je samozřejmě možné definovat i pomocí polymorfismu a abstraktní třídy.

Vzor Protected variations z GRASP - Ostatní návrhové vzory

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.

To by bylo z GRASP vše, avšak v tomto kurzu si ukážeme i další návrhové vzory :)

V další lekci, Servant (Služebník), si ukážeme návrhový vzor Služebník, který se používá v situaci, kdy chceme skupině tříd přidat určitou funkčnost, aniž bychom ji do nich přímo zabudovali.


 

Všechny články v sekci
Ostatní návrhové vzory
Přeskočit článek
(nedoporučujeme)
Servant (Služebník)
Článek pro vás napsal David Hartinger
Avatar
Uživatelské hodnocení:
24 hlasů
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 se informační technologie naučil na Unicorn University - prestižní soukromé vysoké škole IT a ekonomie.
Aktivity