Multiton
Multiton (nebo také originál) je v softwarovém inženýrství návrhový vzor ze skupiny řízení tvorby instancí. Vzor má za cíl řídit počet vytvořených instancí v systému a to pomocí párování k unikátním klíčům.
Popis
Návrhový vzor originál se řadí mezi vzory starající se o tvorbu instancí, přičemž se zaměřuje na řízení počtu instancí. Toho dosahuje přiřazením vytvářených instancí klíči, přičemž na instanci třídy definující náš Multiton se dotazujeme právě tímto klíčem. Je zároveň dobrým zvykem vybírat jako tento klíč nejdůležitější atribut, který instanci náleží, a zároveň musí být hodnota tohoto atributu unikátní.[1] Příkladně souřadnice na hrací ploše, unikátní název, identifikátor nebo nějaká klíčová kombinace.
Alternativní název originál vychází z vlastnosti, kdy při poptávce po instanci k danému klíči se a priori nevytváří nová instance, nýbrž se vrátí ta původní, originální.[1]
Instance Multitonu
Samotná instance je tvořena zpravidla atributem, který instanci tvoří. Příkladně může jít o souřadnice hrací desky šachů (kombinace číslice a písmene, čímž nám vznikne právě 64 instancí třídy Políčko
) či například emailová adresa v seznamu kontaktů. Tento klíč ostatně nemusí být ani obsažen v samotné instanci originálu, avšak vždy platí, že instance klíče musí být unikátní, instance přiřazenému klíči musí být unikátní a unikátní musí být i tato vazba.
Konstruktor
Jako je pro návrhové vzory starající se o tvorbu instancí příznačné, třídy mají omezený přístup ke konstruktoru instance, a to ideálně privátní (z třídy samotné). Konstruktor (či konstruktory) jsou volány obvykle jen přístupovou metodou (statickou) a nebo statickým iniciačním blokem.
Klíč
Třída instancí, které jsou používány pro vyhledávání a identifikování objektů. V podstatě platí, že počet instancí klíče je menší nebo roven počtu instancí třídy originálu.
Klíč bývá zpravidla interpretován jako výčtový typ (pro neměnný počet instancí) nebo jako instance jiné třídy (například jméno v adresáři, podle kterého hledáme instanci třídy Osoba
, jenž je reprezentována množinou telefonních čísel, e-mailových adres, dvojice souřadnic reprezentující políčko při hře Lodě). Druhý zmíněný způsob umožňuje rozšiřování sady instancí originálu (tedy přidávání osob do adresáře či rozšiřování hracího plánku) a to za běhu programu - dynamicky.
Alternativním a ne zcela vhodným způsobem je používání klíče, který není nutně spjat s povahou samotné instance originálu jejího stavu či její podoby, příkladně použití čísla coby označení pořadí instancí. Představit bychom si to mohli na příkladu vyhledávání kontaktu na osobu podle pořadí jeho zařazení do adresáře.
Je vhodné uvažovat situaci, kdy bude požadována instance k null
, tedy kdy bude místo klíče vložen do přístupové metody prázdný objekt. Není-li to v zájmu logiky programu, měl by umět kód na takovou situaci v rámci přístupové metody adekvátně reagovat - vyhozením výjimky či automatickým vrácením prázdné instance (také null
).
Kontejner instancí
Pro uložení dvojic klíč-instance se obvykle používá mapy (slovníku), v němž je mapován klíč na referenci na instanci. Tento přístup umožňuje poměrně snadný a rychlý přístup k instanci, zná-li žadatel klíč.
Alternativním přístupem je využití výčtového typu, jehož instance v sobě obsahují odkazy na instance originálu. Výhodou je snadné použití, ovšem na úkor nebezpečí zveřejnění konstruktoru minimálně do stavu package-private. Další nevýhodou je zásadní omezení v případě dalšího vývoje, kdy je v některých jazycích (např. v Javě) nemožné rozšířit dynamicky sadu klíčů (rozšířením počtu instancí výčtového typu).
Typy
Typy vzoru multiton jsou především dva:
- Bez možnosti přidávání nových instancí - tento typ je mnohem jednodušší na implementaci a zpravidla bývá i bezpečnější. Pro tento typ však platí omezení, že počet instancí je striktně omezen na počet instancí klíče a není možné ho přesáhnout. Vhodné je tento způsob použít například ve hrách jako je sudoku či šachy, kde předem přesně známe neměnný počet polí (pro sudoku 81, pro šachy 64).
- S možností přidávání nových instancí - mnohem variabilnější způsob, který umožňuje dynamicky ovlivňovat počet instancí typu multiton. Obvykle je však náročnější ho zabezpečit proti možným chybám (například pro vícevláknové aplikace). Tyto chyby mohou způsobit nekonzistenci celého systému, kdy dvě části programu odkazují na dvě odlišné instance se stejným klíčem. Jako příklad lze použít matriku, kde vyhledáváme jednotlivé osoby (instance multitonu) podle rodného čísla (klíč). Pokud lze registrovat na matriku novorozence z více míst najednou, je nutné zajistit, aby dva novorozenci (narození ve stejný okamžik) nedostali přidělené stejné rodné číslo (synchronizace vláken například pomocí uzamčení generátoru rodných čísel). Pokud by se to tak stalo, tak by v první řadě byl jeden z těchto novorozenců přepsán druhým (na matrice by přestal existovat) a zároveň by si stále zasahovali vzájemně do života.
Pro oba druhy však platí, že mají soukromý konstruktor a mají vlastní přístupovou metodu, která vrací odkazy na instance s příslušným klíčem.
Důvody k použití
Návrhový vzor originál je dobré použít tam, kde víme, že počet instancí daného typu v systému bude malý, zato bude použit na mnoha místech v kódu.
Hlavními důvody pro nasazení návrhového vzoru originál mohou být následující:
- Náročná (případně pomalá) tvorba instancí - například při čtení většího objemu dat z úložiště či stahování dat ze vzdálených zdrojů Instance jsou náročné na kapacity (například zabírají mnoho paměti) - například skládá-li se instance originálu z velkého datového objemu
- Business model aplikace vyžaduje jen omezený počet instancí - například v případě karetních her nechceme, aby bylo možné vytvářet nové karty mimo ty, které jsou v balíčku
- Business model aplikace vyžaduje znovupoužití instancí - například při postupném přiřazování nových hodnot k instanci s daným klíčem
Zvýšení efektivity
Při tvorbě "drahých" instancí (rozuměj instancí pomalých na tvorbu či náročných na kapacity) se pouze hledá odkaz. Navíc, použijeme-li klíčů, nemusíme porovnávat celé tyto objekty. Stačí nám porovnávat místo toho samotné klíče, které mohou být na porovnání rovnosti i řádově snazší a rychlejší.[1]
Předcházení kolizím
Lze použít tohoto vzoru pro zabránění tvorby identických instancí, které by však byly "dvojčaty". Tyto by měly sice měly shodný pomyslný klíč, ale na dvou místech by se pracovalo se dvěma odlišnými instancemi a tento postup mezi nimi by se pravděpodobně stejně musel následně jednotit.
Usnadnění použití
Při implementaci se již neodkazuji pomocí instance, nýbrž pomocí klíče. Stačí mi tedy jen aby byl programátorovi znám samotný klíč, nemusí již pracovat s danou instancí, případně vkládat všechny parametry potřebné pro tvorbu instance do konstruktoru.
Spřízněné vzory
Stejně jako pro jiné vzory ze skupiny Creational patterns (vzory řízení tvorby instancí) platí, že mají soukromé konstruktory a zároveň nějakou formu přístupové metody.
Návrhový vzor Singleton (Jedináček) je v podstatě speciální druh vzoru Multiton, neboť je omezen počet instancí na maximálně jednu a tím pádem není třeba žádných klíčů. Vedle toho vzor Factory method (tovární metoda), která se stará o řízení tvorby instancí, je v jisté podobě použita, a to v rámci líné inicializace. Zde se řídí, zda má být instance vytvořena, či vrácena již existující.
Vzor Multiton se liší od návrhového vzoru Fond (Object pool nebo Bazén) v následujících specifikách:
- Funkce multitonu neřídí kdy kdo bude mít k daným instancím přístup. Fond je v tomto (v případě vytíženosti instancí, když žádná z instancí není volná) dvojího typu:
- Statický - zařazuje poptávky do fronty a ty postupně obsluhuje
- Dynamický - v případě, že není žádný objekt volný, vytvoří nový a ten předá pro vyhovění poptávce. Je pak na konkrétní implementaci, zda tento objekt zůstane mezi vytvořenými a dále k užití či bude postoupen destrukci.
- Není jasná (a většinou ani důležitá) vazba mezi klíčem a instancí, na čemž vzor Multiton de facto stojí. V případě, že potřebujeme fond, který by tuto vlastnost měl, je vhodné se uchýlit přímo k multitonu (pro statickou formu) či prohlásit instanci reprezentující fond coby instanci multitonu, tedy pro každý klíč máme celý fond instancí (pro potřeby dynamické formy).
Ukázky implementace
Následující ukázky jsou uvedeny v jazyce Java.
Příklad obecné implementace (bez přidávání prvků)
Uvažujeme-li případ bez přidávání prvků, tedy že nebudeme dynamicky (za běhu programu) upravovat množství instancí třídy Multiton
, můžeme veškerou odpovědnost za přípravu prvků přenechat na statickém iniciačním bloku v kombinaci s výčtovým typem. Tato kombinace umožňuje naplnění kontejneru metodou eager initialization (hladová inicializace), které se provede v okamžiku prvního zmínění třídy. Alternativně by bylo možné použít metody lazy initialization, kterou by bylo vhodné implementovat tak, že při dotazu na instanci k danému klíči se zkontroluje, zda tato relace existuje. Pokud ne, vytvoří se tato instance a uloží do kontejneru s přiřazeným klíčem. V každém případě se příslušnému klíči vrátí instance multitonu. U líné inicializace je však nutné ohlídat bezpečnost při aplikaci s více vlákny, neboť by mohlo dojít ke konzistenčním trhlinám.
Na ukázkových příkladech je kontejner obsahující klíč a instanci multitonu reprezentován mapou (slovníkem či asociativním polem), čímž umožňuje vyhledat instanci (Multiton
) podle zadaného klíče (Key
). Klíč může mít 5 různých podob, v tomto případě KEY_0
, KEY_1
, KEY_2
, KEY_3
, a KEY_4
. To znamená, že bude existovat maximálně pět instancí této třídy. Požaduje-li klient instanci podle vzoru klíče KEY_3
, pak zavolá přístupovou metodu, do které v parametru vloží právě tento klíč a tato metoda vrátí příslušnou instanci (zde je tato metoda reprezentována pomocí statické metody getInstance(Key)
).
Eager inicializace
Na následujícím výpise je uvedena ukázka obecného vzoru Multiton v podobě hladové inicializace.
package Multiton.general;
import java.util.HashMap;
import java.util.Map;
/**
* Třída Multiton reprezentující hlavní objekt vzoru Multiton.
*
* Tato třída obsahuje statickou mapu (slovník) složený z klíčů (instancí výčtového typu Key)
* a hodnot reprezentovaných instancemi třídy Multiton.
*
* Dále obsahuje soukromý konstruktor a statickou metodu getInstance(Key), která vrací pro daný
* klíč jedinou instanci.
*/
public class Multiton {
/** Klíče, podle kterých je vyhledávána instance třídy Multiton */
public enum Key {
KEY_0, KEY_1, KEY_2, KEY_3, KEY_4;
}
/** Klíčový atribut reprezentující instanci Multiton */
private final Key key;
/** Kontejner všech existujících instancí přiřazených k vlasním klíčům */
private static Map<Key, Multiton> MULTITONS = new HashMap<>();
static {
for (Key key : Key.values()) {
MULTITONS.put(key, new Multiton(key));
}
}
/**
* Soukromý konstruktor třídy Multiton, který zabraňuje vyrábění instancí mimo tuto třídu
*
* @param key Klíč, podle kterého bude vyhledáváno. Jde o nejdůležitější atribut
* instance a je mezi ostatními instancemi třídy Multiton unikátní.
*/
private Multiton(Key key) {
this.key = key;
}
/**
* Statická metoda getInstance, která je odpovědná za vrácení instance, která jako jediná
* má shodný atribut s tím, který byl vložen.
*
* @param key klíč, který je uložen jako atribut v rámci instance Multiton
*
* @return jedinou instanci třídy Multiton, která má tento atribut
*/
public static Multiton getInstance(Key key) {
return MULTITONS.get(key);
}
}
Lazy inicializace
Líná inicializace vyžaduje kontrolu, zda instance již neexistuje. Pokud-že neexistuje, vytvoří novou k příslušnému klíči a uloží do mapy. V každém případě pak tuto vrátí. Tento postup může být vítán zvláště pokud výroba instancí je až příliš drahá, vyžadované zdroje na instanci jsou příliš vysoké a nebo vývojář předpokládá, že nebudou využity všechny instance najednou, přesto však umožňuje jejich použití.
V následujícím bloku kódu je uvedena implementace při použití líné inicializace. Kód této statické metody nahrazuje metodu getInstance(Key): Multiton
z ukázky hladové inicializace.
/**
* Statická metoda getInstance, která je odpovědná za vrácení instance, která jako jediná
* má shodný atribut s tím, který byl vložen.
*
* Pokud instance pro takový klíč není doposud evidována, je vytvořena nová a uložena do mapy.
*
* @param key klíč, který je uložen jako atribut v rámci instance Multiton
*
* @return jedinou instanci třídy Multiton, která má tento atribut
*/
public static Multiton getInstance(Key key) {
if(!MULTITONS.containsKey(key)) {
MULTITONS.put(key, new Multiton(key));
}
return MULTITONS.get(key);
}
Dále je důležité si pro tento způsob řešení uvědomit, že:
- není třeba statického iniciačního bloku, neboť o tvorbu instancí se stará právě tato metoda
- nyní není řešení vláknově bezpečné - o to se v rámci hladové inicializace stará statický inicializační blok, který garantuje, že veškerý kód v něm uvedený bude proveden maximálně jednou
Příklad Mariáš
Cílem ukázky je vytvořit sadu mariášových karet, na které je možné se dále odkazovat, přičemž je z logiky hry nepřípustné, aby se tvořily neoprávněně nové karty (v realitě tzv. tahat karty z rukávu).
V tomto výpise jsou patrné čtyři objekty:
- Karta - reprezentace karty, které je přiřazena kombinace hodnoty (úrovně) a barvy
- Barva - seznam předem stanovených hodnot; listy, kule, žaludy a srdce
- Uroven - hodnota karty, zpravidla reprezentace římskou číslicí či slovním pojmenováním postavy; VI, VII, VIII, IX, X, Spodek, Svršek, Král a Eso
- Kombinace_BU - objekt obalující instance Barva a Uroven, který umožňuje porovnávání (překrytí metody
equals(Object)
).
V této reprezentaci pak je právě třída Karta
náš multiton a Kombinace_BU
je klíč (v tomto případě složený z kombinace barvy a hodnoty). Jinými slovy se klient užívající karet dotazuje na instanci třídy Karta
pomocí volání metody getInstance(Kombinace_BU)
, tedy vyhledává instanci pomocí klíče.
package Multiton.cards;
import java.util.HashMap;
import java.util.Map;
/**
* Instance třídy Karta reprezentují mariášovou kartu s hodnotou VI až ESO a barvou
* SRDCE, LISTY, KULE nebo ŽALUDY.
*/
public class Karta {
/** Kombinace barvy a hodnoty. Zároveň je klíčem pro potřeby vzoru Multiton */
private final Kombinace_BU kombinace;
/**
* Kontejner všech přípustných karet v programu dle kombinací reprezentovaných
* v instanci mapy
*/
private static Map<Kombinace_BU, Karta> KARTY;
/*
* Statický iniciační blok, který na začátku uloží všechny kombinace barev a hodnot
* jednotlivých karet do mapy KARTY.
*/
static {
KARTY = new HashMap<>();
for (Kombinace_BU.Barva barva : Kombinace_BU.Barva.values()) {
for (Kombinace_BU.Uroven uroven : Kombinace_BU.Uroven.values()) {
Kombinace_BU kombinace = new Kombinace_BU(barva, uroven);
KARTY.put(kombinace, new Karta(kombinace));
}
}
}
/**
* Soukromý konstruktor, který umožňuje své zavolání jen zevnitř této třídy.
*
* @param kombinace kombinace instance typu Barva a instance typu Uroven
*/
private Karta(Kombinace_BU kombinace) {
this.kombinace = kombinace;
}
/**
* Statická přístupová metoda vracející instanci karty dle zadané barvy a úrovně,
* jakou by daná karta měla mít.
*
* @param barva instance typu Barva, kterou by karta měla mít
* @param uroven instance typu Uroven, kterou by karta měla mít
*
* @return unikátní instanci Karta příslušící dodanému klíči
*/
public static Karta getInstance(Kombinace_BU.Barva barva, Kombinace_BU.Uroven uroven) {
return getInstance(new Kombinace_BU(barva, uroven));
}
/**
* Statická přístupová metoda vracející instanci karty dle zadané barvy a úrovně,
* jakou by daná karta měla mít.
*
* @param kombinace instance typu Kombinace_BU, která je
* složena z barvy a z úrovně.
*
* @return unikátní instanci Karta příslušící dodanému klíči
*/
public static Karta getInstance(Kombinace_BU kombinace) {
return KARTY.get(kombinace);
}
}
/**
* Reprezentace kombinace barvy (Srdce, listy, kule a žaludy) a úrovně (šestka až eso).
*/
class Kombinace_BU {
/**
* Instance výčtového typu Barva reprezentují základní barvy mariášových karet.
*/
public enum Barva {
LISTY, KULE, SRDCE, ZALUDY;
}
/**
* Instance výčtového typu Uroven reprezentují hodnotu jednotlivých mariášových karet.
*/
public enum Uroven {
VI, VII, VIII, IX, X, SPODEK, SVRSEK, KRAL, ESO;
}
/** Barva kombinace */
private final Barva barva;
/** Úroveň kombinace */
private final Uroven uroven;
/**
* Konstruktor tvořící kombinaci z barvy a z úrovně, kterou
* dodá volající třída.
*
* @param barva kterou by měla kombinace nést
* @param uroven kterou by měla kombinace nést
*/
public Kombinace_BU(Barva barva, Uroven uroven) {
this.barva = barva;
this.uroven = uroven;
}
/**
* Metoda proovnávající zaměnitelnost této kombinace s dodanou kombinací
*
* @param o objekt, který má být porovnán s tímto objektem
*
* @return metoda vrací pravdu jen pokud jsou barvy i úrovně totožné.
*/
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Kombinace_BU that = (Kombinace_BU) o;
return barva == that.barva && uroven == that.uroven;
}
}
Pro tuto implementaci platí, že na instanci je možné se ptát pomocí instance Kombinace_BU
nebo pomocí dvojice Barva
a Uroven
, z nichž se právě daná kombinace skládá. To umožňuje překrytá metoda Kombinace_BU#equals(Object): boolean
z třídy Object
. Díky ní je možné efektivně (a mnohdy velmi rychle) ověřovat ekvivalenci instancí (klíče) a tím často předstihnout ad hoc vytváření instancí jejich zpětným dohledáváním.[1]
Můžeme tímto dosáhnout například snadného získávání instancí třídy Karta
při snadném skládání úrovně a barvy karty, kterou požadujeme. Tento model můžeme využít například v situaci, kdy každé instanci karty náleží obrázek karty, který je třeba složitě načítat z úložiště v konstruktoru, stejně jako je zbytečné mít duplikáty těchto obrázků uložené v paměti a zbytečně toto místo zabírat.
Problémy s použitím vzoru
Použití ve vícevláknových aplikacích
Vícevlánkové implementace mohou mít fatální vliv na konzistenci stavů aplikací i v rámci vzorů řídících tvorbu instancí. Častým řešením pro Multiton bývá statický iniciační blok, který garantuje provedení svého těla maximálně jednou. Tento způsob však může být nedostatečný, chceme-li mít možnost přidávat instance dynamicky. Zde se musíme uchýlit k řešení pomocí lazy inicializace, případné kombinace s eager. Musíme však synchronizovat vlákna a zajistit, že některé kusy kódu nelze provádět ze dvou míst zároveň.
Pokud-že bychom neprovedli těchto bezpečnostních opatření, aplikace může na dvou místech užívat dvě různé instance se shodným klíčem, mohou nad těmito objekty provádět různých operací, ovšem promítnuty budou jen právě na té instanci, se kterou daná část kódu pracuje. To může ohrozit bezpečný chod celého programu a narušit logiku celého systému.
Problémy se serializací
Při serializaci a jejím praktickém použití v aplikacích (například zápis objektů do síťových kanálů) může způsobit existenci dvou rozdílných instancí se shodným klíčem a může dojít problémů uvedeným v předchozí podkapitole. Problémům se serializací se dá bránit - například v Javě implementací metody readResolve(): Object
(vyhazující výjimku ObjectStreamException
), pomocí které lze stroji napovědět, jak má být nakládáno s právě přečteným objektem.[1]
Reference
- PECINOVSKÝ, RUDOLF. Návrhové vzory : [33 vzorových postupů pro objektové programování]. Vyd. 1. vyd. Brno: Computer Press 527 s. Dostupné online. ISBN 978-80-251-1582-4, ISBN 80-251-1582-8. OCLC 190426348