Tato část je o Flyweight (česky muší váha). Jedná se o návrhový vzor, který se snaží ušetřit paměť.
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.
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());
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);