Skip to content

5 • Návrhové vzory (DP)

Singleton, Observer, Factory Method, Command

Definice

  • opakovaně použitelná řešení často se opakujících problémů v návrhu OOP softwaru
  • nejsou to hotové knihovny nebo kus kódu - jsou to způsoby, jak strukturovat třídy a jejich vztahy
  • původ: kniha Design Patterns: Elements of Reusable Object-Oriented Software (1994) od čtveřice autorů známé jako Gang of Four (GoF)
    • Gamma, Helm, Johnson, Vlissides
    • definovali 23 vzorů rozdělených do tří kategorií

Tři kategorie GoF

  • vytvářecí (creational)
    • řeší, jak vytvářet objekty, aby kód nebyl pevně svázaný s konkrétními třídami
    • Singleton, Factory Method, Builder, Abstract Factory, Prototype…
  • strukturální (structural)
    • řeší, jak skládat třídy/objekty do větších celků
    • Adapter, Composite, Facade…
  • behaviorální (behavioral)
    • řeší komunikaci mezi objekty a rozdělení odpovědnosti
    • Observer, Command, Strategy, State…

Singleton (vytvářecí)

Účel: zaručí, že daná třída má právě jednu instanci v celé aplikaci, a poskytne k ní globální přístup.

Princip:

  • privátní konstruktor (nikdo zvenku nemůže udělat new)
  • statická metoda getInstance(), která instanci vrátí (a při prvním volání vytvoří)
  • statické pole drží referenci
csharp
class Logger {
    private static Logger instance;
    private Logger() {}                  // nikdo zvenku nemůže new
    public static Logger getInstance() {
        if (instance == null)
            instance = new Logger();     // lazy initialization
        return instance;
    }
}

Použití: logger, konfigurace, cache, Unity: třídy typu GameManager, AudioManager.

Výhody:

  • jistota, že existuje jen jedna instance (např. jeden zápis do log souboru)
  • globální přístup z celé aplikace
  • lazy initialization - instance se vytvoří až když je potřeba

Nevýhody:

  • v podstatě globální proměnná → skryté závislosti mezi částmi kódu
  • porušuje Single Responsibility Principle (třída řeší jak svou logiku, tak svůj životní cyklus)
  • špatně se testuje (nejde mockovat, drží stav mezi testy)
  • problémy ve vícevláknovém prostředí (dvě vlákna mohou současně projít if == null a vytvořit dvě instance — řeší se synchronized, double-checked locking, nebo enum implementací)
  • často považován za anti-pattern, pokud se používá místo dependency injection

Observer (behaviorální)

Účel: definuje vztah 1:N mezi objekty – když jeden objekt (Publisher) změní stav, všichni jeho odběratelé (Subscribers / Observers) jsou automaticky informováni.

23 • Událostmi řízené programování

  • základ událostmi řízeného programování
  • Publisher posílá událost přihlášeným Subscriberům

Princip:

  • Publisher drží seznam observerů a metody subscribe(), unsubscribe(), notify()
  • Subscriber implementuje rozhraní s metodou update(), kterou Subject volá
  • Subscriber se sám přihlásí, Publisher ho jen obslouží - nezná konkrétní typy odběratelů

Použití:

  • GUI eventy (kliknutí na tlačítko → handler)
  • C# event / delegate, JavaScript addEventListener
  • reaktivní knihovny (RxJS)
  • MVC - View pozoruje Model
  • pub/sub messaging (Apache Kafka, MQTT)

výhody:

  • loose coupling - Subject a Observer se znají jen přes rozhraní
  • dynamické přihlašování/odhlašování za běhu
  • jeden Subject může mít libovolný počet odběratelů různých typů

nevýhody:

  • memory leaks, pokud se observer zapomene odhlásit (Subject ho drží v seznamu, GC ho neuvolní)
  • neřízené pořadí notifikací - nemůžeš spoléhat, kdo dostane update první
  • těžké debugovat - když přijde event, není okamžitě jasné, kdo všechno na něj zareaguje (kaskáda updatů)
  • riziko nekonečné smyčky (Observer při updatu změní Subject → další notifikace → …)

Factory Method (vytvářecí)

Účel: umožňuje třídě delegovat vytváření instancí na své podtřídy. Klient pracuje s abstraktním rozhraním, neví, jaký konkrétní typ dostane.

Princip:

  • abstraktní třída / interface s metodou createProduct() (= Factory Method)
  • každá podtřída ji přepíše a vrátí jiný konkrétní typ
  • zbytek kódu v rodičovské třídě používá createProduct() aniž by věděl, co přesně vznikne
java
abstract class Dialog {
    void render() {
        Button b = createButton();   // Factory Method
        b.render();
    }
    abstract Button createButton();  // podtřídy rozhodnou
}

class WindowsDialog extends Dialog {
    Button createButton() { return new WindowsButton(); }
}
class MacDialog extends Dialog {
    Button createButton() { return new MacButton(); }
}

Použití:

  • cross-platform UI (jeden kód, různé widgety podle OS)
  • různé typy dokumentů v textovém editoru
  • parsery (XML/JSON/YAML) vybírané podle vstupu
  • ORM - EntityManager.createQuery()

Výhody:

  • Open/Closed Principle - přidání nového typu znamená přidat podtřídu, ne měnit existující kód
  • odděluje vytváření objektu od jeho použití
  • klient nezávisí na konkrétních třídách, jen na rozhraní

Nevýhody:

  • víc tříd = vyšší složitost u jednoduchých případů
  • vyžaduje hierarchii dědičnosti (Factory Method = polymorfismus)
  • pro lidi nečtoucí GoF může být zbytečně abstraktní

Command (behaviorální)

Účel: zabalí požadavek (akci) jako objekt. Místo přímého volání metody vznikne objekt, který nese informaci co udělat a na čem - dá se uložit do fronty, logovat, vrátit zpět (undo), zopakovat.

Princip - typické role:

  • Command - rozhraní s metodou execute() (často i undo())
  • ConcreteCommand - implementace konkrétní akce, drží odkaz na receivera a parametry
  • Receiver - objekt, který akci skutečně provede
  • Invoker - spouštěč (např. tlačítko), neví nic o tom, co Command dělá
  • Client - vytváří Command a předává ho Invokerovi
java
interface Command { void execute(); void undo(); }

class CopyCommand implements Command {
    Editor editor;
    String backup;
    CopyCommand(Editor e) { editor = e; }
    void execute() { backup = editor.getSelection(); 
                     editor.copyToClipboard(); }
    void undo()    { /* clipboard zpět */ }
}

class Button {                    // Invoker
    Command command;
    void click() { command.execute(); }
}

Použití:

  • Undo/Redo (Ctrl+Z) (Photoshop, IDE, Word): historie je stack commandů
  • makra - sekvence commandů, kterou lze přehrát
  • GUI tlačítka a menu - stejná akce dosažitelná z víc míst (toolbar, klávesa, menu)
  • queueing / scheduling - joby ve frontě, transakce v DB
  • logování operací (audit log, event sourcing)
  • network requesty (zabalit požadavek, poslat po síti, provést na druhé straně)

Výhody:

  • odděluje odesílatele akce (Invoker) od příjemce (Receiver) - tlačítko neví, co se po stisku stane
  • snadno se realizuje undo/redo (každý Command si pamatuje, jak se vrátit)
  • commandy lze kombinovat (makro = složený command), frontovat, logovat, odložit
  • Open/Closed - nová akce = nová třída, žádné zásahy do existujícího kódu

Nevýhody:

  • hodně malých tříd - pro každou akci vlastní třída
  • pro jednoduché případy overengineering (proč Command, když stačí volat metodu?)
  • undo bývá komplikovaný u akcí, které mají vedlejší efekty (síťové volání, soubor smazaný z disku)

Výhody návrhových vzorů obecně

  • ověřená řešení - neřešíš problém od nuly, používáš to, co fungovalo tisíckrát
  • společný slovník - když řekneš "udělej to jako Observer", ostatní vývojáři okamžitě vědí, co tím myslíš
  • zlepšují udržovatelnost a rozšiřitelnost kódu
  • podporují principy SOLID (zejména Open/Closed a Dependency Inversion)
  • usnadňují komunikaci v týmu a code review

Nevýhody návrhových vzorů obecně

  • zbytečná složitost (overengineering) - vzor přidaný "protože je elegantní" zhorší kód
  • riziko nadužívání - někdy stačí obyčejná funkce nebo if
  • vyšší vstupní bariéra pro nezkušené vývojáře (víc tříd, víc abstrakce, hůř se čte)
  • mohou překrývat jednoduchá řešení - místo přímočarého kódu se objeví hierarchie pěti tříd
  • vzory z roku 1994 jsou částečně kompenzací slabin starších jazyků - v moderním Pythonu, JavaScriptu nebo Kotlinu se některé řeší vestavěnými prostředky (lambdy, first-class funkce) bez celé struktury tříd
  • Singleton je v komunitě často považován přímo za anti-pattern

Rychlý tahák - jak poznat vzor

VzorKlíčové znaky na diagramu
Singletonprivátní konstruktor + statická Instance property
Factory Methodabstraktní Creator + abstraktní FactoryMethod() + podtřídy
ObserverSubject s Attach/Detach/Notify + Observer s Update()
Commandrozhraní ICommand + Execute() + Invoker + Receiver odděleně

Ostatní vzory (stručně)

VzorSkupinaJedna věta
PoolVytvářecíZnovupoužívá existující instance místo vytváření nových (DB připojení)
FlyweightStrukturálníSdílí společná data mezi mnoha objekty, šetří paměť
ImmutableStrukturálníObjekt nelze po vytvoření změnit, změna = nový objekt (string v C#)
Template MethodBehaviorálníRodič definuje kostru algoritmu, potomci doplní konkrétní kroky
ServantBehaviorálníSdílená funkčnost pro třídy bez společného předka