Flyweight (mušia váha)
Návrhový vzor mušia váha šetrí pamäť pri úlohách, pre ktoré potrebujeme vytvoriť veľký počet inštancií. Tento vzor nie je použiteľný vždy - vytváranej inštancie musí mať určité kritériá, ktoré rozoberieme ďalej. Tiež sa pozrieme na implementáciu, ktorá sa oficiálne za mušej váhu nepovažuje, ale riešia rovnaký problém - len inou metódou.
Princíp
Základnou myšlienkou u tohto návrhového vzoru je rozdelenie triedy na dve časti. Prvá časť je spoločná pre všetky triedy a budeme jej ďalej hovoriť vnútorný stav objektu. Tento stav si o sebe inštancie pamätá sama. Pre príklad si predstavme, že programujeme hru a máme nepriateľov vo svete. Jednotliví nepriatelia sú úplne totožní - vyzerajú rovnako, majú rovnakú silu, inteligenciu, zbrane a podobne. Nie je dôvod, aby sme si tieto informácie pamätali pre každý objekt zvlášť (a zaberali tým pamäť). Namiesto toho tieto informácie prevedieme do samostatnej triedy, ktorá bude reprezentovať náš vnútorný stav. Ako ale nepriateľ zistí, kde na mape sa nachádza? To mu už musí povedať niekto z vonku. Tieto informácie dostane volané metóda priamo vo svojich parametroch. Nazveme je vonkajším stavom objektu, ktorý si objekt nepamätá a musí ho získať.
Vykreslenie textu
Pre implementáciu si povieme o inom príklade, ku hrám sa však vrátime ešte na konci. Tradične sa ako príklad uvádza text, preto ho použijeme aj tu. Vnútorným stavom budú informácie ako font, dekorácie písma (kurzíva, podčiarknutie), veľkosť písma, formátovanie a podobne. Tieto informácie sú spravidla spoločné pre veľkú časť textu - pre celý odsek, pre všetky nadpisy a podobne. Pre jednoduchosť bude vo vnútornej reprezentáciu i znak, ktorý sa má vypisovať.
Vo výsledku máme ku každému štýlu, ktorý v texte použijeme, vygenerovaná všetky písmená. Matematicky je to počet štýlov krát počet písmen (záleží, akú znakovú sadu berieme do úvahy). Zdá sa, že už nám počet inštancií celkom "nabobtnal", ale stále je to menej, než keby sme mali mať pre každý znak v texte samostatný objekt. Teraz už objekt potrebuje iba vedieť, kam sa má vykresliť - to mu dodáme zvonku.
Implementácia
Mušia váha vyžaduje použitie aj ďalších návrhových vzorov. Objekty vnútorného stavu sa od seba líšia len atribúty. Nie je teda dôvod, aby sme mali v programe dve inštancie s rovnakými atribútmi. S tým nám pomôže továreň, ktorá bude ukladať už vytvorené inštancie a ak bude užívateľ vyžadovať ďalšiu inštanciu s rovnakými atribútmi, vráti už vytvorenú. Samotný text bude uložený ako sekvencia vnútorných stavov. Vykreslenie bude prebiehať tak, že prejdeme sekvenciu a text budeme vykresľovať za seba. Teraz už k praktickej časti.
Trieda Znak
je vnútorná reprezentácia stavu. Znaky sa
vytvárajú skrz TovárnaNaZnaky
, ktorá si udržiava v privátnom
atribúte už vytvorené inštancie. Pretože trieda Znak
je
nemenný objekt, stačí nám hľadať len podľa hashe. Text si potom ukladá
sekvenciu znakov a postupne ich vykresľuje. Pre náš prípad zanedbáme
pozicovanie texte ako celku. Konkrétnej implementácie by potom bola
nasledujúca:
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) { 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; } }
Vytvorili sme statickú metódu Hash()
v triede
Znak
. Táto metóda nám dovolí simulovať metódu
GetHashCode()
bez samotného vytvárania inštancie. Toho následne
využívame v továrni. Továreň sa pozrie, či už má inštanciu triedy Znak
so zadanými hodnotami, Ak nie, vytvorí ju, uloží a vráti užívateľovi.
Samotné vykreslenie by mohlo vyzerať nasledovne (zjednodušená verzia).
class Text { public List<Znak> Znaky; public void Vykresli() { Obdelnik ObdelnikNaVykresleni; foreach(Znak z in this.Znaky) { z.Vykresli(ObdelnikNaVykresleni); ObdelnikNaVykresleni.PosunOSirkuPismene(); } } }
Vonkajšie stav (pozícia vykreslenie) sa dodáva až pri samotnom vykreslenie a to parametrom. Pre užívateľov to teda znamená iba naplniť list znakov a Text už sám jednotlivé znaky vykreslí. Ak sa nejaký znak opakuje, v programe bude iba jedna inštancia (ale vložená na niekoľkých miestach). To je princíp Flyweight.
Možné modifikácie
Na koniec by som chcel ešte rozobrať podobný návrh, ktorý sa oficiálne neradí medzi Flyweight, ale riešia rovnaký problém. Vezmeme si späť sľubovanú hordu nepriateľov. Títo nepriatelia sa po mape pohybujú - potrebujeme teda poznať ich súradnice. Problém u tohto prípadu je, že nemôžeme návrhový vzor Flyweight dosť dobre aplikovať. Súradnice sa mení a my nemáme spoľahlivú štruktúru, ktorá by tieto zmeny reflektovala. Riešením by bolo nadefinovať samostatnú triedu pre vnútorný stav (ako ho poznáme do teraz) a ďalšiu triedu pre vonkajší stav. Trieda pre vonkajší stav by mala privátne atribút vnútorného stavu. Najvyššie asi poslúži ukážka:
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; } }
Na rozdiel od Flyweight neušetríme počet inštancií (musíme vytvoriť
skutočne toľko inštancií, koľko máme v hre objektov), ale ušetríme
pamäť. Spoločná časť má pre ukážku minimálne 4 * 3 = 12 bajtov pre typ
int
a 8 bajtov pre textúru (budeme predpokladať, že sa jedná o
ukazovateľ). Keby sme mali 1024 nepriateľov, ušetríme minimálne 20 * 1024 =
20kB a to už nie je zanedbateľné množstvo. Tiež nám to dáva väčšiu
flexibilitu. Môžeme napríklad do vnútorného stavu zahrnúť počet životov
(väčšinu času budú mať všetky monštrá plný počet život) a pri
zranení môžeme vnútorný stav vymeniť. Tiež môžeme vnútorné stavy
ponoriť (vnútorný stav, ktorý má vnútorný stav).
Návrhové vzory nie sú zákony. Sú to skôr kuchárky, ktoré si môžeme upravovať podľa našich potrieb. Vždy sa nájde prípad, kedy je potrebné vymyslieť špecifický návrh systému. Návrhové vzory nám dávajú náhľad do toho, ako by sa to mohlo riešiť, ale rozhodne nehovoria, ako sa to musí riešiť. Práve od toho sú tu programátori.