IT rekvalifikace s garancí práce. Seniorní programátoři vydělávají až 160 000 Kč/měsíc a rekvalifikace je prvním krokem. Zjisti, jak na to!
Hledáme nové posily do ITnetwork týmu. Podívej se na volné pozice a přidej se do nejagilnější firmy na trhu - Více informací.

Lekce 9 - Flyweight (muší váha)

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:

UML diagram příkladu pro návrhový vzor Flyweight z GOF - Návrhové vzory GoF

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.


 

Předchozí článek
Kvíz - Adapter, Facade, Proxy, Decorator ve Vzory GOF
Všechny články v sekci
Návrhové vzory GoF
Přeskočit článek
(nedoporučujeme)
Composite
Článek pro vás napsal Patrik Valkovič
Avatar
Uživatelské hodnocení:
30 hlasů
Věnuji se programování v C++ a C#. Kromě toho také programuji v PHP (Nette) a JavaScriptu (NodeJS).
Aktivity