# Stratégia

V minulej kapitole sme hovorilil, že občas máme problém s algoritmami alebo procesmi, ktoré sú skoro na vlas podobné, ale líšia sa len v pár veciach. Template metóda bola jedným z robustnejších riešení, ktoré máme k dispozícii.

Čo však v prípade, ak potrebujeme špecifický kód nielen oddeliť od všeobecnej implementácie, ale mať ho po ruke, aby mohol byť použiteľný znova v inej situácii? Taký čas aplikujeme princíp composition over inheritance, teda kompozíciu nad dedením, a namiesto vytvárania podtried rozdelíme problém na niekoľko izolovaných častí. Komponent, ktorý bude vykonávať špecifickú funkcionalitu v abstraktom algoritme, budeme volať Stratégia.

# Modelová situácia

Ukážku návrhového vzoru sme rozdelili na 2 časti - jednoduchšiu behaviorálnu parametrizáciu a komplexnejšiu a plnohodnotnú stratégiu.

# Behaviorálna parametrizácia

V tejto časti bude našou úlohou triediť jablká. Každé jablko má svoju váhu, odrodu a kvalitu. Trieda reprezentujúca túto dátovú štruktúru môže vyzerať nasledovne:

public class Apple {

    private final int weight;
    private final Cultivar cultivar;
    private final QualityClass qualityClass;

    public Apple(int weight, Cultivar cultivar, QualityClass qualityClass) {
        this.weight = weight;
        this.cultivar = Objects.requireNonNull(cultivar, "Cultivar cannot be null!");
        this.qualityClass = Objects.requireNonNull(qualityClass, "Quality class cannot be null!");
    }

    public int getWeight() {
        return weight;
    }

    public Cultivar getCultivar() {
        return cultivar;
    }

    public QualityClass getQualityClass() {
        return qualityClass;
    }

    @Override
    public String toString() {
        return "Apple("
                + weight + "g, "
                + cultivar + " cultivar, "
                + qualityClass + " quality)";
    }

    /**
     * Selected cultivars from https://en.wikipedia.org/wiki/Apple#Cultivars
     */
    public enum Cultivar {
        ALICE,
        AMBROSIA,
        GOLDEN_DELICIOUS,
        GRANNY_SMITH,
        MCINTOSH,
    }

    /**
     * Qualities based on https://www.applesfromeurope.eu/for-professionals/commercial-quality-of-apples
     */
    public enum QualityClass {
        EXTRA,
        I,
        II
    }
}

Predstavme si, že by sme zo státisíc jabĺk, ktoré máme na sklade, mali pre klienta vybrať len tie, ktoré sú ťažšie ako určitá hmotnosť. Pre nás je to triviálna úloha, preto si napíšeme pomocnú statickú metódu, ktorá nám zo zoznamu jabĺk a hmotnosti vráti nový zoznam, ktorý bude obsahovať len jablká ťažšie ako daná hmotnosť. Nízkoúrovňová implementácia vyzerá nasledovne:





 
 
 





public static List<Apple> filterHeavierThan(List<Apple> apples, int weight) {
    List<Apple> filtered = new ArrayList<>();

    for (Apple apple : apples) {
        if (apple.getWeight() > weight) {
            filtered.add(apple);
        }
    }

    return filtered;
}

Všimnime si zvýraznené riadky. Tie sú jediné, ktoré nás zaujímajú, ktoré popisujú logiku metódy. Všetko ostatné (dokonca čiastočne aj riadok s pridaním do zoznamu) je len šum zakrývajúci podstatu metódy. Tento problém bol v Jave dlhodobo známy a preto (aj po vzore iných jazykov) boli vo verzii 8 pridané Stream API. Vďaka nim môžeme tú istú logiku zapísať funkcionálnym štýlom, kde len zapíšeme, ako z pôvododného listu vytvoríme ten náš. Kód sa nám zmení na niečo nasledovné:



 



public static List<Apple> filterHeavierThan(List<Apple> apples, int weight) {
    return apples.stream()
            .filter(apple -> apple.getWeight() > weight)
            .collect(Collectors.toList());
}

Klienti však budú chcieť, aby sme vyberali nielen podľa hmotnosti, ale aj podľa kvality a odrody jabĺk. Preto po vzore filterHeavierThan vytvoríme ďalšie dva statické filtrovacie metódy:



 





 



public static List<Apple> filterQuality(List<Apple> apples, Apple.QualityClass quality) {
    return apples.stream()
            .filter(apple -> apple.getQualityClass() == quality)
            .collect(Collectors.toList());
}

public static List<Apple> filterCultivar(List<Apple> apples, Apple.Cultivar cultivar) {
    return apples.stream()
            .filter(apple -> apple.getCultivar() == cultivar)
            .collect(Collectors.toList());
}

Opäť sa pozrime na zvýraznený kód a uvidíme, že len ten sa líši - aj keď sme vytváranie listov deleguje na Stream API, stále máme veĺa podobných krokov. Všetky tri metódy sa líšia len v podmienke. Vieme s tým niečo robiť? Začnime tým, že sa pozrieme, aký je typ argumentu filtrovacej metódy filter. Zistíme, že je to java.util.function.Predicate<T> - tzv. predikát, čo je funkcia, ktorá mapuje argument typu T na boolovské hodnoty true alebo false. Napr. predikát apple -> apple.getWeight() > weight, kde weight by bol 50, by jablko s hmotnosťou 30g namapoval na false a jablko s hmotnosťou 60g na true. Funkcia filter len interne povie, že do nového zoznamu chce práve tie jablká z pôvodného zoznamu, ktoré sú namapované na hodnotu true. Trieda Predicate<T> je však verejná trieda a preto ju môžeme použiť aj vo vlastnom kóde. To nám umožní vytvoriť všeobecnú statickú metódu filter, ktorá bude vyzerať nasledovne:

public static List<Apple> filter(List<Apple> apple, Predicate<Apple> condition) {
    return apple.stream()
            .filter(condition)
            .collect(Collectors.toList());
}

To nám umožní volať filtrovaciu metódu s akoukoľvek podmienkou. Špecifické metódy, ktoré sme už nakódili, tak môžeme značne zjednodušiť:

public static List<Apple> filterHeavierThan(List<Apple> apples, int weight) {
    return filter(apples, apple -> apple.getWeight() > weight);
}

public static List<Apple> filterQuality(List<Apple> apples, Apple.QualityClass quality) {
    return filter(apples, apple -> apple.getQualityClass() == quality);
}

public static List<Apple> filterCultivar(List<Apple> apples, Apple.Cultivar cultivar) {
    return filter(apples, apple -> apple.getCultivar() == cultivar);
}

To, že do metódy filter (či už našej, alebo tej zo Stream API) môžeme priamo vložiť logiku, ktorá špecifikuje časť funkcionality danej metódy, voláme behaviorálna parametrizácia.

# Stratégia

Dnešné moderné továrne sa stavajú tak, aby v prípade potreby dokázali vyprodukovať po zmenení konfigurácie iné alebo upravené predmety plynulo bez zastavenia výroby. Dá sa povedať, že samotné linky sú postavené všeobecne a meníme len stratégiu výroby.

My si môžeme na chvíľu predstaviť, že sme v budúcnosti a dokážeme v továrni vyrábať aj jablká. Začnime tým, že si vytvoríme vnorenú triedu Generator v materskej AppleFactory:

public class AppleFactory {

    public interface Generator {

        int generateWeight();

        Apple.Cultivar generateCultivar();

        Apple.QualityClass generateQualityClass();

        boolean isValid(Apple apple);
    }
}

Náš generátor má 3 metódy, ktoré vygenerujú vlastnosti jabĺk. Metóda isValid slúši na potvrdenie, či je vytvorené jablko validné (napr. jablko vážiace 10 gramov nemôže byť triedy extra). Každá továreň potrebuje generátor jabĺk na správne fungovanie, preto ho predáme ako argument v konštruktore:

public class AppleFactory {

    private final Generator generator;

    public AppleFactory(Generator generator) {
        this.generator = generator;
    }

    // rest omitted for brevity
}

AppleFactory je napísaná, ako názov napovedá, podľa vzoru Factory, preto aby mala zmysel, potrebujeme metódu make, ktorá vytvorí nové jablko. Interne budeme generovať jablko až dovtedy, kým nebude validné. To vieme zapísať nasledovne:

public Apple make() {
    Apple apple;

    do {
        apple = new Apple(
                generator.generateWeight(),
                generator.generateCultivar(),
                generator.generateQualityClass()
        );
    } while (!generator.isValid(apple));

    return apple;
}

Ostáva nám už len definovať niekoľko generátorov. Môžeme začať s nainvným náhodným generátorom:

public class RandomAppleGenerator implements AppleFactory.Generator {

    private final Random random = new Random();

    @Override
    public int generateWeight() {
        return random.nextInt(191) + 50;
    }

    @Override
    public Apple.Cultivar generateCultivar() {
        return Apple.Cultivar.values()[random.nextInt(Apple.Cultivar.values().length)];
    }

    @Override
    public Apple.QualityClass generateQualityClass() {
        return Apple.QualityClass.values()[random.nextInt(Apple.QualityClass.values().length)];
    }

    @Override
    public boolean isValid(Apple apple) {
        return true;
    }
}

Všetky atribúty sú náhodne generované a metóda isValid automaticky schváli každé jedno vytvorené jablko. To pre nás nie je až tak veľmi zaujímavé, preto si vytvoríme pseudo-náhodnú verziu, ktorá sa bude líšiť len vo validácii. Na ukážku zabránime, aby jablká extra kvality mali pod 50 gramov, resp. aby boli odrody McIntosh:

public class PseudoRandomAppleGenerator implements AppleFactory.Generator {

    @Override
    public boolean isValid(Apple apple) {
        if (apple.getWeight() < 50 && apple.getQualityClass() == Apple.QualityClass.EXTRA) {
            return false;
        }

        if (apple.getCultivar() == Apple.Cultivar.MCINTOSH && apple.getQualityClass() == Apple.QualityClass.EXTRA) {
            return false;
        }

        return true;
    }

    // rest is same as in `RandomAppleGenerator`
}

V tejto chvíli už vieme vytvárať plnohodnotné inštancie tovární:

AppleFactory randomFactory = new AppleFactory(new RandomAppleGenerator());
AppleFactory pseudoRandomFactory = new AppleFactory(new PseudoRandomAppleGenerator());

# Best practices

  • Pri behaviorálnej parametrizácii sa snažte naplno využiť zabudované Java API (balík java.util.function) namiesto vytvárania vlastných funkčných rozhraní
  • Preferujte použitie Stratégie nad Template metódou

# Úlohy

# 1. Zlepšite syntax filtrovania jabĺk

Pozrime sa na to, ako by sme v praxi používali už zabudované filtrovacie metódy pre váhu, kvalitu a odrodu:








 


 

// Given pre-filled list
List<Apple> apples = loadApples();

// Filter by weight
List<Apple> heavy = filterHeavierThan(apples, 50);

// Filter by weight and quality
List<Apple> heavyHighQuality = filterHeavierThan(filterQuality(apples, Apple.QualityClass.EXTRA), 50);

// Filter by weight or quality
List<Apple> heavyOrHighQuality = filter(apples, apple -> apple.getWeight() > 50 || apple.getQualityClass() == Apple.QualityClass.EXTRA);

Vidíme, že pri komplikovanejších podmienkach sú naše metódy veľmi neflexibilné, resp. nedostačujúce. Čo však, ak by sme vedeli to isté zapísať nasledovne?








 


 

// Given pre-filled list
List<Apple> apples = loadApples();

// Filter by weight
List<Apple> heavy = filter(apples, heavierThan(50));

// Filter by weight and quality
List<Apple> heavyHighQuality = filter(apples, heavierThan(50).and(quality(Apple.QualityClass.EXTRA)));

// Filter by weight or quality
List<Apple> heavyOrHighQuality = filter(apples, heavierThan(50).or(quality(Apple.QualityClass.EXTRA)));

Takéto niečo môžeme dokázať, nakoľko funkcionálne rozhranie Predicate má v sebe niekoľko predvolených metód, ako napr. and a or, ktoré skladajú predikáty do logických podmienok. Vašou úlohou bude vytvoriť statické factory metódy heavierThan, quality a cultivar, ktoré vrátia príslušný Predicate<Apple> taký, aby ste s nimi vedeli zapísať príklad vyššie pomocou týchto predvolených pomocných metód.

Poznámka

Technika, keď metóda vráti ďalšiu funkciu ako návratovú hodnotu, voláme currying, a nájdeme ju najmä vo funkcionálne orientovaných jazykoch.

# 2. Prerobte metódu AppleFactory.make tak, aby bola funkcionálna

Celú knihu kladieme dôraz na funkcionálne princípy, nakoľko tento štýl je zvyčajne čitateľnejší a lepší na údržbu. Metóda make triedy AppleFactory, ktorú sme ukázali, je však ukážkový príklad na imperatívny kód. Aj keď je krátka a zrozumiteľná, funkcionálny štýl by ju stále mohol zlepšiť.

Pozrite si statickú metódu Stream.generate a pokúste sa s použitím Stream API pretvoriť aktuálny imperatívny kód metódy make na funkcionálny.

# 3. Prerobte Logger z minulej kapitoly za použitia Stratégie

V minulej kapitole sme si ukázali, ako sa dá napísať logovací systém chýb v aplikácii s rôznym spôsobom uloženia správ za použitia vzoru Template metóda. Teraz bude vašou úlohou vytvoriť rovnaký systém, avšak za použitia vzoru Stratégia.

V podstate máte všetok kód hotový, len ho musíte vhodne presunúť do nových tried. Začnite tým, že si definujete rozhranie pre zapisovateľa, ktorý bude obsahovať metódu write podobne, ako to bolo v prípade AbstractLogger. Tohto zapisovateľa potom budete predávať ako argument konštruktoru samotného loggera.

# Riešenia úloh

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

# Pozri tiež

# Diskusia