Vzhled
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 == nulla vytvořit dvě instance — řeší sesynchronized,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, JavaScriptaddEventListener - 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 iundo()) - 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
| Vzor | Klíčové znaky na diagramu |
|---|---|
| Singleton | privátní konstruktor + statická Instance property |
| Factory Method | abstraktní Creator + abstraktní FactoryMethod() + podtřídy |
| Observer | Subject s Attach/Detach/Notify + Observer s Update() |
| Command | rozhraní ICommand + Execute() + Invoker + Receiver odděleně |
Ostatní vzory (stručně)
| Vzor | Skupina | Jedna věta |
|---|---|---|
| Pool | Vytvářecí | Znovupoužívá existující instance místo vytváření nových (DB připojení) |
| Flyweight | Strukturální | Sdílí společná data mezi mnoha objekty, šetří paměť |
| Immutable | Strukturální | Objekt nelze po vytvoření změnit, změna = nový objekt (string v C#) |
| Template Method | Behaviorální | Rodič definuje kostru algoritmu, potomci doplní konkrétní kroky |
| Servant | Behaviorální | Sdílená funkčnost pro třídy bez společného předka |