# Chain of responsibility

Keďže sme si ukázali, že modularita má pre náš kód množstvo výhod, v jej zvyšovaní budeme pokračovať aj v tejto kapitole.

Predstavme si, že máme nejakú úlohu. My na jej riešenie vypracujeme Plán A, no v prípade núdze potrebujeme aj zálohu, preto máme pripravený aj Plán B. Keď príde na riešenie úlohy, Plán A môže byť úspešný a nepotrebujeme už nič viacej. No niekedy sa stane, že pre jeho neúspech siahneme po záložnom Pláne B. Ten môže opäť uspieť, resp. zlyhať. V prípade neúspechu však už niet alternatívy, preto by nám neostalo nič iné, len priznať definitívnu porážku.

Reťazec zodpovednosti (angl. chain of responsibility) je práve ten návrhový vzor, ktorý rieši delegáciu problému na ďalší spoj v reťazci, predčasné skončenie delegácie a taktiež aj ošetrenie posledného spoja.

# Modelová situácia

Pri pracovaní s dátami od používateľa platí dôležité pravidlo - nikdy never vstupu. Keď vám používateľ pošle na server dáta, vy sa musíte uistiť o ich správnosti a integrite, než ich budete ďalej spracovávať a ukladať.

Poďme sa pozrieť na jednoduchý príklad, ktorý sme tu už mali - blogovací systém. Začnime dátovou štruktúrou, ktorá bude reprezentovať používateľom vytvorený článok:

public class Article {

    private final String author;
    private final String title;
    private final String content;
    private final String slug;
    private final Set<String> categories;

    // Constructor and getters omitted for brevity
}

Pre atribúty tejto štruktúry platí nasledovné:

Atribút Príklad Podmienky
author Jozef Mrkvička Nenulový, neprázdny
title Môj prvý článok Nenulový, neprázdny
content Toto je môj prvý článok Nenulový, neprázdny
slug moj-prvy-clanok Nenulový, neprázdny, regex: ^[a-z0-9-]+$
categories [personal, food] Nenulový, neprázdny, hodnoty len z: uncategorized, personal, food

Ak by sme chceli vytvoriť validačnú funkciu len s použitím podmienok, zistili by sme, že je to neprehľadné, náročné na údržbu a ťažkopádne rozšíriteľné:

public boolean validateArticle(Article article) {
    if (article == null) {
        return false;
    }

    if (article.getAuthor() == null || article.getAuthor().isEmpty()) {
        return false;
    }

    if (article.getTitle() == null || article.getTitle().isEmpty()) {
        return false;
    }

    if (article.getContent() == null || article.getContent().isEmpty()) {
        return false;
    }

    if (article.getSlug() == null || !article.getSlug().matches("^[a-z0-9-]+$")) {
        return false;
    }

    Set<String> categories = Set.of("uncategorized", "personal", "food");
    if (article.getCategories() == null || categories.containsAll(article.getCategories())) {
        return false;
    }
    
    return true;
}

Poďme sa preto snažiť vytvoriť validátor, ktorému nadefinujeme pravidlá a následne bude môcť konzumovať objekt článku, ktorý overí jeho validitu.

Začnime veľmi jednoducho. V štandardnej knižnici Javy máme triedu Predicate<T>, ktorá akceptuje atribút typu T a vráti boolovskú hodnotu. Nebudeme však používať priamo ju, ale zabalíme ju do triedy Validator<T>. Implementácia by mohla vyzerať nasledovne:

public class Validator<T> {

    private final Predicate<T> predicate;

    public Validator(Predicate<T> predicate) {
        this.predicate = Objects.requireNonNull(predicate);
    }
}

Samozrejme, predikát musíme vedieť aj zavolať. Preto vytvoríme metódu validate, ktorá mu bude toto volanie delegovať:

public boolean validate(T element) {
    return predicate.test(element);
}

Túto zatiaľ primitívnu implementáciu si môžeme vyskúšať v JShell-i:

> Validator<String> validator = new Validator<String>(string -> string != null);
Defined field Validator<String> validator = sk.tuke.fei.kpi.oop.chapters.behavioral.chainOfResponsibility.Validator@329c3aeb

> validator.validate(null);
false

> validator.validate("string");
true

Aby ste zápis o niečo málo skrášlili (a aby sme sa trošku pripravili na ďalšie rozšírenie), konštruktor urobíme privátnym a skryjeme ho do novej statickej factory metódy rule:



 



 
 
 




public class Validator<T> {

    private Validator(Predicate<T> predicate) {
        this.predicate = Objects.requireNonNull(predicate);
    }

    public static <T> Validator<T> rule(Predicate<T> validator) {
        return new Validator<>(validator);
    }

    // rest omitted for brevity
}

Vytvorenie validátora potom bude vyzerať nasledovne:

 








> Validator<String> validator = Validator.<String>rule(string -> string != null);
Defined field Validator<String> validator = sk.tuke.fei.kpi.oop.chapters.behavioral.chainOfResponsibility.Validator@329c3aeb

> validator.validate(null);
false

> validator.validate("string");
true

Poznámka

Lambdu string -> string != null, kontrolujúcu, či objekt nenabúda hodnotu nulovú referenciu, vieme nahradiť referenciou na statickú metódu Objects::nonNull, ktorá je súčasťou štandardnej knižnice Javy. Tým sa nám zápis ešte o niečo skrášli na tvar Validator.<String>rule(Objects::nonNull).

V tejto chvíli by sme sa mohli pýtať, načo nám je dobrý takýto validátor, ktorý vlastne pôvodný problém len sťažuje. A mali by sme pravdu, ak ho ďalej nevylepšíme, je nám zbytočný. V tomto kroku preto aplikujeme návrhový vzor chain of responsibility, ktorý nám umožní spojiť viacero validátorov dokopy.

V princípe musíme implementovať akúsi verziu spájaného zoznamu. To znamená, že každý validátor bude mať referenciu na ďalší validátor. Najprv otestuje vstup na svojom predikáte, a pokiaľ je všetko správne, deleguje validáciu na ďalší validátor v poradí. Poďme sa pozrieť na zmeny v triede Validator:




 










 
 
 

 
 
 
 
 
 
 


public class Validator<T> {

    private final Predicate<T> predicate;
    private final Validator<T> next;

    private Validator(Predicate<T> predicate, Validator<T> next) {
        this.predicate = Objects.requireNonNull(predicate);
        this.next = next;
    }

    public static <T> Validator<T> rule(Predicate<T> validator) {
        return new Validator<>(validator, null);
    }

    public Validator<T> and(Predicate<T> validator) {
        return new Validator<>(validator, this);
    }

    public boolean validate(T element) {
        if (!predicate.test(element)) {
            return false;
        }

        return next == null || next.validate(element);
    }
}

Prvá zmena je, že sme pridali nový nemenný atribút next, ktorý slúži ako ukazovateľ na ďalší spoj v reťazci validátorov.

Druhá zmena je pridanie metódy and, ktorá (podobne ako cons vo funkcionálnom liste) pridá na začiatok zoznamu nový validátor.

Tretia a posledná zmena je prepísanie metódy validate, aby obsahovala logiku delegácie, ktorú sme si už popísali vyššie.

Vylepšený validátor si môžeme vyskúšať na overenie, že reťazec znakov nie je nulový a ani prázdny:






 
 

> Validator<String> notNullNotEmptyValidator = Validator
        .<String>rule(Objects::nonNull)
        .and(string -> !string.isEmpty());
Defined field Validator<String> validator = sk.tuke.fei.kpi.oop.chapters.behavioral.chainOfResponsibility.Validator@3fd9c32e4

> notNullNotEmptyValidator.validate(null);
java.lang.NullPointerException thrown

Ako si však môžeme všimnúť, keď sme definovali pravidlá v poradí 1. nenulový a 2. neprázdny, dostali sme výnimku NullPointerException. To preto, lebo keďže metóda and pripája nový validátor na začiatok reťazca, overenie neprázdnosti bude spustené pred overením nenulovosti.

Upravený validátor, ktorý zachová poradie overení, budete môcť implementovať v rámci úloh. Nateraz urobíme jednoduchú zmenu - len prehodíme poradie jednotlivých validačných krokov a všetko bude fungovať tak, ako má:


 
 











> Validator<String> notNullNotEmptyValidator = Validator
        .<String>rule(string -> !string.isEmpty())
        .and(Objects::nonNull);
Defined field Validator<String> validator = sk.tuke.fei.kpi.oop.chapters.behavioral.chainOfResponsibility.Validator@3fd9c32e4

> notNullNotEmptyValidator.validate(null);
false

> notNullNotEmptyValidator.validate("");
false

> notNullNotEmptyValidator.validate("string");
true

Keď už máme všetko, čo potrebujeme, poďme si pripraviť čiastkové validátory. Overenie nenulovosti a neprázdnosti pre atribúty author, title a content už máme, ostáva nám overenie atribútu slug a categories. Všetky tieto validátory vyzerajú nasledovne:

Validator<String> notNullNotEmptyValidator = Validator
        .<String>rule(object -> !object.isEmpty())
        .and(Objects::nonNull);

Validator<String> slugValidator = Validator
        .<String>rule(slug -> slug.matches("^[a-z0-9-]+$"))
        .and(Objects::nonNull);

Set<String> categories = Set.of("uncategorized", "personal", "food");
Validator<Set<String>> categoriesValidator = Validator
        .<Set<String>>rule(categories::containsAll)
        .and(Objects::nonNull);

V tejto chvíli by sme sa mohli uspokojiť a použiť už tieto implementácie, no tým by sme stále ostali len na podmienkovaní. Riešenie? Ďalší, nadradený validátor! V ňom postupne overíme každý atribút príslušným dcérskym validátorom:

Validator<Article> articleValidator = Validator
        .<Article>rule(a -> notNullNotEmptyValidator.validate(a.getAuthor()))
        .and(a -> notNullNotEmptyValidator.validate(a.getTitle()))
        .and(a -> notNullNotEmptyValidator.validate(a.getContent()))
        .and(a -> slugValidator.validate(a.getSlug()))
        .and(a -> categoriesValidator.validate(a.getCategories()))
        .and(Objects::nonNull);

A to je všetko! Pomocou návrhového vzoru chain of responsibility sme vytvorili modulárny, znovupoužiteľný validátor, ktorý je jednoduchý na údržbu a rozšírenie. Na záver si ukážme overenie jedného ukážkového článku:

> Article article = new Article(
    "John Doe",
    "Example Article",
    "My example article.",
    "example-article",
    Set.of("food"));
Defined field Article article = sk.tuke.fei.kpi.oop.chapters.behavioral.chainOfResponsibility.Article@aa27ca28f

> articleValidator.validate(article);
true

Nechávame už na vás, aby ste si vyskúšali validátor všemožne otestovať.

# Best practices

  • Namiesto nulovej referencie posledného spoja preferujte nulový objekt
  • Ak je to možné, preferujte funkcionálne rozhrania nad triedami

# Úlohy

# 1. Nahraďte nulovú referenciu posledného validátora nulovým objektom

Metóda validate v triede Validator<T> momentálne musí kontrolovať, či ďalší spoj existuje, alebo nie. Keďže nulovú referenciu nemáme radi a snažíme sa jej vyhnúť, vytvorte objekt nulového validátora, ktorý sa bude vkladať v metóde rule ako koniec reťazca, a ktorého metóda validate vždy vráti hodnotu true.

# 2. Prerobte Validátor na funkcionálne rozhranie

V tejto chvíli je náš validátor akási jemná vrstva nad funkcionálnym rozhraním Predicate<T>. Vašou úlohou bude vytvoriť funkcionálne rozhranie FunctionalValidator<T>, pre ktoré bude platiť:

  • Zachová si metódy validate, rule a and (samozrejme, s upravenou implementáciou)
  • Zbaví sa potreby referencie na ďalší validátor v poradí a taktiež aj nulového ukončujucého validátora
  • Metóda and už bude validátory priradzovať na koniec reťazca, nie na začiatok, ako to robí Validator<T>

# 3. Vytvorte skupinu tzv. middleware-ov, ktoré budú spracovávať HTTP požiadavku

Jedna z vlastností nášho validátora, ktorú sme si nehovorili, je to, že logiku návrhového vzoru (teda vytváranie reťaze validátorov a delegovanie na ďalší spoj), tzv. orchestráciu, rieši exkluzívne on. Na celú réžiu teda potrebujete len jednu samostatnú triedu.

Alternatívou je, že jednotlivé spoje by vôbec nevedeli, kde sa nachádzajú, ani čo po nich nasleduje, vedela by to len osobitná časť kódu, ktorá by len celú reťaz orchestrovala, ale naopak by nevedela, čo sa konkrétne v jednotlivých častiach deje, zaujímal by ju len konečný výsledok. Tento spôsob je veľmi populárny napr. v backendovom web developmente v tzv. middlewaroch.

Každá HTTP požiadavka (HTTP request), ktorú server prijíma, sa posiela na príslušný ovládač (controller), ktorý na základe nej vykoná nejakú akciu, resp. vráti nejaké dáta. Výsledok operácie vráti ako HTTP odpoveď (HTTP response). Celý proces vyzerá takto:

Požiadavka -> Ovládač -> Odpoveď

Častokrát sa však stáva, že máme bežnú funkcionalitu, ktorú potrebujeme medzi viacerými ovládačmi zdieľať. Typický príklad je autentikácia, no taktiež aj logovanie. Na to používame tzv. middleware-y - kusy izolovanej logiky, ktorú aplikujeme na prichádzajúcu požiadavku, aby sme ju buď transformovali, alebo vykonali nejakú funkcionalitu. Taktiež, napr. v prípade neúspešnej autentikácie, môžeme reťaz a vykonávanie zastaviť skôr, ako sa požiadavka stihne dostať k ovládaču. Upravený proces vyzerá takto:

Požiadavka -> Middleware-y -> Ovládač -> Odpoveď

Požiadavka -> Middleware-y -> Odpoveď

Poďme sa pozrieť na to, čo už máme pripravené. Začneme triedou Request, ktorá reprezentuje zjednodušenú HTTP požiadavku:

public class Request {

    private Method method;
    private String uri;
    private String body;
    private String locale;
    private Map<String, String> headers;

    public enum Method {
        GET, POST, PUT, DELETE, PATCH,
        HEAD, TRACE, OPTIONS, CONNECT
    }

    // Constructor, getters and setters omitted for brevity
}

Všimnime si niekoľko vecí:

  • Je menná (mutable), aby sme ju vedeli v middlewaroch transformovať
  • Obsahuje nasledovné atribúty:
    • method - HTTP metóda požiadavky, napr. GET
    • uri - cesta k zdroju/akcii na serveri, napr. auth/login
    • body - telo požiadavky, obsahuje dáta (napr. meno a heslo pri prihlásení)
    • locale - aktuálny jazyk, môže slúžiť na prispôsobené správy pre konkrétne krajiny a podobne
    • headers - mapa hlavičiek a ich hodnôt, slúžia ako metadáta požiadavky

Na každú požiadavku server vráti HTTP odpoveď. Pre účely tohto príkladu to nebudeme komplikovať a vytvoríme na to len jednoduchú nemennú (immutable) triedu Response, ktorá bude obsahovať jeden atribút - telo odpovede:

public class Response {

    private final String body;

    public Response(String body) {
        this.body = body;
    }

    public String getBody() {
        return body;
    }
}

Poznámka

Ako bonusovú úlohu by ste mohli triedu odpovede Response rozšíriť ešte aspoň o tzv. HTTP status kód - číslo, ktoré nám jednoznačne povie, aký typ odpovede server posiela, napr. či ide o úspech, presmerovanie, chybu požiadavky alebo chybu servera.

Posledná vec, ktorá nám chýba, je ovládač, teda komponent, ktorý transformuje požiadavku na odpoveď. V našom prípade použijeme len jednoduchý PageController, ktorý v odpovedi vráti jazyk a telo požiadavky:

public class PageController {

    public Response getPage(Request request) {
        String body = "Locale = " + request.getLocale() + "; Body = " + request.getBody();
        return new Response(body);
    }
}

Ostáva nám zadefinovať si rozhranie middleware-u. To bude obsahovať metódu handle, ktorá akceptuje objekt požiadavky a funkciu, ktorá zavolá ďalší spoj v reťazi a vráti odpoveď. Vyzerať bude nasledovne:

public interface Middleware {

    Response handle(Request request, Function<Request, Response> next);
}

Ako by sa mala táto metóda používať? Middleware by mal po zavolaní vykonať svoju logiku, a potom sa rozhodnúť, či chce umožniť reťazcu pokračovať (volaním metódu apply funkcie next s parametrom požiadavky), alebo či vráti svoju vlastnú odpoveď. Pokiaľ sa rozhodne pre prvú možnosť, má na výber, či návratovú hodnotu (odpoveď) vráti okamžite, alebo si ju uloží do lokálnej premennej a urobí po vykonaní zvyšku reťazca tzv. post-processing.

Funkcia next je dodávaná práve orchestrátorom, ktorý v nej rozhodne, čo sa bude diať ďalej vo vykonávaní. Jednotlivým middleware-om je teda jedno, či po nich nasleduje ďalší middleware, alebo už priamo ovládač. Ak vás zaujíma, ako je táto orchestrácia implementovaná, pozrite si na príklade nižšie metódy runWithMiddlwares a nextHandler.

Vaša úloha spočíva vo vytvorení nasledovných middleware-ov:

  • LoggerMiddleware - pomôcka pre developerov, aby v konzole videli, čo a ako rýchlo sa vykonáva
    • Na začiatku vykonávania do konzoly vypíše metódu a URI požiadavky
    • Na konci vykonávania do konzoly vypíše, ako dlho sa požiadavka spracovávala v milisekundách. Nakoľko pôjde o bleskurýchle výpisy, použite na to statickú metódu System.nanoTime().
  • LocaleMiddleware - nastaví hodnotu jazyk požiadavky na taký, aký je uvedený v hlavičke s kľúčom Locale. Ak daná hlavička neexistuje, nech nastaví jazyk na en.
  • TrimMiddleware - zabezpečí, aby boli z tela požiadavky odstránené medzery pred a za textom. Text by tak premenil na Text. Nezabudnite, že telo môže nadobúdať hodnotu null.
  • AuthMiddleware - bude zabezpečovať ovládač pred neautorizovaným vykonaním
    • Pokračovanie reťazcu umožní len v tom prípade, že hlavička obsahuje kľúč Authenticate s hodnotou TKN345
    • Pokiaľ taký kľúč neexistuje, resp. je hodnota zlá, vráti odpoveď s textom Not authenticated!
    • Pokiaľ je požiadavka vykonávaná na URI auth/login, kontrola sa nebude vykonávať a požiadavka sa pošle ďalej v reťazci

Na otestovanie máte k dispozícii nasledujúci príklad s hotovým orchestrátorom. Vami vytvorené middleware-y musíte vložiť do metódy middlewares v správnom poradí. Následne môžete použiť statické metódy example[1-4] v JShell na overenie vašej implementácie - riadťe sa pokynmi v JavaDoc nad jednotlivými príkladmi.










































































































 



































public class MiddlewareExample {

    /**
     * Scenario: Attempt to log in to the system.
     * Expected output (exact time will vary):
     *
     * Request for POST auth/login
     * Request finished. Time 0.118638 milliseconds.
     * Locale = en; Body = user=example&password=password
     */
    public static void example1() {
        Request request = new Request(
            Request.Method.POST,
            "auth/login",
            "user=example&password=password",
            null,
            Collections.emptyMap()
        );

        Response response = runWithMiddlewares(request, middlewares());

        System.out.println(response.getBody());
    }

    /**
     * Scenario: Attempt to access page without being authenticated
     * Expected output (exact time will vary):
     *
     * Request for GET dashboard
     * Request finished. Time 0.035112 milliseconds.
     * Not authenticated!
     */
    public static void example2() {
        Request request = new Request(
            Request.Method.GET,
            "dashboard",
            null,
            null,
            Collections.emptyMap()
        );

        Response response = runWithMiddlewares(request, middlewares());

        System.out.println(response.getBody());
    }

    /**
     * Scenario: Attempt to access page with incorrect authentication token
     * Expected output (exact time will vary):
     *
     * Request for GET dashboard
     * Request finished. Time 0.002819 milliseconds.
     * Not authenticated!
     */
    public static void example3() {
        Map<String, String> headers = new HashMap<String, String>(){
            {
                put("Authenticate", "BD879");
            }
        };

        Request request = new Request(
            Request.Method.GET,
            "dashboard",
            null,
            null,
            headers
        );

        Response response = runWithMiddlewares(request, middlewares());

        System.out.println(response.getBody());
    }

    /**
     * Scenario: Attempt to access page with correct authentication token and custom locale preference
     * Expected output (exact time will vary):
     *
     * Request for GET dashboard
     * Request finished. Time 0.010345 milliseconds.
     * Locale = sk; Body = Body with spaces
     */
    public static void example4() {
        Map<String, String> headers = new HashMap<String, String>(){
            {
                put("Authenticate", "TKN345");
                put("Locale", "sk");
            }
        };

        Request request = new Request(
            Request.Method.GET,
            "dashboard",
            "      Body with spaces       ",
            null,
            headers
        );

        Response response = runWithMiddlewares(request, middlewares());

        System.out.println(response.getBody());
    }

    private static List<Middleware> middlewares() {
        return Arrays.asList(
            // TODO: add your middlewares in the correct order!
        );
    }

    private static Response runWithMiddlewares(Request request, List<Middleware> middlewares) {
        // This is just a preparation for the first call of recursive `nextHandler` method
        Middleware currentMiddleware = middlewares.isEmpty() ? null : middlewares.get(0);
        return nextHandler(request, 0, middlewares, currentMiddleware);
    }

    private static Response nextHandler(Request request, int currentIndex,
                                        List<Middleware> allMiddlewares, Middleware currentMiddleware) {
        // The last middleware should finish by triggering
        // controller instead of next middleware
        if (currentIndex + 1 >= allMiddlewares.size()) {
            return currentMiddleware.handle(
                request,
                currentRequest -> new PageController().getPage(request)
            );
        }

        // If we are in the middle of the chain, then middleware
        // should be able to call the next one in the chain
        int nextIndex = currentIndex + 1;
        Middleware nextMiddleware = allMiddlewares.get(nextIndex);
        return currentMiddleware.handle(
            request,
            // The reason we are accepting different request than the one
            // received from `runWIthMiddlewares` is that we want to allow
            // middlewares to not only change, but also completely replace
            // requests if they feel it's necessary to do so
            currentRequest -> nextHandler(currentRequest, nextIndex, allMiddlewares, nextMiddleware)
        );
    }
}

# Riešenia úloh

Nasledujúce kódy obsahujú riešenia zo všetkých úloh.

# Pozri tiež

# Diskusia