Přidej si svou IT školu do profilu a najdi spolužáky zde na síti :)

Návrhové vzory GRASP

Návrh Návrhové vzory Návrhové vzory GRASP

ONEbit hosting Unicorn College Tento obsah je dostupný zdarma v rámci projektu IT lidem. Vydávání, hosting a aktualizace umožňují jeho sponzoři.

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.

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

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

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

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

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

High cohesion

Vysoká soudruž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á soudruž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é soudruž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, 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

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š nízké 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. 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:

Mluvící zvířata jako ukázka polymorfismu v OOP

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

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

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í soudružnost.

To by bylo z GRASP vše a já se na vás budu těšit u dalších on-line kurzů na síti ITnetwork.


 

 

Článek pro vás napsal David Čápka
Avatar
Jak se ti líbí článek?
4 hlasů
Autor pracuje jako softwarový architekt a pedagog na projektu ITnetwork.cz (a jeho zahraničních verzích). Velmi si váží svobody podnikání v naší zemi a věří, že když se člověk neštítí práce, tak dokáže úplně cokoli.
Unicorn College Autor se informační technologie naučil na Unicorn College - prestižní soukromé vysoké škole IT a ekonomie.
Miniatura
Předchozí článek
Návrhové vzory GOF
Miniatura
Všechny články v sekci
Návrhové vzory
Miniatura
Následující článek
Servant (Služebník)
Aktivity (5)

 

 

Komentáře

Avatar
Daniel Kosík (Ghost4Man):1. října 22:58

V části Správná podoba kódu konzolové kalkulačky:

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

Na konci v kalkulacka.Sec­ti() kalkulačka přece neví, co má sčítat. Neměly by tam být argumenty a a b?

Odpovědět 1. října 22:58
Všechno na světě můžete chtít. Jenom nemůžete chtít, aby se vám chtělo.
Avatar
David Čápka
Tým ITnetwork
Avatar
Odpovídá na Daniel Kosík (Ghost4Man)
David Čápka:2. října 0:02

Jasně, měly, díky, už jsem to opravil :)

Odpovědět 2. října 0:02
Miluji svou práci a zdejší komunitu, baví mě se rozvíjet, děkuji každému členovi za to, že zde působí.
Avatar
berykubik
Redaktor
Avatar
berykubik:2. října 9:54

Polymorfismus je o jednotném rozhraní, za kterým se skrývají různé implementace, popř. o schopnosti jednoho objektu vystupovat pod různými rozhraními, s dědičností a předkem to nemusí mít nic společného.

 
Odpovědět  +1 2. října 9:54
Avatar
David Čápka
Tým ITnetwork
Avatar
Odpovídá na berykubik
David Čápka:2. října 10:18

Máš pravdu, že je to hlavně o rozhraní, text jsem upravil, díky :)

Odpovědět  +1 2. října 10:18
Miluji svou práci a zdejší komunitu, baví mě se rozvíjet, děkuji každému členovi za to, že zde působí.
Děláme co je v našich silách, aby byly zdejší diskuze co nejkvalitnější. Proto do nich také mohou přispívat pouze registrovaní členové. Pro zapojení do diskuze se přihlas. Pokud ještě nemáš účet, zaregistruj se, je to zdarma.

Zobrazeno 4 zpráv z 4.