Lekce 1 - Návrhové vzory GRASP
Vítejte v tutoriálu Návrhové vzory GRASP, které patří do vzorů přiřazení odpovědnosti. Začneme vzorem Controller, který odděluje logickou a prezentační část.
Úvod
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. Na rozdíl například od návrhových vzorů ze skupiny GOF se
nejedná o konkrétní vzory implementace, ale spíše o dobré
praktiky, tedy poučky.
O významu přidělení odpovědnosti jsme hovořili také v kurzu Softwarové architektury a depencency injection.
Seznam vzorů GRASP
GRASP obsahuje následující vzory:
- Controller,
- Creator,
- High cohesion,
- Indirection,
- Information expert,
- Low coupling,
- Polymorphism,
- Protected variations,
- Pure fabrication.
Dnes si probereme vzor Controller . Na ostatní vzory se můžete těšit v dalších tutoriálech.
Controller
Pojem kontroler, jakožto programátoři se zájmem o návrh softwaru určitě dobře známe minimálně v pojetí MVC architektury. Česky bychom jej mohli přeložit jako "ovladač".
Rolí kontroleru je komunikace s uživatelem.
Kontroler nalezneme v určité podobě v podstatě ve všech dobře napsaných aplikacích. Například v okenních aplikací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é.
Příklad
Ukažme si jednoduchý příklad. Předpokládejme, že programujeme kalkulačku.
UML diagram
UML diagram naší kalkulačky vypadá takto:
Špatná implementace
Ukažme si odstrašující příklad monolitické aplikace v konzolové a okenní aplikaci.
Konzolová aplikace
V konzolové aplikaci špatná implementace vypadá takto:
-
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; }
-
public int secti() { Scanner scanner = new Scanner(System.in); System.out.println("Zadej 1. číslo"); int a = scanner.nextInt(); System.out.println("Zadej 2. číslo"); int b = scanner.nextInt(); return a + b; }
-
function secti() { echo "Zadej 1. číslo\n"; $a = readline(); echo "Zadej 2. číslo\n"; $b = readline(); return $a + $b; }
-
const secti = () => { const a = parseInt(prompt('Zadej 1. číslo')); const b = parseInt(prompt('Zadej 2. číslo')); return a + b; }
-
def secti: a = int(input("Zadej 1. číslo\n")) b = int(input("Zadej 2. číslo\n")) 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. Někdy také říkáme, že metoda má side effects, není tedy univerzální. 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.
Okenní aplikace
Právě v okenní aplikaci může být problém méně viditelný v případě, když logiku píšeme přímo do obslužných metod ovládacích prvků formuláře. 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(); }
-
void sectiTlacitko() { int a = Integer.parseInt(cislo1.Text); int b = Integer.parseInt(cislo2.Text); vysledekLabel.Text = (a + b).ToString(); }
-
function sectiTlacitko() { $a = (int)$cislo1; $b = (int)$cislo2; $vysledek = "Výsledek: " . ($a + $b); }
-
function sectiTlacitko() { const a = parseInt(document.getElementById('cislo1').value); const b = parseInt(document.getElementById('cislo2').value); document.getElementById('vysledekLabel').innerText = "Výsledek: " + (a + b); }
-
def secti_tlacitko(): a = int(cislo1.get()) b = int(cislo2.get()) vysledek_label.config(text="Výsledek: " + str(a + b))
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 nebo by neměla dělat něco jiného.
Správná implementace
A teď se podívejme na správnou implementaci naší kalkulačky v konzolové a okenní aplikaci.
Konzolová aplikace
Správná implementace metody main()
v konzolové aplikaci
vypadá takto:
-
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)); }
-
public static void main(String[] args) { Kalkulacka kalkulacka = new Kalkulacka(); Scanner scanner = new Scanner(System.in); System.out.println("Zadej 1. číslo"); int a = Integer.parseInt(scanner.nextLine()); System.out.println("Zadej 2. číslo"); int b = Integer.parseInt(scanner.nextLine()); System.out.println(kalkulacka.secti(a, b)); }
-
function main() { $kalkulacka = new Kalkulacka(); echo "Zadej 1. číslo\n"; $a = (int)readline(); echo "Zadej 2. číslo\n"; $b = (int)readline(); echo $kalkulacka->secti($a, $b) . "\n"; }
-
function main() { const kalkulacka = new Kalkulacka(); const readline = require('readline-sync'); const a = parseInt(readline.question('Zadej 1. číslo: ')); const b = parseInt(readline.question('Zadej 2. číslo: ')); console.log(kalkulacka.secti(a, b)); }
-
def main(): kalkulacka = Kalkulacka() a = int(input("Zadej 1. číslo\n")) b = int(input("Zadej 2. číslo\n")) print(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í.
Okenní aplikace
Správné řešení implementace naší kalkulačky v okenní aplikaci bychom napsali takto:
-
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(); } }
-
class KalkulackaKontroler { private Kalkulacka kalkulacka = new Kalkulacka(); public void sectiTlacitko() { int a = Integer.ParseInt(cislo1.Text); int b = Integer.ParseInt(cislo2.Text); vysledekLabel.Text = (kalkulacka.secti(a, b)).ToString(); } }
-
class KalkulackaKontroler { function __construct() { $this->kalkulacka = new Kalkulacka(); } function sectiTlacitko() { $a = $inputA->value; $b = $inputB->value; echo "Výsledek: " . this.kalkulacka.secti($a, $b); } }
-
class KalkulackaKontroler { constructor() { this.kalkulacka = new Kalkulacka(); } sectiTlacitko_Click() { const a = parseInt(document.getElementById('cislo1').value); const b = parseInt(document.getElementById('cislo2').value); document.getElementById('vysledekLabel').innerText = "Výsledek: " + this.kalkulacka.secti(a, b); } }
-
class KalkulackaKontroler: def secti_tlacitko(self): a = int(self.cislo1.get()) b = int(self.cislo2.get()) self.vysledek_label.config(text="Výsledek: " + str(self.kalkulacka.secti(a, b)))
Vidíme, že parsování je stále role kontroleru, protože jde o
zpracování vstupu. Stejně tak i změna hodnoty labelu , což je zase výstup.
Nicméně samotný výpočet je opět ve třídě Kalkulacka
,
která o formulářích vůbec neví.
Příklad pro jazyk PHP
Abychom měli ukázky univerzální, ukažme si ještě příklad pro jazyk
PHP. Mějme tuto šablonu auta.phtml
:
<table border="1"> <?php foreach ($auta as $auto) : ?> <tr> <td><?= htmlspecialchars($auto['spz']) ?></td> <td><?= htmlspecialchars($auto['barva']) ?></td> </tr> <?php endforeach ?> </table>
A nyní si ukažme, jak se v PHP vypíše stránka:
- bez kontroleru,
- s kontrolerem.
Bez kontroleru
Výpis stránky bez kontroleru bychom mohli napsat takto:
<?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>
S kontrolerem
A s kontrolerem bychom ji napsali takto:
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 } }
Kontrolerem jsme oddělili logiku a prezentaci do dvou souborů a tím snížili počet vazeb.
V další lekci, Návrhové vzory GRASP - Dokončení, se budeme zabývat dalšími vzory GRASP pro přiřazení odpovědnosti. Budou to například Creator, High cohesion, Indirection a další.