# Singleton

V predchádzajúcich kapitolách sme vyriešili, ako vhodne vytvárať objekty, no tým sme kreačnú problematiku nevyčerpali. V mnohých prípadoch máme ako podmienku, že trieda musí mať nanajvýš 1 existujúcu inštanciu, resp. by bolo plýtvaním ich mať viacero naraz. Najjednoduchší návrhový vzor, ktorý rieši tento problém, je singleton.

# Modelová situácia

Aby sme vedeli, čo programy vykonávajú, resp. či sa vykonávajú správne, potrebujeme sledovať jeho správanie. Veľmi účinné je tzv. logovanie - výpis ladiacich správ. Podrobnejšie si to popíšeme v kapitolách template metóda a stratégia, nateraz si vystačíme s nasledujúcou primitívnou implementáciou, ktorá vypisuje správy do konzoly:

public class Logger {

    public void log(String message) {
        System.out.println(message);
    }
}

Jeho používanie je jednoduché - stačí si vytvoriť inštanciu a zavolať metódu log. Správa sa následne vypíše do konzoly:

> Logger logger = new Logger();
Defined field Logger logger = sk.tuke.fei.kpi.oop.chapters.creational.singleton.Logger@a67dcf21

> logger.log("Test message");
Test message

Nedáva nám však veľký zmysel, aby sme vytvárali novú inštanciu pre každú triedu alebo metódu, kde by sme potrebovali logovanie použiť. Najlepšie riešenie by preto bolo, aby sme mali v jeden moment len jednu inštanciu triedy Logger. Ako to však zaručiť?

Začneme tým, že zabránime vytváraniu inštancií mimo triedy. Všetko, čo nám na to treba, je vytvoriť konštruktor, ktorý označíme ako private:



 






public class Logger {

    private Logger() {
        // private constructor to mark this class singleton
    }

    // rest omitted for brevity
}

Privátny konštruktor je možné volať len z vnútra triedy. To využijeme a vytvoríme privátnu vnútornú statickú konštantu, ktorá bude držať jedinú existujúcu inštanciu logera. K nej pridáme aj verejný statický getter:



 

 






public class Logger {

    private static final Logger instance = new Logger();

    public static Logger getInstance() {
        return instance;
    }

    // rest omitted for brevity
}

Logger stále používame rovnako, mení sa však spôsob, akým sa dostaneme k jeho inštancii. Zároveň si všimnime, že viacnásobné volanie gettera getInstance vráti rovnaký objekt, čo vidíme na hashi premennej zobrazenej v JShelli:

 
 


 




> Logger logger = Logger.getInstance();
Defined field Logger logger = sk.tuke.fei.kpi.oop.chapters.creational.singleton.Logger@a67dcf21

> Logger logger2 = Logger.getInstance();
Defined field Logger logger2 = sk.tuke.fei.kpi.oop.chapters.creational.singleton.Logger@a67dcf21

> logger.log("Test message");
Test message

Čo si však musíme uvedomiť - statické premenné sú inicializované okamžite po štarte programu. Preto ak by vytváranie objektu trvalo dlho, resp. by potrebovalo veľa zdrojov na svoju existenciu, je zbytočné mať vytvorenú inštanciu aj v čase, kedy nevieme, či ju vôbec program použije.

Riešením je tzv. lenivá inicializácia. Konštantu instance zmeníme na premennú a na začiatku jej priradíme nulovú hodnotu. V getteri následne kontrolujeme, či inštancia bola vytvorená - ak nie, tak premennej priradíme hodnotu. Následne vrátime referenciu na inštanciu. To nám zaručí, že vytvorená bude až pri prvom použití v programe, nie skôr:



 


 
 








public class Logger {

    private static Logger instance = null;

    public static Logger getInstance() {
        if (instance == null) {
            instance = new Logger();
        }

        return instance;
    }

    // rest omitted for brevity
}

A to je všetko, čo k vzoru singleton potrebujeme vedieť.

Poznámka

Privátny konštruktor zaručí, že inštanciu triedy nevytvoríte normálnym spôsobom, no pomocou meta programovania za pomoci reflekcie (Java Reflection API) je to stále možné.

# Best practices

  • Singleton je považovaný za tzv. antipattern, teda nevhodný návrhový vzor. V praxi existuje len veľmi málo validných využití singletonu, ako napr. demonštrovaný logger či statická factory. Pokiaľ je to možné, využite návrhový vzor dependency injection.
  • Statický getter je vhodné nazývať getInstance, getDefaultInstance a podobne.

# Úlohy

# 1. Súborový logger

Keď spustíte svoj program do produkcie, zvyčajne už nebudete mať dosah na konzolu, resp. logov by bolo tak veľa, že by ste sa v nich ťažko orientovalo. Preto sa v praxi používa zápis logov do súboru, ktorý následne môžete ďalej spracovať aj automatizovanými nástrojmi.

Vašou úlohou bude vytvoriť triedu FileLogger, ktorá umožní zápis logov do súboru. V tomto prípade však nepôjde o singleton na úrovni triedy, ale singleton na úrovni súborov - môžete otvoriť viacero súborových loggerov, avšak vždy len jeden na súbor. Trieda by mala mať 2 statické gettery:

  • getInstance, ktorá akceptuje názov súboru
  • getDefaultInstance, ktorá nemá žiadne argumenty a vráti logger s predvoleným súborom (jeho názov je na vás)

# Riešenia úloh

# Diskusia