# Dekorátor

V ideálnom svete by sme vlastnili a mohli upravovať všetok kód, s ktorým pracujeme. Nanešťastie, v praxi to tak nefunguje. Používame externé knižnice, zastaraný (neotestovaný) kód, či zdieľaný kód medzi viacerými tímami. Tie sú mimo náš dosah, preto ak ich potrebujeme modifikovať, resp. rozšíriť ich funkcionalitu, potrebujeme to urobiť bez ich úpravy. Napriek tomu, že to znie protichodne, je to možné docieliť - a to pomocou návrhového vzoru dekorátor.

# Modelová situácia

V každej väčšej aplikácií alebo informačnom systéme pracujeme s databázou, nech už je akákoľvek. Operácie s ňou patria do kategórie vstupno/výstupných (I/O), ktoré sú spravidla najdrahšie a najdlhšie trvajúce. Preto optimalizácia prístupov, napr. SQL príkazov, je pre nás dôležitá, aby sme zvýšili výkon aplikácie a tým znížili cenu prevádzky.

Poďme sa pozrieť na to, ako by mohol vyzerať ukážkový prístup k SQL databáze. Začneme konektorom, ktorý bude mať 2 metódy - connect na pripojenie a execute na vykonanie príkazu. V reálnom svete by execute mal v prípade SELECT príkazov vrátiť nejaké dáta a UPDATE, INSERT a DELETE počet ovplyvnených záznamov, no pre ukážku nebude mať žiadnu návratovú hodnotu.

public interface DatabaseConnector {

    void connect();

    void execute(String query);
}

Aby sme simulovali reálny konektor, vytvoríme MySQLDatabaseConnector, ktorý pri volaní oboch metód len na náhodne dlhú dobu uspí vlákno a teda vytvorí akúsi simuláciu vykonávania príkazu a pripájania sa. Trieda vyzerá nasledovne:

public class MySQLDatabaseConnector implements DatabaseConnector {

    private final Random random = new Random();

    @Override
    public void connect() {
        randomSleep();
        randomSleep();
        randomSleep();
        randomSleep();
    }

    @Override
    public void execute(String query) {
        randomSleep();
    }

    private void randomSleep() {
        int milliseconds = random.nextInt(1000);
        try {
            TimeUnit.MILLISECONDS.sleep(milliseconds);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

Poďme si ho "vyskúšať" na ukážkovom kóde:

> DatabaseConnector connector = new MySQLDatabaseConnector();
Defined field DatabaseConnector connector = sk.tuke.fei.kpi.oop.chapters.structural.decorator.DatabaseConnector@5ffc355b

> connector.connect();
> connector.execute("SELECT * FROM users");
> connector.execute("DELETE FROM users WHERE id = 134");

Prirodzene, nič sa reálne nestane, len sa jednotlivé volania nevykonajú hneď, ale budeme mať náhodnú odozvu. A to je práve to, čo potrebujeme merať. Ako dlho trvá pripojenie na databázu? Ako dlho trvá vykonanie jednotlivých SQL príkazov? Všetky tieto poznatky môžu byť dôležité pri optimalizácii nášho kódu.

Vytvoríme si preto dekorátor s názvom MonitoredDatabaseConnector, ktorý implementuje rozhranie DatabaseConnector. Ten bude mať jediný atribút - iný ľubovoľný databázový konektor, ktorému bude zatiaľ len delegovať všetky volania metód rozhrania. Trieda tak vyzerá nasledovne:



 







 




 



public class MonitoredDatabaseConnector implements DatabaseConnector {

    private final DatabaseConnector databaseConnector;

    public MonitoredDatabaseConnector(DatabaseConnector databaseConnector) {
        this.databaseConnector = Objects.requireNonNull(databaseConnector);
    }

    @Override
    public void connect() {
        databaseConnector.connect();
    }

    @Override
    public void execute(String query) {
        databaseConnector.execute(query);
    }
}

Čo je dôležité si uvedomiť je to, že keďže náš dekorátor rozširuje rozhranie pre databázové konektory, môže sa tváriť, že ním je aj naozaj, a tak nám bezproblémovo zapadne už do existujúceho kódu bez toho, aby ten vedel o našej extra funkcionalite.

Že sa ukážka zhora bude správať úplne rovnako aj po dekorovaní konektora si môžeme demonštrovať. Začnime tým, že si vytvoríme novú premennú monitoredConnector, ktorá bude obsahovať dekorovanú premennú connector:




 


> DatabaseConnector connector = new MySQLDatabaseConnector();
Defined field DatabaseConnector connector = sk.tuke.fei.kpi.oop.chapters.structural.decorator.DatabaseConnector@58f838eb

> DatabaseConnector monitoredConnector = new MonitoredDatabaseConnector(connector);
Defined field DatabaseConnector monitoredConnector = sk.tuke.fei.kpi.oop.chapters.structural.decorator.DatabaseConnector@45c0df8a

Následne vyskúšajme volania metód a pozorujme, že správanie ostáva rovnaké:

> connector.connect();
> connector.execute("SELECT * FROM users");
> connector.execute("DELETE FROM users WHERE id = 134");

V tejto chvíli môžeme skúsiť pridať základné logovanie. Zatiaľ len vypíšme správy pred a po vykonaní metódy, pričom v prípade vykonania SQL príkazu zaznamenáme aj ten:





 

 




 

 





public class MonitoredDatabaseConnector implements DatabaseConnector {

    @Override
    public void connect() {
        System.out.println("Connecting to database...");
        databaseConnector.connect();
        System.out.println("Connected!");
    }

    @Override
    public void execute(String query) {
        System.out.println("Executing query: " + query);
        databaseConnector.execute(query);
        System.out.println("Executed!");
    }

    // rest omitted for brevity
}

Môžeme znovu spustiť test, aby sme videli výsledky:

> monitoredConnector.connect();
Connecting to database...
Connected!

> monitoredConnector.execute("SELECT * FROM users");
Executing query: SELECT * FROM users
Executed!

> monitoredConnector.execute("DELETE FROM users WHERE id = 134");
Executing query: DELETE FROM users WHERE id = 134
Executed!

Už teraz máme čiastočný úspech - v logoch nájdeme informáciu nielen o úspešnom pripojení sa k databáze, ale aj o tom, aké príkazy sa voči nej vykonávajú. Ostáva nám už len pridať stopky, ktoré zaznamenajú dĺžku vykonávania. Najprv ukážeme kód a potom si ho vysvetlíme:

public class MonitoredDatabaseConnector implements DatabaseConnector {
    
    private final DecimalFormat formatter = new DecimalFormat("0.0#####");

    private String watch(Runnable runnable) {
        long start = System.nanoTime();
        runnable.run();
        long end = System.nanoTime();

        return formatter.format((end - start) / 1000000D);
    }

    // rest omitted for brevity
}

Metóda watch akceptuje funkciu, ktorá nič nevracia a nič neprijíma ako argument. Do nej potom neskôr vložíme volanie obaľovaného konektora. Robíme to tak preto, aby sme mohli pred aj po volaní zaznamenať aktuálny čas v nanosekundách. Rozdiel týchto časov vydelíme miliónom, čím získame čas vykonávania v milisekundách. Aby sme tento údaj zobrazili v rozumnom formáte, použijeme utilitu DecimalFormat, aby sme milisekundy ukázali s celou našou presnosťou - teda na milióntiny.

Ostáva nám zabaliť touto metódou delegáciu volania pôvodného konektora a vypísať výsledok do konzoly:






 
 




 
 
 





public class MonitoredDatabaseConnector implements DatabaseConnector {
    
    @Override
    public void connect() {
        System.out.println("Connecting to database...");
        String connectingTime = watch(databaseConnector::connect);
        System.out.println("Connected! Total time: " + connectingTime + " milliseconds.");
    }

    @Override
    public void execute(String query) {
        System.out.println("Executing query: " + query);
        String executionTime = watch(() -> databaseConnector.execute(query));
        System.out.println("Executed! Total time: " + executionTime + " milliseconds.");
    }

    // rest omitted for brevity
}

Celý príklad si tak môžeme spustiť posledný krát, aby sme videli finálny výsledok nášho dekorátora:

> DatabaseConnector connector = new MySQLDatabaseConnector();
Defined field DatabaseConnector connector = sk.tuke.fei.kpi.oop.chapters.structural.decorator.DatabaseConnector@58f838eb

> DatabaseConnector monitoredConnector = new MonitoredDatabaseConnector(connector);
Defined field DatabaseConnector monitoredConnector = sk.tuke.fei.kpi.oop.chapters.structural.decorator.DatabaseConnector@45c0df8a

> monitoredConnector.connect();
Connecting to database...
Connected! Total time: 1602.724497 milliseconds.

> monitoredConnector.execute("SELECT * FROM users");
Executing query: SELECT * FROM users
Executed! Total time: 996.824329 milliseconds.

> monitoredConnector.execute("DELETE FROM users WHERE id = 134");
Executing query: DELETE FROM users WHERE id = 134
Executed! Total time: 636.923635 milliseconds.

# Best practices

  • Obalený objekt môžete nazývať aj všeobecne delegate
  • Delegujte volania na všetky metódy rozhraní, vrátane tých s predvolenou logikou (metódy označené default). Zaručíte tak, že ak niektorá implementácia tieto metódy preťaží, funkcionalita zostane zachovaná aj pri použití dekorátora.
  • Namiesto vytvárania dekorátorov s priveľkým množstvom funkcionality dekorátory oddeľte a vnorte ich do seba
  • Nebojte sa používať dekorátor aj v prípade, že dokážete upraviť zdrojový kód dekorovanej triedy. Dosiahnete tým izoláciu funkcionality a dodržanie princípu jednej zodpovednosti. Dávajte však pozor, aby ste to neprehnali.

# Úlohy

# 1. Vytvorte dekorátory pre mapu a zoznam, ktoré umožnia získavanie prvkov cez triedu Optional

Už sme viackrát spomínali, že nulová referencia je niečo, čo by sme v ideálnom svete nemali, a v praxi tak používame rôzne triky, napr. triedu Optional, ktorou sa snažíme tento "stav ničoty" lepšie riešiť. Bohužiaľ, táto trieda prišla do Javy až vo verzii 8, preto ju staré rozhrania neobsahujú.

Vašou úlohou bude vytvoriť dekorátor OptionalList a OptionalMap, ktoré obalia rozhrania List a Map a okrem delegovania volaní všetkých metód vytvoria niekoľko nových metód, ktoré pri vrátení prvkov z týchto kolekcií obalia triedou Optional. Ako príklad, ak zavoláme list.getOptional(5), vráti nám to prvok na indexe 5 obalený Optional, resp. vráti Optional.empty(), pokiaľ daný index neexistuje. Podobne to spravte so všetkými metódami, ktoré môžu pri rôznych operáciach vrátiť späť prvok z kolekcie.

Poznámka

Aby sme mohli využiť tieto nové metódy, musíme si prípadnú premennú nadefinovať s dátovým typom OptionalList alebo OptionalMap namiesto rozhraní List a Map. Stále však platí, že ak by nejaká metóda akceptovala len všeobecné rozhranie, náš dekorátor zabezpečí, že pôvodná funkcionalita objektov zostane nedotknutá

# Riešenia úloh

# Diskusia