Flyweight


Tato část je o Flyweight (česky muší váha). Jedná se o návrhový vzor, který se snaží ušetřit paměť.

Proč Flyweight použít

Návrhový vzor Flyweight můžeme použít, když například ukládáme stejnou informaci pro větší množství objektů. Namísto toho abychom například ukládali stejnou hodnotu v každém objektu jen můžeme specifikovat, jaké skupiny objektů se hodnota týká a uložit ji mimo objekty. Pokud například děláme nějaký textový editor, ve kterém se formátuje text, tak namísto toho abychom pro každý znak ukládali jestli má být napsaný třeba tučně, můžeme jen určit počáteční znak, od kterého má být text tučný a konečný znak, kde již tučný text být nemá. Flyweight se tedy snaží vyhnout redundanci při ukládání dat.

Příklad - Formátování textu

Flyweight se dobře ukazuje na formátování textu. Následující ukázka ukazuje formátování textu bez použití Flyweight. Pro každý znak textu se formátování ukládá zvlášť. Formátuje se jen velikost písmen, protože text vypisujeme pouze do konzole.

class FormatovanyText {
    constructor(text) {
        this.text = text;
        // v tomto poli se pro každý znak textu ukládá,
        // jestli má být napsán velkým písmenem
        this.velkaPismena = new Array(text.length).map(() => false);
    }

    // funkce pro naformátování části textu na velká písmena
    nastavVelkaPismena(start, konec) {
        for (let i = start; i < konec; i++)
            this.velkaPismena[i] = true;
    }

    toString() {
        // do tohoto pole se postupně vloží znaky textu
        const buffer = [];
        // tento cyklus projde každý znak v textu
        for (let i in this.text) {
            let znak = this.text[i];
            // pokud má být znak napsán velkým písmenem, tak se tak
            // do pole přidá; jinak se přidá bez modifikace
            buffer.push(this.velkaPismena[i] ? znak.toUpperCase() : znak);
        }
        // znaky v poli se spojí do řetězce a ten metoda vrátí
        return buffer.join("");
    }
}


// vytvoření formátovaného textu
const text = new FormatovanyText("Návrhové vzory v JavaScriptu");
text.nastavVelkaPismena(9, 14);
// vypsání naformátovaného textu
console.log(text.toString());

Formátování textu není potřeba ukládat pro každý znak zvlášť, jak to ukazuje předchozí ukázka. Můžeme si určit rozsah znaků, které se mají formátovat a informace o formátování tedy můžeme uložit jen jednou. Následující ukázka ukazuje, jak to můžeme udělat. Pokud chceme text formátovat, tak nejdříve musíme získat rozsah na který se formátovaní aplikuje a informace o formátování nastavujeme na tomto rozsahu.

// třída sloužící k formátování části textu
// - formátování se již neukládá na každý znak, ale na rozsah znaků
class TextovyRozsah {
    constructor(start, konec) {
        this.start = start;
        this.konec = konec;
        // určuje, jestli má být část textu napsána velkými písmeny
        this.velkaPismena = false;
    }

    // tato metoda slouží ke zjištění, jestli rozsah obsahuje předaný znak
    obsahuje(pozice) {
        return pozice >= this.start && pozice <= this.konec;
    }
}

class FormatovanyText {
    constructor(text) {
        this.text = text;
        // v tomto poli se budou ukládat textové rozsahy,
        // které budou obsahovat formátování textu
        this.formatovani = [];
    }

    // pro formátování části textu můžeme pomocí této metody
    // získat rozsah, který můžeme naformátovat
    ziskejRozsah(start, konec) {
        const rozsah = new TextovyRozsah(start, konec);
        this.formatovani.push(rozsah);
        return rozsah;
    }

    toString() {
        // do tohoto pole se postupně vloží znaky textu
        const buffer = [];
        // tento cyklus projde každý znak v textu
        for (let i in this.text) {
            let znak = this.text[i];
            // projde se každý rozsah v poli formatovani
            for (let rozsah of this.formatovani) {
                // pokud rozsah nastavuje, že se má nastavit velké písmeno a znak
                // se v rozsahu nachází, tak se znak nastaví na velké písmeno
                if (rozsah.obsahuje(i) && rozsah.velkaPismena)
                    znak = znak.toUpperCase();
            }
            // znak se přidá do pole
            buffer.push(znak);
        }
        // znaky v poli se spojí do řetězce a ten metoda vrátí
        return buffer.join("");
    }
}

// vytvoření formátovaného textu
const text = new FormatovanyText("Návrhové vzory v JavaScriptu");
text.ziskejRozsah(9, 14).velkaPismena = true;
// vypsání naformátovaného textu
console.log(text.toString());

Příklad - Nastavování stejných vlastností více objektům

Následující ukázka ukazuje třídu Nepritel, která představuje nepřítele třeba v nějaké hře. Třída Nepritel ukládá jméno nepřítele a nějaké další vlastnosti. V kódu vytváříme 1000 instancí této třídy a pro každou nastavujeme jedno z 5 jmen.

// tato třída představuje nepřítele třeba v nějaké hře
class Nepritel {
    constructor(jmeno) {
        // uložení jména nepřítele
        this.jmeno = jmeno;
        // nějaké další vlastnosti
        this.zivoty = 10;
        this.sila = 5;
    }
}


// funkce, která vrátí náhodné jméno z 5 možných jmen
function nahodneJmeno() {
    const cislo = Math.trunc(Math.random() * 5);

    switch (cislo) {
        case 0:
            return "Ivan";
        case 1:
            return "Karel";
        case 2:
            return "Marek";
        case 3:
            return "Pavel";
        case 4:
            return "Igor";
    }
}

const nepratele = [];
// vygenerování 1000 nepřátel
for (let i = 0; i < 1000; i++) {
    // nepříteli se při jeho vytváření nastaví jedno
    // z 5 jmen, které vrátí funkce nahodneJmeno
    const nepritel = new Nepritel(nahodneJmeno());
    nepratele.push(nepritel);
}

// součet všech znaků ve jménech vygenerovaných nepřátel
let pocetZnaku = 0;
for (let nepritel of nepratele)
    pocetZnaku += nepritel.jmeno.length;
console.log(pocetZnaku);

V předchozí ukázce ukládáme jméno každého nepřítele jako jeho vlastní vlastnost. Vzhledem k tomu, že generujeme 1000 nepřátel a každý nepřítel může mít jedno z 5 jmen, je vhodné jména nepřátel ukládat například v nějakém poli a vždy jen nepříteli nastavit index na jméno v tomto poli, podle toho jaké jméno má mít. Tím ušetříme paměť, ale na druhou stranu o něco zvýšíme časovou náročnost při vytváření nového nepřítele, protože musíme zjišťovat index jména v poli. To je ale u našeho příkladu zanedbatelné, protože máme jen 5 možných jmen.

class Nepritel {
    // jmena se budou ukládat v tomto poli, které patří třídě
    static jmena = [];

    constructor(jmeno) {
        // uložení indexu na jméno v poli jmena
        this.jmenoID = this._ziskejNeboPridejJmeno(jmeno);
        // nějaké další vlastnosti
        this.zivoty = 10;
        this.sila = 5;
    }

    _ziskejNeboPridejJmeno(jmeno) {
        // pokud pole jmena obsahuje předané jméno, tak
        // se vrátí index pod kterým je v poli uloženo
        let idx = Nepritel.jmena.indexOf(jmeno);
        if (idx !== -1) return idx;

        // pokud pole jmena neobsahuje předané jméno, tak
        // se do něj jméno přidá a vrátí se jeho index
        Nepritel.jmena.push(jmeno);
        return Nepritel.jmena.length-1;
    }
}


function nahodneJmeno() {
    const cislo = Math.trunc(Math.random() * 5);

    switch (cislo) {
        case 0:
            return "Ivan";
        case 1:
            return "Karel";
        case 2:
            return "Marek";
        case 3:
            return "Pavel";
        case 4:
            return "Igor";
    }
}

const nepratele = [];
// vygenerování 1000 nepřátel
for (let i = 0; i < 1000; i++) {
    const nepritel = new Nepritel(nahodneJmeno());
    nepratele.push(nepritel);
}

// součet všech znaků ve jménech vygenerovaných nepřátel
// - bude jich málo oproti předchozí ukázce
let pocetZnaku = 0;
for (let jmeno of Nepritel.jmena)
    pocetZnaku += jmeno.length;
console.log(pocetZnaku);