# Factory

V minulej kapitole sme si ukázali, ako zjednodušiť vytváranie komplexných objektov. Namiesto používania dlhých a neprehľadných konštruktorov sme si vytvorili flexibilný builder.

Pri jeho používaní v praxi by sme si ale všimli niekoľko opakujúcich sa situácií, ktoré pri dodržaní DRY princípu musíme vyriešiť. Zistili by sme, že častokrát môžeme vytvárať ten istý objekt, resp. podobné objekty, ktoré sa líšia len v jednom alebo dvoch parametroch. Prirodzene sa preto naskytá otázka, kde a ako túto funkcionalitu zjednotiť. Odpoveďou je jedna z foriem vzoru továreň, alebo anglicky factory.

Poznámka

V rámci tejto kapitoly sme spojili dokopy návrhové vzory factory metóda, abstraktná factory a taktiež aj niekoľko neštandarizovaných variácií oboch vzorov z praxe. Keď budeme rozprávať o všeobecných definíciach, upozorníme na to.

# Modelová situácia

V rámci modelových situácií sa budeme častokrát odkazovať na prácu, ktorú sme ukázali v kapitole o vzore Builder. Preto vám odporúčame si ho predtým zopakovať.

# Factory metóda

Pokiaľ pracujete na väčších systémoch, kde konkrétnu dátovú štruktúru persistentne ukladáte (disk, databáza), ale zároveň aj posielate mimo systému (napr. cez webové služby), je vhodné si vytvoriť 2 osobitné triedy pre každý z prípadov. Triedy pre perzistenciu sa v takomto prípade zvyknú nazývať entity, kým triedy pre komunikáciu s vonkajšími systémami sa zvyknú volať objekty pre prenos dát (angl. DTO - data transfer object). To nám umožňuje robiť zmeny na jednej aj druhej strane nezávisle na sebe.

Aby to však fungovalo, musíme si definovať mapovanie z jednej štruktúry do druhej. Vysvetlíme si to na jednoduchom príklade - blogovacia stránka. Základom blogu sú články - a keďže chceme mať kvalitný kód, rozdelili sme si ho na entitu v databáze a objekt na prenos dát:

public class ArticleEntity {

    private final int id;
    private final boolean published;
    private final String author;
    private final String title;
    private final String content;
    private final LocalDate createdAt;

    // rest omitted for brevity
}
public class ArticleDto {

    private final String id;
    private final String author;
    private final String header;
    private final String body;

    // rest omitted for brevity
}

Poznámka

Vo veľkých systémoch sa zvykne používať ešte jedna vrstva - interná reprezentácia v rámci kódu samotnej aplikácie. Tá už nezvykne mať žiadnu ďalšiu príponu. Preto by sme model článku nazvali len Article.

Všimnime si niekoľko zásadnýh rozdielov oboch tried:

  • Objekt na prenos dát neobsahuje všetky atribúty entity
  • Názvy atribútov obsahujúce rovnaké dáta (title a header, content a body) sú odlišné
  • ID má odlišný dátový typ

V princípe nám to však nerobí problémy - musíme si len definovať vhodné mapovanie. V našom prípade vyzerá nasledovne:

ArticleEntity entity = database.findArticle();

return new ArticleDto(
    Integer.toString(entity.getId()),
    entity.getAuthor(),
    entity.getTitle(),
    entity.getContent()
);

Čo nás ale trápiť bude, je to, že tento kód budeme mať na veľa miestach. Okrem zbytočne sa opakujúcich blokov to zároveň znamená problém v prípade, ak sa toto mapovanie akokoľvek zmení. Preto musíme nájsť vhodné riešenie - statická factory metóda:



 









public class ArticleDto {

    public static ArticleDto fromEntity(ArticleEntity entity) {
        return new ArticleDto(
            Integer.toString(entity.getId()),
            entity.getAuthor(),
            entity.getTitle(),
            entity.getContent()
        );
    }
}

Príklad vyššie by sa nám tak skrátil na:

ArticleEntity entity = database.findArticle();

return ArticleDto.fromEntity(entity);

Okrem značnej redukcie kódu sa však táto metóda dá veľmi pekne využiť aj so Stream API. Predstavme si, že z databázy by sme načítali nie jeden článok, ale celý zoznam, ktorý by sme následne mali namapovať do objektov na prenos dát. Vďaka našej statickej factory metóde by to mohlo vyzerať napríklad takto:




 


List<ArticleEntity> entities = database.findAllArticles();

return entities.stream()
        .map(ArticleDto::fromEntity)
        .collect(toList()); // Imported statically from `java.util.stream.Collectors`

Prípadov, kedy sa podobné transformačné funkcie oplatí použiť, je viacero. No nie je to jediné využitie pre factory metódy. Ďalší príklad, dokonca zo štandardnej knižnice, sú statické factory metódy v triedach ako napr. Collectors. Ukážku sme videli už vyššie: Collectors.toList() nám pri každom volaní vytvorí nový Collector, ktorý bude použitý na transformáciu streamu do zoznamu. Keďže všetky kolektory do zoznamu budú rovnaké, nie je nutné, aby toList malo akýkoľvek parameter. Niečo podobné si môžeme vytvoriť aj pre triedu článku - mohli by sme napr. zobrazovať novým používateľom ukážkový článok, s ktorým by mohli začať. Tvorbu takéhoto článku by v takom prípade mala na starosti factory metóda example:

public class ArticleEntity {

    public static ArticleEntity example() {
        return new ArticleEntity(
            -1,
            false,
            "John Doe",
            "Article title",
            "This is the content of the article",
            LocalDate.now()
        );
    }
}

# Factory trieda

Vráťme sa k téme z minulej kapitoly - k autám. Ústrednou "postavou" v nej bol rodinný van Ford S-Max. Podobne, ako v prípade mapovania dvoch tried z predchádzajúceho príkladu, aj teraz by sme mohli uvažovať o tom, že by sme toto vytváranie niekam presunuli. Dať to do triedy Car by sa mohlo na prvý pohľad zdať ako dobrý nápad, avšak pokiaľ by sme začali pridávať ďalšie a ďalšie autá, veľmi rýchlo by sme prišli na to, že takto to nepôjde. Preto si vytvoríme osobitnú triedu nielen pre jedno auto, ale viacero modelových radov značky Ford. Nazveme ju FordFactory:

public final class FordFactory {

    private FordFactory() {
        // Private constructor to make this class static
    }
}

Všimnime si niekoľko vecí:

  • Trieda je označená ako final - nedá sa z nej dediť
  • Jediný konštruktor je prázdny a private - nie je možné klasicky vytvoriť inde v kóde inštanciu tejto triedy

Tieto vlastnosti nám zabezpečujú, že trieda bude len statická, nakoľko nepotrebujeme objekt na to, aby sme mali prístup k jednotlivým metódam. V tejto chvíli pokračujme vytvorením factory metódy pre model S-Max:

public final class FordFactory {

    public static Car sMax() {
        return new Car.Builder()
            .brand("Ford")
            .model("S-Max")
            .yearOfProduction(2019)
            .dieselFuel()
            .automaticGear()
            .sixGears()
            .fuelConsumption(5.8f)
            .price(39500)
            .build();
    }

    // Rest omitted for brevity
}

Ako sme spomenuli, v tejto triede by sme chceli pridať funkcionalitu na vytváranie aj iných modelov značky Ford, preto do tejto "továrne" zahrnieme aj rady Kuga a Fiesta:

public final class FordFactory {

    public static Car kugaAnniversary() {
        return new Car.Builder()
            .brand("Ford")
            .model("Kuga Anniversary")
            .yearOfProduction(2019)
            .gasolineFuel()
            .manualGear()
            .sixGears()
            .fuelConsumption(6.3f)
            .price(24580)
            .build();
    }

    public static Car fiesta() {
        return new Car.Builder()
            .brand("Ford")
            .model("Fiesta")
            .yearOfProduction(2019)
            .gasolineFuel()
            .manualGear()
            .sixGears()
            .fuelConsumption(5.5f)
            .price(13940)
            .build();
    }

    // Rest omitted for brevity
}

Akýkoľvek ďalší model, ktorý by sme chceli pridať, by bola ďalšia factory metóda v tejto triede. Ak by sme chceli factory metódu pre inú značku ako Ford (napr. Škoda), vytvorili by sme si novú separátnu triedu (napr. SkodaFactory).

# Abstrakcia vo factory

Ukážky vyššie sú špecifické prípady návrhových vzorov factory metóda a abstraktná factory. Teraz sa pozrieme na tieto vzory tak, ako boli definované v knihe Design Patterns: Elements of Reusable Object-Oriented Software v knihe od Erich Gamma, Richard Helm, Ralph Johnson a John Vlissides (všeobecne známy ako Gang of Four). Ich spoločná vlastnosť je abstrakcia - tieto "továrne" už nie sú statické, ale pracujeme s rôznymi implementáciami abstraktných rozhraní alebo metód.

# Factory metóda

Druhá modelová situácia, ktorú sme v minulej kapitole riešili, bola o vytvorení builderov pre Fibonacciho postupnosť. Keď sme vytvorili 3 rôzne implementácie pre rozhranie FibonacciBuilder, zistili sme, že takmer všetka logika je pri nich rovnaká, rozdiel je len v spôsobe výpočtu n-tého čísla. Preto sme si vytvorili ešte jednu úroveň abstrakcie v podobe triedy AbstractFibonacciBuilder. Pre pripomenutie, tu je ukážka:



 




public abstract class AbstractFibonacciBuilder implements FibonacciBuilder {

    protected abstract long compute(int index);

    // rest omitted for brevity - check Builder chapter
}

Aj keď sa to možno na prvý pohľad nezdá, metóda compute je práve (abstraktná) factory metóda - je to rozhranie na vytváranie objektu (v tomto prípade len čísla), pričom spôsob, ako a aký konkrétny objekt metóda vráti, je delegovaný na implementujúcu triedu.

# Abstraktná factory

Ako sme spomínali pri našej statickej factory triede, ak by sme chceli "továreň" pre inú značku, museli by sme vytvoriť novú triedu s jej špecifickými modelovými radmi. Tento spôsob by však narazil na limitácie, ak by sme potrebovali zjednotiť funkcionalitu medzi jednotlivými automobilkami. Čo ak by sme napríklad chceli vytvoriť metódu, ktorá nám vráti SUV danej značky? Pre značky Ford a Škoda by mohla vyzerať nasledovne:

public Car suv(String brand) {
    if ("Ford".equalsIgnoreCase(brand)) {
        return FordFactory.kugaAnniversary();
    } else if ("Škoda".equalsIgnoreCase(brand)) {
        return SkodaFactory.kodiaq();
    }

    throw new IllegalArgumentException("Unknown brand: " + brand);
}

Ako si môžeme všimnúť, toto riešenie nie je vôbec škálovateľné a rýchlo by sa stalo neudržateľné s pribúdajúcimi výrobcami a typmi áut. Preto sa musíme odkloniť od statických metód a prejsť na objekty - definovať si jednotné rozhranie pre všetkých výrobcov áut - CarFactory:

public interface CarFactory {

    Car suv();

    Car crossover();

    Car sedan();

    Car hatchback();

    Car coupe();

    Car minivan();

    Car van();
}

Aj keď nás táto implementácia obmedzí v tom, že si musíme vybrať práve jednu modelovú radu pre konkrétny druh auta, ako protihodnotu dostaneme možnosť písať všeobecný kód bez toho, aby sme vedeli, o akú automobilku sa jedná.

Implementácia tohto rozhrania pre automobilku Škoda môže vyzerať nasledovne:

































 





public class SkodaFactory implements CarFactory {

    @Override
    public Car suv() {
        return new Car.Builder()
            .brand("Škoda")
            .model("Kodiaq")
            .yearOfProduction(2019)
            .dieselFuel()
            .manualGear()
            .sixGears()
            .fuelConsumption(6.0f)
            .price(34000)
            .build();
    }

    @Override
    public Car sedan() {
        return new Car.Builder()
            .brand("Škoda")
            .model("Octavia")
            .yearOfProduction(2019)
            .dieselFuel()
            .manualGear()
            .fiveGears()
            .fuelConsumption(4.5f)
            .price(22570)
            .build();
    }

    @Override
    public Car crossover() {
        throw new UnsupportedOperationException("Factory does not support making of crossover cars.");
    }

    // rest of the interface is implemented as the method `crossover`
}

Všimnite si zvýraznený riadok. Keďže máme všeobecné rozhranie pre automobilky, môže sa stať, že niektorá nebude ponúkať daný typ auta, resp. nie v našej ponuke. V takom prípade môžeme volajúcemu kódu túto skutočnosť signalizovať výnimkou UnsupportedOperationException, aby bolo jasné, že "továreň" dané automobily nevyrába.

# Využitie v praxi

Na prvý pohľad sa môže zdať, že takáto abstrakcia je prehnaná a len zbytočne dvíha komplexitu kódu. Opak je však pravdou - pri správnom použití ňou dokážeme celú logiku zjednodušiť.

Pri factory metóde na n-té Fibonacciho číslo sme dokázali odizolovať "omáčky" naokolo (validácia dát, zápis do listu) od samotného algoritmu a mohli sme tak jednoducho implementovať 3 rôzne spôsoby.

Abstraktná factory nám naopak vyriešila problém, kedy by sme museli pre rôzny druh značky spúšťať inú časť kódu. Ak by sme potrebovali metódu na predaj auta typu SUV, vyzerala by nejako takto:

public Car buySuv(CarFactory manufacturer) {
    Car suv = manufacturer.suv();

    // any necessary processing
    
    return suv;
}

Ako vidíme, logika samotnej metódy sa vôbec nestará o to, aké konkrétne auto spracúvava - potrebuje len vedieť, ktorý výrobca auto dodá a zvyšok je už pre všetkých rovnaký. Takúto metódu by sme volali nasledovne:

CarFactory skodaFactory = new SkodaFactory();
buySuv(skodaFactory);

Všeobecne však platí, že pri každom probléme treba zvážiť, ktorý spôsob použitia vzoru factory je najlepší. Ak sa aj neskôr ukáže, že rozhodnutie bolo zlé alebo nedostatočné, netreba sa báť refactoringu a prepísať starší kód tak, aby lepšie vyhovoval novej situácii.

# Best practices

  • Factory sa dobre kombinuje so vzorom Prototyp (na ktorý sa pozrieme v ďalšej kapitole)
  • Metódy sa môžu volať, resp. začínať na make, poprípade provide, aby ich účel bol jasný

# Úlohy

# 1. Upravte FordFactory a SkodaFactory tak, aby dodržiavali DRY princíp

Ak ste si už vycibrili zmysel pre identifikovanie problémov s DRY princípom, určite ste postrehli, že v "továrňach" FordFactory a SkodaFactory sa nám kus kódu opakuje pre každú factory metódu. Identifikujte tento kód a vhodne ho zovšeobecnite.

# 2. Vytvorte factory, ktorá premení kód auta na jeho inštanciu

Ak by sme mali automobilovú predajňu s viacerými autami, určite by sme ich museli mať všetky v systéme uložené pod unikátnym kódom. Mohli by vyzerať napr. nasledovne:

  • SKD-19-OCTV-1.6D - Škoda Octavia
  • FRD-19-SMX-2D - Ford S-Max

Vytvorte factory triedu, ktorá na základe kódu vráti správny model auta. Je úplne na vás, ako triedu implementujete, no musíte dodržať 3 pravidlá:

  • Nesmiete použiť if-else rebrík ani switch - vyhľadávanie áut musí byť dynamické a jednoduché na údržbu a pridávanie nových modelov
  • Vždy vráťte novú inštanciu modelu (nevracajte vždy tú istú pre ten istý model)
  • Využite už existujúce "továrne", ktoré sme si v rámci kapitoly pripravili

Samotné kódy si vymyslite, aké uznáte za vhodné - nie sú pre účel tejto úlohy pdostatné.

Poznámka

Nezabudnite ošetriť prípad, kedy poskytnutý kód nie je platný (je null, alebo pre daný kód neprislúcha žiadne auto). Ako to urobíte, je už na vás.

# Riešenia úloh

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

# Pozri tiež

# Diskusia