Návrhové vzory

Odpřednášel a zapsal Vráťa Kalenda – díky!

Návrhové vzory dávají programátorovy "Best practices", jakýsi návod, jak daný programátorský problém úspěšně řešili lidé před ním. Neříkají "co" naprogramovat, ale "JAK" to "co" naprogramovat. Jinými slovy to znamená, že návrhové vzory nespadají do fáze analýzy problémové domény, ale do fáze architektury a návrhu softwarového projektu.

Důležitou knihou pro Návrhové Vzory byla pulikace: Gamma, Erich; Richard Helm, Ralph Johnson, and John Vlissides (1995). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley. ISBN 0-201-63361-2.

Tato kniha představila většinu dnes známých vzorů. Někdy se vzorům říká taky "Gang Of Four", "GoF Patterns" a to kvůli tomu, že autoři této knihy jsou 4.

Základní myšlenky společné pro všechny návrhové vzory

Interfacový přístup k problému – Jedna ze stěžejních myšlenek, která je společná pro všechny návrhové vzory je, že se jejich autoři snaží na daný kus softwaru dívat jako na skupinu interfaců, co si mezi sebou volají metody. Implementace, co stojí za těmito interfacy je až druhořadá. Důležité je, aby každá část systému měla svůj jasný a zřetelný interface. Pokud si dobře uvědomíme, kde některá část systému končí, a jiná začíná a tuto linii vytvoříme ze spolupracujících interfaců, pak tím předcházíme sphagetti kódu, umožňujeme snadnou testovatelnost a vyměnitelnost komponent.

Doporučení autorů návrhových vzorů zní ve zkratce: „Zaměřte se na přístup přes spolupráci pomocí interfaců a nikoliv na implementaci objektů“

(V ruby "bohužel" interfacy neexistují. V PHP již ano a Java nebo C# by se bez nich neobešla. Interfacem ale nemusí být doslova přímo intefacový konstrukt v jazyku. V ruby může skoro stejně dobře posloužit jasně nadefinovaná a zdokumentovaná třída popřípadě modul.)

Polymorfismus jako hlavní nástroj návrhových vzorů – Polymorfismus znamená, že na instanci dané třídy se můžeme koukat také jako na instanci třídy jednoho ze svých předků, popřípadě jakožto něco, co implementuje daný interface.

Představme si, že píšeme umělou inteligenci pro jednu nejmenovanou strategickou hru ve vesmíru. Máme základní třídu Jendotka a potom odvozené třídy Tank, Mariňák a Battlecruiser. Třída Battlecruiser navíc implementuje rozhraní ILetajici.

Jednotka má definovány tyto metody: Přesuň_se(kam), Zaútoč(Na_co).

Tank má navíc metodu: Ostřeluj(co). Battlecruiser má navíc: Vystřel z hlavního kanónu. Třída implementující ILetajici musí mít nadefinovanou metodu Leť(kam).

Když pak píšeme umělou inteligenci a máme pole (nebo seznam) jednotek (Mariňáky, Tanky a Battlecruisery), víme, že je můžeme poslat do středu mapy pomocí volání metody přesuň_se(střed_mapy) (Koukáme se na Tank, Mariňáka nebo Battlecruisera jakožto na jejich předka – třídu Jednotka). Víme taky, že poku zrovna operujeme nad třídou Tank, můžeme něco Ostřelovat z dělostřeleckého kanónu (Ověříme si, že se jedná o Tank; v Ruby if unit.class == Tank) a také víme že pro Battlecruiser nemusíme hledat cestu po zemi, protože tam prostě doletí, jelikož splňuje rozhraní ILetajici (Přistupuji k němu jakožto k létajícímu objektu skrze rozhraní ILetajici. Nezajímá mě, že se jedná přímo o Battlecruiser.)

Pokud například píšu submodul umělé inteligence, který se stará o přesun jendotek. Nemusím vůbec vědět, nad jakými třídami jednotek operuju. Stačí mě, že dědí ze svého společného předka jednotka a že nemusím zjišťovat trasu pro ty, co splňují ILetajici.

Rozdělení návrhových vzorů

Návrhové vzory děléme do třech kategorií:

Návrhový vzor Singleton

Singleton se používá tam, kde chceme během programu umožnit použíbat pouze jednu instanci dané třídy, která bude sdílená všemi částmy kódu, které ji potřebují.

Příkladem vhodných kandidátů na návrhový vzor singleton je například logování (Logger), třída spravující přístup do databáze, třída spravující síťovou kartu, třída starající se o používání grafické karty.

Implementace singletonu v Ruby:
class Logger
  def initialize
    @log = File.open("log.txt", "a")
  end
  
  #staticka promena, ktera drzi instanci singletonu.   
  @@instance = Logger.new
 
  def self.instance
    return @@instance
  end
 
  def log(msg)
    @log.puts(msg)
  end

  private_class_method :new   #Zamezeni volani konstruktoru kymkoli jinym nez tridou samotnou – zajistuje kontrolu nad poctem vytvorenych instanci.
end

#Priklad pouziti 
Logger.instance.log('message 1')

Doporučuji skvělý článek na: dalibornasevic.com/posts/9-ruby-singleton-pattern-again

Pár důvodů, proč opravdu zvážit používání singletonů: (zdroj blogs.msdn.com/b/scottdensmore/archive/2004/05/25/140827.aspx)

Návrhový vzor Flyweight (muší váha)

Tento návrhový vzor minimalizuje paměťové nároky instance objektu tím, že se snaží co nejvíc dat sdílet s objekty stejné třídy. Obvykle se sdílená data ukládají do statické datové struktury, která se předává muším instancím ve chvíli, kdy jsou daná data potřeba.

Příkladem použití je třeba grafická reprezentace znaků v textovém editoru, kdy je potřeba, aby každý znak měl k dispozici informace o typu písma a jeho metrikách. Pokud bychom měli tyto informace ukládat pro každý znak zvlášť bylo by to paměťově neefektivní, ba v mnoha případěch zcela nemožné (kdybychom měli 10 000 písmen 'a' v dokumentu, měli bychom tyto informace uložené 10 000x). Proto jsou tyto informace uložené v externí datové struktuře, která je sdílena všemi znaky a každý znak si interně (v rámci instančních proměnných) pamatuje jen svoji pozici v textu na obrazovce a pokud jsou informace o metrice písma požadovány, znak si je vyžádá z externí datové struktury.

Více na Wikipedii

Návrhový vzor Observer (pozorovatel)

Návrhový vzor pozorovatel umožňuje zainteresovaným pozorovatelům (objektům) získávat informace o subjektu pokaždé, když se jeho stav změní.

Subjekt si interně udržuje seznam objektů, kteří ho pozorují a automaticky jim posílá zprávy (volá jejich metody) pokaždé, když se jeho stav změní. Pozorovatelé si můžou obvykle vybrat, jaké události je zajímají, a naslochat jenom jim.

Často se návrhový vzor observer používá v GUI aplikacích. Představme si například Windows dialog pro vypnutí počítače. Ten funguje tak, že na každém tlačítku (Uspat, Vypnout, přepnout uživatele), je "pověšený" observer. Zajímá ho událost OnClick, kterou tlačítko vyšle, když na něj uživatel klikne. Třída provádějící vypnutí počítače se nemusí sama starat, jestli už ke stisknutí tlačítka došlo. Tlačítko se samo ozve a stisknutí nahlásí. (tzv. Hollywoodský princip: Nevolejte, ozveme se vám sami.)

Další časté použití observerů je například při logování. Můžeme například navěsit observer na třídu, která zprostředkovává komunikaci s databází, aby se nám nahlásila pokaždé, když udělá dotaz na databázi. Ten potom například můžeme zalogovat.

Chtěl bych zde upozornit, že ne všude je observer vhodný. Použitím observera ztrácíte jednoznačný a na první pohled zřejmý tok vykonávání kódu a k tomu abyste porozuměli, jak se daný kód vykonává, musíte mít znalost o tom, kdo má jaké observery. Proto se doporučuje observery používat na věci, které jsou vedlejší – tj. které nejsou přímým účelem a smyslem vykonávaného kódu (jako například logování) a nebo tam, kde je potřeba čekat na vstup od uživatele nebo externího systému. V takových případech totiž není nic efektivnějšího.

Více na Wikipedii

Perlička: Kontroverze kolem návrhových vzorů

Někteří lidé kritizují NV za to, že se je progamátoři snaží brát moc striktně a někdy hledají návrhový vzor i v problémech kde prostě není, nebo by implementace bez NV byla daleko přímočařejší a jednodušší. Také jim vytýkají, že programátoři, kteří je striktně používají, nehledají krewtivní přešení daného problému.

Peter Norvig demonstroval, že 16 z 23 návhrových vzorů jsou přímo podporovány v Lispu a že tedy jsou spíš takovou berličkou pro jazyky, které nemají takovou vyjadřovací schopnost, jako právě Lisp.

Zdroje: