Flyweight (muší váha)

Návrhové vzory Flyweight (muší váha)

Návrhový vzor muší váha slouží k  ušetření paměti při úkolech, pro které potřebujeme vytvořit velký počet instancí určitého typu. 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 muší váhu nepovažuje, ale řeší stejný problém - jen jinou metodou.

Princip

Základní myšlenkou u tohoto návrhového vzoru je rozdělení třídy na dvě části. První část je společná pro všechny třídy a budeme jí dále říkat vnitřní stav objektu. Tento stav si o sobě instance pamatuje sama. Pro příklad si představme, že programujeme hru a máme nepřátele rozdělené do několika typů. Jednotlivé typy 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 místo). Místo toho tyto informace převedeme do samostatné třídy, která bude reprezentovat náš vnitřní stav. Jak ale nepřítel zjistí, kam se má na obrazovce vykreslit? To mu již musí říct někdo z venku. Tyto informace dostane volaná metoda přímo ve svých  parametrech. Nazveme je vnějším stavem objektu, který si objekt nepamatuje a musí jej získat.

Vykreslení textu

Pro implementaci si řekneme o jiném příkladu, k hrám se nicméně vrátíme ještě na konci. Tradičně se jako příklad uvádí text, proto jej použijeme i zde. Vnitřním stavem budou informace jako font, zda je text tučný nebo kurzívou, 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", ale stále je to méně, než kdybychom měli mít pro každý znak v  textu samostatný objekt. Nyní už objekt potřebuje pouze vědět, kam se má vykreslit - to mu dodáme z vnějšku.

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. Dvě instance se stejnými atributy jsou totožné – to by nám mělo být povědomé. Také zřejmě nechceme v každé pytvářet instance ručně proísmeno. V tomhle nám pomůže továrna. Ta bude ukládat již vytvořené instance a bude-li uživatel vyžadovat další instanci se stejnými atributy, vrátí již dříve 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. Nyní již k praktické části.

UML diagram

Třída Znak je vnitřní reprezentace stavu. Znaky se vytvářejí skrz TovárnaNaZnaky, 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. Konkrétní implementace by pak byla následující.

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;
    }
}

Vytvořili jsme statickou metodu Hash ve třídě Znak. Tato metoda nám dovolí simulovat GetHashCode metodu bez samotného vytvář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. Samotné vykreslení by mohlo vypadat následovně (zjednodušená verze).

class Text
{
    public List<Znak> Znaky;

    public void Vykresli()
    {
        Obdelnik ObdelnikNaVykresleni;
        foreach(Znak z in this.Znaky)
        {
            z.Vykresli(ObdelnikNaVykresleni);
            ObdelnikNaVykresleni.PosunOSirkuPismene();
        }
    }
}

Vnější stav (pozice vykreslení) je dodán až při samotném vykreslení. Pro uživatele to tedy znamená pouze naplnit list znaků a Text už sám jednotlivé znaky vykreslí. Pokud se nějaký znak opakuje, bude mít pouze jednu instanci (ale vloženou na několika místech). To je princip flyweight.

Možné modifikace

Na konec bych chtěl ještě rozebrat jistý návrh, který se oficiálně neřadí mezi flyweight, ale řeší stejný problém. Vezmeme si zpět slibovanou hordu nepřátel. Tito 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. Nejlépe asi poslouží ukázka.

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 rozdíl od flyweight neušetříme počet instancí (musíme vytvořit skutečně tolik instancí, kolik máme ve hře objektů), ale ušetříme místo. Společná část má minimálně 4*3=12 bajtů pro typ int a 8 bajtů pro texturu (budeme předpokládat, že se jedná o ukazatel). Kdybychom měli 1024 nepřátel, máme 20*1024=20kB a to už není zanedbatelná část. 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šechna monstra 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.


 

  Aktivity (5)

Článek pro vás napsal patrik.valkovic
Avatar
Věnuji se programování v C++ a C#. Kromě toho také programuji v PHP (Nette) a JavaScriptu.

Jak se ti líbí článek?
Celkem (3 hlasů) :
4.666674.666674.666674.666674.66667


 


Miniatura
Předchozí článek
Decorator (dekorátor)
Miniatura
Všechny články v sekci
Návrhové vzory

 

 

Komentáře

Děláme co je v našich silách, aby byly zdejší diskuze co nejkvalitnější. Proto do nich také mohou přispívat pouze registrovaní členové. Pro zapojení do diskuze se přihlas. Pokud ještě nemáš účet, zaregistruj se, je to zdarma.

Zatím nikdo nevložil komentář - buď první!