NOVINKA – Víkendový online kurz Software tester, který tě posune dál. Zjisti, jak na to!
NOVINKA - Online rekvalifikační kurz Java programátor. Oblíbená a studenty ověřená rekvalifikace - nyní i online.

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:

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

Š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ší.


 

Všechny články v sekci
Ostatní návrhové vzory
Přeskočit článek
(nedoporučujeme)
Návrhové vzory GRASP - Dokončení
Článek pro vás napsal David Hartinger
Avatar
Uživatelské hodnocení:
39 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