Lekce 9 - Flyweight (muší váha)
V předchozím kvízu, Kvíz - Adapter, Facade, Proxy, Decorator ve Vzory GOF, jsme si ověřili nabyté zkušenosti z předchozích lekcí.
V tutoriálu Návrhové vzory GoF si představíme návrhový vzor Flyweight. Flyweight šetří paměť při úkolech, pro které potřebujeme vytvořit velký počet instancí. Tento vzor není použitelný vždy. Vytvářené instance musí mít určitá kritéria, která rozebereme dále. Také se podíváme na implementaci, která se oficiálně za Flyweight nepovažuje. Avšak řeší stejný problém, jen jinou metodou.
Princip vzoru Flyweight
Základní myšlenkou u tohoto návrhového vzoru je rozdělení třídy na dvě části, které představují:
- Vnitřní stav objektu - reprezentuje společnou část pro všechny třídy. Stav si o sobě instance pamatuje sama.
- Vnější stav objektu - reprezentuje informace, které instance musí získat z venku.
Příklady implementací vzoru Flyweight
Implementaci vzoru Flyweight si ukážeme na těchto dvou příkladech:
- vykreslení textu,
- hra s hordou nepřátel.
Vykreslení textu
Tradičně se jako příklad uvádí text, proto jej použijeme i zde. Definujme si, co bude vnitřním a vnějším stavem.
Vnitřní stav
Vnitřním stavem budou informace jako font, dekorace písma (kurzíva, podtržení), velikost písma, formátování a podobně. Tyto informace jsou zpravidla společné pro velkou část textu – pro celý odstavec, pro všechny nadpisy a podobně. Pro jednoduchost bude ve vnitřní reprezentaci i znak, který se má vypisovat.
Ve výsledku máme ke každému stylu, který v textu použijeme, vygenerovaná všechna písmena. Matematicky je to počet stylů krát počet písmen (záleží, jakou znakovou sadu bereme v úvahu). Zdá se, že už nám počet instancí celkem "nabobtnal". Stále je to ale méně, než kdybychom měli mít pro každý znak v textu samostatný objekt.
Vnější stav
Z vnějšku dodáme objektu kam se má co vykreslit.
Nyní se už dáme na implementaci.
Popis implementace
Muší váha vyžaduje použití i dalších návrhových vzorů. Objekty vnitřního stavu se od sebe liší pouze atributy. Není tedy důvod, abychom měli v programu dvě instance se stejnými atributy. S tím nám pomůže továrna, která bude ukládat již vytvořené instance. Bude-li uživatel vyžadovat další instanci se stejnými atributy, vrátí již vytvořenou. Samotný text bude uložen jako sekvence vnitřních stavů. Vykreslení bude probíhat tak, že projdeme sekvenci a text budeme vykreslovat za sebe.
Implementace
Zakresleme implementaci pomocí diagramu:

Třída Znak
je vnitřní reprezentace stavu.
Znaky se vytvářejí skrz třídu TovarnaNaZnaky
, která si
udržuje v privátním atributu již vytvořené instance. Protože třída
Znak
je neměnný objekt, stačí nám hledat pouze podle hashe.
Text si poté ukládá sekvenci znaků a postupně je vykresluje. Pro náš
případ zanedbáme pozicování textu jako celku.
Třídy Znak
a
TovarnaNaZnaky
Nejprve implementujme do kódu třídy Znak
a
TovarnaNaZnaky
:
-
class Znak { public char znak; public string nazevFontu; public int velikost; public int GetHash() { return Hash(this.znak, this.nazevFontu, this.velikost); } public void Vykresli(Obdelnik kde) { // vykreslení na zadané místo } public static int Hash(char znak, string nazevFontu, int velikost) { return znak.GetHashCode() * 31 + nazevFontu.GetHashCode() * 17 + velikost.GetHashCode(); } } class TovarnaNaZnaky { private Dictionary<int, Znak> vytvoreneInstance = new Dictionary<int, Znak>(); public Znak ZiskejZnak(char znak, string nazevFontu, int velikost) { int predpokladanyHash = Znak.Hash(znak, nazevFontu, velikost); Znak navraceni; if (vytvoreneInstance.TryGetValue(predpokladanyHash, out navraceni)) return navraceni; navraceni = new Znak { znak = znak, nazevFontu = nazevFontu, velikost = velikost }; vytvoreneInstance.Add(navraceni.GetHash(), navraceni); return navraceni; } }
-
class Znak { public char znak; public string nazevFontu; public int velikost; public int getHashCode() { return hash(this.znak, this.nazevFontu, this.velikost); } public void vykresli(Obdelnik kde) { // vykreslení na zadané místo } public static int hash(char Znak, string nazevFontu, int velikost) { return znak.getHashCode() * 31 . nazevFontu.getHashCode() * 17 . velikost.getHashCode(); } } class TovarnaNaZnaky { private Dictionary<int,Znak> vytvoreneInstance; public Znak ziskejZnak(char znak, string nazevFontu, int velikost) { int predpokladanyHash = Znak.hash(znak, nazevFontu, velikost); Znak navraceni; if (this.Instance.tryGetValue(predpokladanyHash, navraceni)) return Navraceni; navraceni = new Znak(); navraceni.znak = znak; navraceni.nazevFontu = nazevFontu; navraceni.velikost = velikost; this.Instance.Add(navraceni.getHashCode(), navraceni); return navraceni; } }
-
class Znak { public $znak; public $nazevFontu; public $velikost; public function getHash() { return $this->hash($this->znak, $this->nazevFontu, $this->velikost); } public function vykresli($kde) { // vykreslení na zadané místo } public static function hash($znak, $nazevFontu, $velikost) { return $znak * 31 + $nazevFontu + 17 + $velikost; } } class TovarnaNaZnaky { private $vytvoreneInstance = array(); public function ziskejZnak($znak, $nazevFontu, $velikost) { $predpokladanyHash = Znak::hash($znak, $nazevFontu, $velikost); if (array_key_exists($predpokladanyHash, $this->vytvoreneInstance)) { return $this->vytvoreneInstance[$predpokladanyHash]; } $navraceni = new Znak(); $navraceni->znak = $znak; $navraceni->nazevFontu = $nazevFontu; $navraceni->velikost = $velikost; $this->vytvoreneInstance[$navraceni->getHash()] = $navraceni; return $navraceni; } }
-
class Znak { constructor(znak, nazevFontu, velikost) { this.znak = znak; this.nazevFontu = nazevFontu; this.velikost = velikost; } getHash() { return this.hash(this.znak, this.nazevFontu, this.velikost); } vykresli(kde) { // vykreslení na zadané místo } static hash(znak, nazevFontu, velikost) { return znak.charCodeAt(0) * 31 + nazevFontu.hashCode() * 17 + velikost; } } class TovarnaNaZnaky { constructor() { this.vytvoreneInstance = new Map(); } ziskejZnak(znak, nazevFontu, velikost) { const predpokladanyHash = Znak.hash(znak, nazevFontu, velikost); if (this.vytvoreneInstance.has(predpokladanyHash)) { return this.vytvoreneInstance.get(predpokladanyHash); } const navraceni = new Znak(znak, nazevFontu, velikost); this.vytvoreneInstance.set(navraceri.getHash(), navraceni); return navraceni; } }
-
class Znak: def __init__(self, znak, nazevFontu, velikost): self.znak = znak self.nazevFontu = nazevFontu self.velikost = velikost def get_hash(self): return self.hash(self.znak, self.nazevFontu, self.velikost) def vykresli(self, kde): # vykreslení na zadané místo pass @staticmethod def hash(znak, nazevFontu, velikost): return hash(znak) * 31 + hash(nazevFontu) * 17 + hash(velikost) class TovarnaNaZnaky: def __init__(self): self.vytvorene_instance = {} def ziskej_znak(self, znak, nazevFontu, velikost): predpokladany_hash = Znak.hash(znak, nazevFontu, velikost) if predpokladany_hash in self.vytvorene_instance: return self.vytvorene_instance[predpokladany_hash] navraceni = Znak(znak, nazevFontu, velikost) self.vytvorene_instance[navraceni.get_hash()] = navraceni return navraceni
Vytvořili jsme statickou metodu hash()
ve třídě
Znak
. Tato metoda nám dovolí simulovat metodu
getHashCode()
bez samotné vytvoření instance. Toho následně
využíváme v továrně. Továrna se podívá, zda již má instanci třídy
Znak
se zadanými hodnotami. Pokud ne, vytvoří ji, uloží a
vrátí uživateli.
Třída Text
Samotné vykreslení pak zjednodušeně implementujeme do třídy
Text
:
-
class Text { public List<Znak> znaky; public void Vykresli() { Obdelnik obdelnikNaVykresleni = new Obdelnik(); foreach (Znak z in znaky) { z.vykresli(obdelnikNaVykresleni); obdelnikNaVykresleni.posunOSirkuPismene(); } } }
-
class Text { public List<Znak> Znaky; public void vykresli() { Obdelnik obdelnikNaVykresleni; foreach(Znak z in this.znaky) { z.vykresli(obdelnikNaVykresleni); obdelnikNaVykresleni.posunOSirkuPismene(); } } }
-
class Text { public $znaky; public function vykresli() { $obdelnikNaVykresleni = new Obdelnik(); foreach ($this->znaky as $z) { $z->vykresli($obdelnikNaVykresleni); $obdelnikNaVykresleni->posunOSirkuPismene(); } } }
-
class Text { constructor() { this.znaky = []; } vykresli() { const obdelnikNaVykresleni = new Obdelnik(); for (const z of this.znaky) { z.vykresli(obdelnikNaVykresleni); obdelnikNaVykresleni.posunOSirkuPismene(); } } }
-
class Text: def __init__(self): self.znaky = [] def vykresli(self): obdelnikNaVykresleni = Obdelnik() for z in self.znaky: z.vykresli(obdelnikNaVykresleni) obdelnikNaVykresleni.posunOSirkuPismene()
Vnější stav (pozice vykreslení) je dodán až při
samotném vykreslení a to parametrem. Pro uživatele to tedy znamená pouze
naplnit list znaků a Text
už sám jednotlivé znaky vykreslí.
Pokud se nějaký znak opakuje, v programu bude pouze jedna
instance, ale vložená na několika místech. Toto je princip vzoru
Flyweight.
Hra s hordou nepřátel
Pojďme na druhý příklad, který se sice oficiálně neřadí mezi implementace vzoru Flyweight, ale řeší stejný problém.
Představme si, že programujeme hru s velkým množstvím nepřátel. Jednotliví nepřátelé jsou naprosto totožní. Vypadají stejně, mají stejnou sílu, inteligenci, zbraně a podobně. Není důvod, abychom si tyto informace pamatovali pro každý objekt zvlášť a zabírali tím paměť. Místo toho tyto informace převedeme do samostatné třídy, která bude reprezentovat náš vnitřní stav.
Jak ale nepřítel zjistí, kde se na mapě nachází? To mu již musí říct někdo z venku. Tyto informace dostane volaná metoda přímo ve svých parametrech. Tyto informace budou pro nás vnějším stavem.
Nepřátelé se po mapě pohybují - potřebujeme tedy znát jejich souřadnice. Problém u tohoto případu je, že nemůžeme návrhový vzor Flyweight dost dobře aplikovat. Souřadnice se mění a my nemáme spolehlivou strukturu, která by tyto změny reflektovala. Řešením by bylo nadefinovat samostatnou třídu pro vnitřní stav (jak ho známe do teď) a další třídu pro vnější stav. Třída pro vnější stav by měla privátní atribut vnitřního stavu:
-
public class ZakladniNepritel { public Texture vzhled; public int sila; public int maximalniPocetZivotu; public int inteligence; } public class Nepritel { private ZakladniNepritel zaklad; public int pocetZivotu; public int poziceX; public int poziceY; public Nepritel(ZakladniNepritel zakladParametr) { this.zaklad = zakladParametr; } }
-
class ZakladniNepritel { public Texture vzhled; public int sila; public int maximalniPocetZivotu; public int inteligence; } class Nepritel { private ZakladniNepritel zaklad; public int pocetZivotu; public int poziceX; public int poziceY; public Nepritel(ZakladniNepritel zakladParametr) { this.zaklad=zakladParametr; } }
-
class ZakladniNepritel { public $vzhled; public $sila; public $maximalniPocetZivotu; public $inteligence; } class Nepritel { private $zaklad; public $pocetZivotu; public $poziceX; public $poziceY; public function __construct(ZakladniNepritel $zakladParametr) { $this->zaklad = $zakladParametr; } }
-
class ZakladniNepritel { constructor() { this.vzhled = null; this.sila = 0; this.maximalniPocetZivotu = 0; this.inteligence = 0; } } class Nepritel { constructor(zakladParametr) { this.zaklad = zakladParametr; this.pocetZivotu = 0; this.poziceX = 0; this.poziceY = 0; } }
-
class ZakladniNepritel: def __init__(self): self.vzhled = None self.sila = 0 self.maximalniPocetZivotu = 0 self.inteligence = 0 class Nepritel: def __init__(self, zakladParametr): self.zaklad = zakladParametr self.pocetZivotu = 0 self.poziceX = 0 self.poziceY = 0
Na rozdíl od Flyweight neušetříme počet instancí,
protože musíme vytvořit skutečně tolik instancí, kolik máme ve hře
objektů. Ušetříme ale paměť. Společná část má pro ukázku minimálně
4 * 3 = 12 bajtů pro typ int
a 8 bajtů pro texturu, u které
předpokládáme, že se jedná o ukazatel.
Kdybychom měli 1024 nepřátel, ušetříme minimálně
20*1024=20kB a to už není zanedbatelné množství. Také nám to
dává větší flexibilitu. Můžeme například do vnitřního stavu zahrnout
počet životů (většinu doby budou mít všichni nepřátelé plný počet
životů) a při zranění můžeme vnitřní stav vyměnit. Také můžeme
vnitřní stavy zanořovat (vnitřní stav, který má vnitřní stav).
Návrhové vzory nejsou zákony. Jsou to spíše kuchařky, které si můžeme upravovat podle našich potřeb. Vždy se najde případ, kdy je potřeba vymyslet specifický návrh systému. Návrhové vzory nám dávají náhled do toho, jak by se to mohlo řešit, ale rozhodně neříkají, jak se to musí řešit. Právě od toho jsou tady programátoři.
V další lekci, Composite, si ukážeme návrhový vzor Composite. Vzor přináší doporučené řešení situace, kdy pracujeme s nějakou stromovou strukturou, např. navigačním menu.