# Proxy

V živote má človek len obmedzený čas a energiu na to, aby si všetko riešil sám. Preto mu v mnohých situáciách pomáhajú ľudia, ktorí konajú v jeho mene a sprostredkúvajú mu rôzne služby - typický príklad je napr. splnomocenstvo na komunikáciu s úradmi. Takíto ľudia sú zástupcovia - angl. proxies.

V objektovo orientovanom programovaní má návrhový vzor proxy podobnú myšlienku. Cieľom je vytvoriť zástupcu, ktorý spravuje prístup k originálnemu objektu. V praxi sa proxy používa na množstvo rôznych účelov, medzi bežné patria lenivá incializácia (angl. lazy initialization), kontrola prístupu, zastúpenie vzdialeného objektu/služby či napr. správa vyrovnávacej pamäti. Práve posledné spomínané využitie je témou tejto kapitoly.

# Modelová situácia

V tejto modelovej situácii sme dostali za úlohu vytvoriť správu vyrovnávacej pamäte pre repozitár používateľov. Dáta používateľov sa nemenia príliš často, preto pre zrýchlenie operácií môžeme používateľove údaje držať pre rýchly prístup istý čas vo vyrovnávacej pamäti.

Začnime s definovaním objektu používateľa. Pre ukážku nám stačí, aby mal identifikátor, meno, priezvisko a krátky voliteľný popis o sebe. K týmto parametrom vygenerujeme príslušný konštruktor, gettery a aj toString metódu pre lepší výpis v konzole.

public class User {

    private final long id;
    private final String name;
    private final String surname;
    private final String bio;

    // constructor, getters and toString omitted for brevity
}

Používatelia budú uložení v repozitári s nasledujúcim rozhraním. Nateraz bude repozitár podporovať len existenciu jedného používateľa súčasne (čo neskôr zmeníme):

public interface UserRepository {

    User getUser();

    void setUser(User user);

    void removeUser();
}

Repozitáre ponúkajú prístup k entitám, ktoré sú niekde uložené. Ich umiestnenie je však v réžii samotného repozitára - môže to byť nielen databáza, ale napr. aj súbor na disku. Pre jednoduchosť ukážky zvolíme ukladanie do pamäte. Preto vytvoríme MemoryUserRepository s nasledujúcou jednoduchou implementáciou:



 



 
 




 




 



public class MemoryUserRepository implements UserRepository {

    private User user;

    @Override
    public User getUser() {
        System.out.println("Accessing user repository...");
        return user;
    }

    @Override
    public void setUser(User user) {
        this.user = Objects.requireNonNull(user);
    }

    @Override
    public void removeUser() {
        this.user = null;
    }
}

Všimnite si hlavne metódu getUser - do konzole vypisujeme informáciu, že kód sa snažil získať používateľa. Tento výpis nám v ďalších ukážkach pomôže zistiť, kedy sa používateľ vyberal z repozitára a kedy z vyrovnávacej pamäti.

Funkčnosť tejto implementácie si môžeme vyskúšať na nasledujúcej jednoduchej ukážke:

 


 


 



 

 



 

 



> User user = new User(1, "John", "Doe", "Professional placeholder, father of none.");
Defined field User user = User(1, John Doe, Professional placeholder, father of none.)

> UserRepository repository = new MemoryUserRepository();
Defined field UserRepository repository = sk.tuke.fei.kpi.oop.chapters.structural.proxy.UserRepository@e37b221a

> repository.getUser();
Accessing user repository...
null

> repository.setUser(user);

> repository.getUser();
Accessing user repository...
User(1, John Doe, Professional placeholder, father of none.)

> repository.removeUser();

> repository.getUser();
Accessing user repository...
null

V tejto chvíli máme základnú implementáciu hotovú a môžeme postupne začať pracovať na vyrovnávacej pamäti. Pri návrhovom vzore proxy využívame existujúce rozhrania služieb a pre zvyšok aplikácie sa tvárime, že sme pôvodná služba. Začneme podobne, ako pri vzore dekorátor - vytvoríme triedu CachedUserRepository, ktorá bude implementovať rozhranie UserRepository. Nateraz bude len akceptovať v konštruktore parameter pôvodnej služby a bude jej delegovať všetky volania metód. Ak by sme takto obalili premennú repository z ukážky vyššie, program by fungoval úplne rovnako:



 


 




 




 




 



public class CachedUserRepository implements UserRepository {

    private final UserRepository repository;

    public CachedUserRepository(UserRepository repository) {
        this.repository = repository;
    }

    @Override
    public User getUser() {
        return repository.getUser();
    }

    @Override
    public void setUser(User user) {
        repository.setUser(user);
    }

    @Override
    public void removeUser() {
        repository.removeUser();
    }
}

Keď sme si istí, že pôvodná funkcionalita stále funguje, môžeme postupne začať pracovať na ukladaní používateľa vo vyrovnávacej pamäti. Na začiatok môžeme vyskúšať jednoduchú verziu podobnú lenivej inicializácii:



 



 
 
 
 





public class CachedUserRepository implements UserRepository {

    private User cachedUser;

    @Override
    public User getUser() {
        if (cachedUser == null) {
            cachedUser = repository.getUser();
        }
        return cachedUser;
    }

    // rest omitted for brevity
}

Tento kód nám zabezpečí, že opakované volania metódy getUser nebudú vždy pristupovať k obalenému repozitáru, ale v prípade existencie inštancie používateľa vo vyrovnávacej pamäti ju vrátia. To môžeme vidieť aj vo výpisoch nasledujúcej ukážky:












 
 


 


> User user = new User(1, "John", "Doe", "Professional placeholder, father of none.");
Defined field User user = User(1, John Doe, Professional placeholder, father of none.)

> UserRepository memoryRepository = new MemoryUserRepository();
Defined field UserRepository memoryRepository = sk.tuke.fei.kpi.oop.chapters.structural.proxy.UserRepository@e37b221a

> UserRepository repository = new CachedUserRepository(memoryRepository);
Defined field UserRepository memoryRepository = sk.tuke.fei.kpi.oop.chapters.structural.proxy.CachedUserRepository@ff35adc2

> repository.setUser(user);

> repository.getUser();
Accessing user repository...
User(1, John Doe, Professional placeholder, father of none.)

> repository.getUser();
User(1, John Doe, Professional placeholder, father of none.)

Táto implementácia má však jeden kritický problém - vyrovnávaca pamäť nikdy neexpiruje. To znamená, že aj keď používateľa aktualizujeme, už vždy dostaneme predchádzajúcu verziu a kód už nezájde do repozitára pre najnovšiu verziu:

 




 
 

> User updatedUser = new User(1, "John", "Doe", "Currently unemployed");
Defined field User user = User(1, John Doe, Currently unemployed)

> repository.setUser(user);

> repository.getUser();
User(1, John Doe, Professional placeholder, father of none.)

Je preto nutné zariadiť, aby sa používateľ vo vyrovnávacej pamäti po istom čase obnovoval. To môžeme dosiahnuť tým, že uložená inštancia by mala len určitú životnosť, pričom ak tá skončí, vyberieme používateľa opäť z repozitára.

Pri tejto jednoduchej implementácii by sme mohli uvažovať o tom, že si pridáme ďalšiu premennú, ktorá bude uchovávať informáciu o životnosti. Určite by to fungovalo, avšak akonáhle by sme potrebovali implementáciu škálovať na viacero používateľov, nebolo by to efektívne. Preto si namiesto toho vytvoríme jednoduchú vnorenú triedu, ktorá bude obaľovať inštanciu používateľa a dátum a čas konca platnosti záznamu:







 
 
















public class CachedUserRepository implements UserRepository {

    // rest omitted for brevity

    private class UserCache {

        private final LocalDateTime expirationTime;
        private final User user;

        private UserCache(LocalDateTime expirationTime, User user) {
            this.expirationTime = expirationTime;
            this.user = user;
        }

        public LocalDateTime getExpirationTime() {
            return expirationTime;
        }

        public User getUser() {
            return user;
        }
    }
}

Poznámka

Ako vidíte, trieda UserCache nie je označená ako static to znamená, že jej inštancie môžu existovať len v rámci inštancií materskej CachedUserRepository a zároveň objekty triedy UserCache majú prístup k privátnym metódam a atribútom materského objektu.

Keď máme triedu pripravenú, v prvom kroku si upravíme ukladanie používateľa. Najprv vytvoríme konštantu TIMEOUT, ktorá bude držať informáciu o životnosti záznamu vo vyrovnávacej pamäti. Využijeme pri tom triedu Duration, ktorá je súčasťou moderného Java DateTime API, čím si pohodlne môžeme ukladať ľubovoľnú životnosť - v tomto prípade trojsekundovú pomocou volania statickej factory metódy Duration.ofSeconds(3):



 

 




 

 





public class CachedUserRepository implements UserRepository {

    private static final Duration TIMEOUT = Duration.ofSeconds(3);

    private UserCache cachedUser;

    @Override
    public User getUser() {
        if (cachedUser == null) {
            cachedUser = new UserCache(repository.getUser(), LocalDateTime.now().plus(TIMEOUT));
        }
        return cachedUser.getUser();
    }

    // rest omitted for brevity
}

Poznámka

Triedy z Java DateTime API sú nemenné (immutable), to znamená, že volanie LocalDateTime.now().plus(TIMEOUT) nám vráti novú inštanciu triedy LocalDateTime, ktorá bude predstavovať čas a dátum v budúcnosti posunutú od aktuálneho o hodnotu konštanty TIMEOUT.

Ukážka nám bude fungovať, no stále však s rovnakým problémom. Preto musíme pridať aj kontrolu, ktorá umožní, že vyrovnávaca pamäť sa obnoví po jej expirovaní. Vytvoríme preto metódu isExpired, ktorá túto kontrolu vykoná a zavoláme ju v podmienke pre vytvorenie záznamu v metóde getUser:





 





 
 
 









public class CachedUserRepository implements UserRepository {

    @Override
    public User getUser() {
        if (cachedUser == null || cachedUser.isExpired()) {
            cachedUser = new UserCache(repository.getUser(), LocalDateTime.now().plus(TIMEOUT));
        }
        return cachedUser.getUser();
    }

    private class UserCache {
        
        public boolean isExpired() {
            return LocalDateTime.now().isAfter(expirationTime);
        }

        // rest omitted for brevity
    }

    // rest omitted for brevity
}

V tejto chvíli je všetko hotové. Správnosť implementácie si môžeme overiť na nasledujúcom príklade v JShell, kde budeme pomocou metódy Thread.sleep simulovať vypršanie platnosti vyrovnávacej pamäte:

> User user = new User(1, "John", "Doe", "Professional placeholder, father of none.");
Defined field User user = User(1, John Doe, Professional placeholder, father of none.)

> UserRepository memoryRepository = new MemoryUserRepository();
Defined field UserRepository memoryRepository = sk.tuke.fei.kpi.oop.chapters.structural.proxy.UserRepository@e37b221a

> UserRepository repository = new CachedUserRepository(memoryRepository);
Defined field UserRepository memoryRepository = sk.tuke.fei.kpi.oop.chapters.structural.proxy.CachedUserRepository@ff35adc2

> repository.setUser(user1);

> repository.getUser(10);
Accessing user repository...
User(1, John Doe, Professional placeholder, father of none.)

> repository.getUser(10);
User(1, John Doe, Professional placeholder, father of none.)

> Thread.sleep(3500);

> repository.getUser(10);
Accessing user repository...
User(1, John Doe, Professional placeholder, father of none.) 

> User user1Updated = new User(1, "John", "Doe", "Currently unemployed");
Defined field User user = User(1, John Doe, Currently unemployed)

> repository.setUser(user1Updated);

> repository.getUser(10);
User(1, John Doe, Professional placeholder, father of none.)

> Thread.sleep(3500);

> repository.getUser(10);
Accessing user repository...
User(1, John Doe, Currently unemployed)

# Best practices

  • 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í proxy.
  • Namiesto vytvárania proxy s priveľkým množstvom funkcionality ich oddeľte a vnorte do seba.
  • Nebojte sa používať proxy 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. Odstráňte záznam vyrovnávacej pamäti spolu s odstránením používateľa

Aktuálna implementácia funguje tak, že pokiaľ aktualizujeme používateľa, nedostaneme údaje hneď. Túto limitáciu prijímame, lebo neočakávame, že používatelia by si aktualizovali svoje údaje priveľmi často. Problémom však ostáva odstránenie - v tomto prípade by sme z viacerých dôvodov chceli (napr. GDPR), aby bola akcia odstránenia používateľa efektívna okamžite. Vašou úlohou je teda zaručiť, aby sa prípadný existujúci záznam o používateľovi pri jeho odstránení odstránil aj z vyrovnávacej pamäti.

# 2. Pridajte podporu správy viacerých používateľov

Repozitár používateľov, ktorý podporuje len jedného používateľa, nemá z praktických dôvodov žiadnu hodnotu. Vašou úlohou je preto rozšíriť implementáciu repozitára aj vyrovnávacej pamäte tak, aby podporovali ukladanie ľubovoľného počtu používateľov (obmedzeného len operačnou pamäťou). Získavanie a odstraňovanie zabezpečte cez používateľove ID, pričom metóda getUser by mala vrátiť Optional, aby bolo možné efektívne pracovanie s prípadnou absenciou hodnoty.

# 3. Umožnite voliteľné nastavenie životnosti záznamov a maximálnej veľkosti vyrovnávacej pamäte

Životnosť, ktorú sme si pre záznamy vyrovnávacej pamäte nastavili, by bolo vhodné umožniť konfigurovať. Rovnako výhodné by bolo mať konfigurovateľnú veľkosť tejto pamäte. Vašou úlohou je preto:

  • Umožnenie predania životnosti (Duration) a maximálnej veľkosti (int) vyrovnávacej pamäte v konštruktore Proxy
  • Ponechanie aktuálneho konštruktora proxy, ktorý za tieto atribúty nastaví predvolené hodnoty

# Riešenia úloh

# Pozri tiež

# Diskusia