# Builder

Veľká časť objektov, ktoré vytvárame, vyžaduje len niekoľko jednoduchých parametrov, aby mohli byť skonštruované. Občas sa však môže stať, že týchto parametrov je veľa, ich samotná príprava je zložitá či zdĺhavá, alebo samotný objekt potrebujeme vytvoriť po častiach. V týchto situáciach sa nám výborne hodí návrzhový vzor "staviteľ" (angl. builder).

Princíp fungovania tohto návrhového vzoru je veľmi jednoduchý - vytvoríme si pomocnú triedu, ktorou jedinou úlohou bude postupne zbierať čiastkové dáta, pričom z nich po zavolaní príslušnej vytvárajúcej metódy vytvorí inštanciu materskej triedy. Táto trieda môže rovnako v sebe skrývať aj komplexné spracovanie vstupných údajov, či ich validáciu pre zaručenie integrity dát. Viac si ukážeme na modelovej situácii.

# Modelová situácia

Predstavme si, že máme triedu Car. Táto trieda má byť nemenná (immutable) - a teda, ako sme sa už skôr naučili, všetky jej parametre musíme zadať už pri vytváraní a potom ich už nemeniť. Od zákazníka sme dostali nasledujúcu špecifikáciu údajov, ktoré auto musí mať:

Údaj Dátový typ Podmienka Predvolená hodnota
Značka String Musí byť zadaná Nie je
Model String Musí byť zadaný Nie je
Rok výroby Integer > 1900; <= súčasný rok Súčasný rok
Výkon (kW) Float >= 0 0
Výkon (kone) Float >= 0; 0.746 násobok výkonu v kW 0
Objem motora (cm^3) Integer >= 0 0
Typ paliva Enumerable Benzín, nafta, LPG, elektrina Benzín
Prevodovka (typ) Enumerable Manuálna alebo Automatická Manuálna
Prevodovka (stupne) Integer > 0 5
Spotreba na 100 km Float Musí byť zadaná; > 0 Nie je
Cena v centoch Integer Musí byť zadaná; >= 0 Nie je

Poznámka

Výkon by sme za normálnych okolností držali v 1 dátovej štruktúre, pre účeli demonštrácie sa nám však hodí tieto hodnoty oddeliť.

Aby sme splnili podmienku nemennosti údajov, musia byť všetky definované ako final a teda musia byť súčasťou konštruktora:

public class Car {

    private final String brand;
    private final String model;
    private final int yearOfProduction;
    private final float powerKW;
    private final float powerHP;
    private final int engineDisplacement;
    private final FuelType fuelType;
    private final GearType gearType;
    private final int gears;
    private final float fuelConsumption;
    private final int price;

    public Car(String brand,
               String model,
               int yearOfProduction,
               float powerKW,
               float powerHP,
               int engineDisplacement,
               FuelType fuelType,
               GearType gearType,
               int gears,
               float fuelConsumption,
               int price) {
        this.brand = brand;
        this.model = model;
        this.yearOfProduction = yearOfProduction;
        this.powerKW = powerKW;
        this.powerHP = powerHP;
        this.engineDisplacement = engineDisplacement;
        this.fuelType = fuelType;
        this.gearType = gearType;
        this.gears = gears;
        this.fuelConsumption = fuelConsumption;
        this.price = price;
    }

    // Getters omitted for brevity

    public enum FuelType {
        GASOLINE,
        DIESEL,
        LPG,
        ELECTRICITY
    }

    public enum GearType {
        MANUAL,
        AUTOMATIC
    }
}

S toľkými parametrami, navyše mnohými nepovinnými, by bolo maximálne nepraktické vytvárať objekt priamo cez konštruktor - nehovoriac o neprehľadnosti a komplikovanosti prípadných úprav. Posúďte sami:

Car car = new Car("Ford", "S-Max", 2019, 0, 0, 1997, null, null, 6, 5.8f, 39500);

Našťastnie, na takéto situácie existuje návrhový vzor - túto funkcionalitu prenecháme novej vnorenej triede s názvom Builder:

public class Car {

    private Car(...) {
        ...
    }

    // Rest of the Car class omitted for brevity

    public static class Builder {

        private String brand = null;
        private String model = null;
        private int yearOfProduction = Year.now().getValue();
        private float powerKW = 0;
        private float powerHP = 0;
        private int engineDisplacement = 0;
        private FuelType fuelType = FuelType.GASOLINE;
        private GearType gearType = GearType.MANUAL;
        private int gears = 5;
        private float fuelConsumption = Float.NaN;
        private int price = Integer.MIN_VALUE;

        public Builder() {
            //
        }

        public Builder brand(String brand) {
            this.brand = brand;
            return this;
        }

        // Rest of the setters omitted for brevity
        // They have equivalent signature as `brand()`
    }
}

Všimnime si niekoľko dôležitých vecí:

  • Konštruktor triedy Car bol zmenený na private, čo vynucuje použitie builder triedy pre vytvorenie inštancie
  • Trieda Builder je menná (mutable) - jej atribúty vieme podľa potreby menit viackrát
  • Setter-y vracajú aktuálnu inštanciu builder-a, čo umožňuje reťazenie volania metód
  • Setter-y nezačínajú prefixom set
  • Builder dovoľuje hodnoty len nastaviť, nie ich získať (getter-y sú vynechané)
  • Predvolené hodnoty sú automaticky nastavené
  • Pre údaje, ktoré nemajú predvolené hodnoty, sme nastavili neplatné hodnoty null, Float.NaN (not a number - neplatné čislo) a Integer.MIN_VALUE

Ako však z týchto nazbieraných dát vytvoríme inštanciu triedy Car? Budeme potrebovať jednu výstupnú kreačnú metódu, kde skryjeme jej komplexný konštruktor - build:

public Car build() {
    return new Car(brand, model, yearOfProduction, powerKW, powerHP,
                engineDisplacement, fuelType, gearType, gears,
                fuelConsumption, price);
}

V tejto chvíli máme všetko pripravené a môžeme si vzor builder ukázať v praxi:

Car car = new Car.Builder()
        .brand("Ford")
        .model("S-Max")
        .yearOfProduction(2019)
        .fuelType(Car.FuelType.DIESEL)
        .gearType(Car.GearType.AUTOMATIC)
        .gears(6)
        .fuelConsumption(5.8f)
        .price(39500)
        .build();

Problémom však je, že stále nekontrolujeme, či všetky údaje spĺňajú zadané podmienky. Preto napríklad tento kód:

Car car = new Car.Builder().build();

nám vytvorí triedu s chybnými dátami! Musíme sa uistiť, že trieda Builder takéto niečo nedovolí a v prípade porušenia vyhodí chybu. Vytvorme teda jednoduchú validačnú metódu v Builder triede:


 











public Builder brand(String brand) {
    validateBrand(brand);
    this.brand = brand;
    return this;
}

private void validateBrand(String brand) {
    Objects.requireNonNull(brand, "Brand cannot be null!");
    if (brand.trim().isEmpty()) {
        throw new IllegalArgumentException("Brand cannot be empty!");
    }
}

Podľa našej tabuľky najprv kontrolujeme, či reťazec zo značkou auto nie je null. Používame pri tom pomocnú statickú metódu Objects.requireNonNull, ktorá je súčasťou štandardnej Java knižnice. Ak je značka null, metóda vyhodí výnimku NullPointerException s nami špecifikovanou správou (ktorú môžeme vynechať). V druhom prípade manuálne kontrolujeme, či je reťazec so značkou prázdny. Metóda trim() nám najprv odstráni zo začiatku a konca reťazca "biele znaky (whitespace characters)", pričom následne isEmpty() skontroluje dĺžku osekaného reťazca. Ak je táto dĺžka 0, manuálne vyhodíme výnimku IllegalArgumentException, ktorá signalizuje vývojárovi, že daný vstup nie je validný a nemôžeme ho prijať.

Ak teraz skúsime nasledujúci kód:

Car car = new Car.Builder().brand("   ").build();

Dostaneme chybu typu IllegalArgumentException so správou Brand cannot be empty!. Ak však skúsime vytvoriť inštanciu Car len s predvolenými hodnotami:

Car car = new Car.Builder().build();

Všetko prebehne v poriadku. Je to preto, lebo pre povinné hodnoty sa môže stať (ako v tomto prípade), že nikdy nebudú nastavené a teda ani zvalidované. Preto tieto údaje musíme skontrolovať ešte raz, a to pred vytvorením inštancie Car v metóde build:


 






public Car build() {
    validateBrand(brand);

    return new Car(brand, model, yearOfProduction, powerKW, powerHP,
                engineDisplacement, fuelType, gearType, gears,
                fuelConsumption, price);
}

V tejto chvíli už predchádzajúca ukážka zlyhá s chybou NullPointerException a správou Brand cannot be null!. Výborne, značku máme ošetrenú a vieme zvyšku programu garantovať, že po vytvorení bude obsahovať len správne hodnoty. V rámci úloh si podobné overenie dokončíme aj pre ostatné údaje.

Poznámka

Niektoré pokročilé funkcie jazykov (reflexia, serializácia/deserializácia) dokážu náš kód obísť a vytvoriť inštanciu triedy Car s nevalidnými dátami. Týmito spôsobmi sa však v tejto kapitole nebudeme zaoberať a budeme predpokladať, že vývojári môžu pracovať len s naším builder-om.

Na záver si môžeme ukázať ešte jedno vylepšenie. Pozrime sa na kód pomocnej statickej metódy Objects.requireNonNull:




 


public static <T> T requireNonNull(T obj, String message) {
    if (obj == null)
        throw new NullPointerException(message);
    return obj;
}

Ako vidíme, táto metóda nielen objekt zvaliduje, ale ho v prípade úspechu aj vráti. To nám umožňuje písať kratšie konštrukcie, napríklad namiesto:

Objects.requireNonNull(brand);
this.brand = brand;

stačí napísať:

this.brand = Objects.requireNonNull(brand);

Prerobíme teda našu validateBrand metódu na validatedBrand, ktorá po validácií vráti vstupnú hodnotu. Následne tomu prispôsobime aj zvyšok kódu:











 





 






private String validatedBrand(String brand) {
    Objects.requireNonNull(brand, "Brand cannot be null!");
    if (brand.trim().isEmpty()) {
        throw new IllegalArgumentException("Brand cannot be empty!");
    }

    return brand;
}

public Builder brand(String brand) {
    this.brand = validatedBrand(brand);
    return this;
}

public Car build() {
    return new Car(
            validatedBrand(brand),
            model, yearOfProduction, powerKW, powerHP,
            engineDisplacement, fuelType, gearType, gears,
            fuelConsumption, price
    );
}

Poznámka

Ak by sme veľmi chceli, mohli by sme všetky dáta validovať len raz - a to v metóde build. Držíme sa však princípu fail-fast, teda chybu oznámime čo najskôr, ako vieme. Vďaka tomu píšeme kód, ktorý programátorovi ohlási problém blízko k jeho zdroju a tým uľahčí jeho opravu.

# Abstrakcia vo vzore builder

Tentokrát sme dostali trošku odlišnú úlohu od tej prechádzajúcej. Nemusíme si vytvárať žiadnu novú dátovu štruktúru, budeme pracovať so zoznamom celých čísel List<Long>. Naša práca bude spočívať v spracovaní čísel, ktoré symbolizujú poradie vo Fibonacciho postupnosti, a ich premene na príslušnú hodnotu. Ak teda dostaneme na vstupe tieto čísla:

1, 5, 3, 1, 8, 2

Vrátime nasledovné:

1, 5, 2, 1, 21, 1

Zároveň sme dostali rozhranie pre návrhový vzor, ktorú musíme implementovať:

public interface FibonacciBuilder {

    /**
     * Calculates and appends next Fibonacci's number with a given index
     * 
     * @return this instance of builder for method chaining
     * @throws IllegalArgumentException if index is smaller than 1
     */
    FibonacciBuilder add(int index);

    /**
     * Creates a list with calculated Fibonacci's numbers
     * 
     * @return new instance of immutable list of calculated numbers
     */
    List<Long> build();
}

Existuje viacero spôsobov, ako môžeme n-té číslo Fibonacciho postupnosti vypočítať. Najjednoduchší je rekurzia, tá je však aj najmenej efektívna pri zložitosti približne O(1.6^n). Podstatne efektívnejší je lineárny algoritmus v cykle so zložitosťou O(n). Ak však chceme konštantný čas O(1), musíme použiť Binetov vzorec, ktorý je však kvôli obmedzenej presnosti dátového typu double použiteľný len do určitého indexu.

Poznámka

Nepresnosť výpočtu Binetového vzorca v Jave vieme vyriešiť pomocou použitia triedy BigDecimal, ktorá sa používa pri výpočtoch s veľmi veľkými necelými číslami. K tomuto vylepšeniu sa vrátime v rámci úloh na konci tejto kapitoly.

Keďže všetky prístupy sú vhodné na niečo iné, vytvoríme osobitnú implementáciu pre každý jeden:

Rekurzia:

public class RecursiveFibonacciBuilder implements FibonacciBuilder {

    private List<Long> numbers = new ArrayList<>();

    @Override
    public FibonacciBuilder add(int index) {
        numbers.add(compute(validatedIndex(index)));
        return this;
    }

    private int validatedIndex(int index) {
        if (index < 1) {
            throw new IllegalArgumentException("Index of Fibonacci's sequence must be a positive number!");
        }
        return index;
    }

    private long compute(int index) {
        if (index <= 2) {
            return 1;
        }
        return compute(index - 1) + compute(index - 2);
    }

    @Override
    public List<Long> build() {
        // First of all, we are creating a copy of internal list
        // to make sure further modifications will not affect builded list.
        // Second of all, to comply with specification in `FibonacciBuilder`
        // interface, we are wrapping copied list in unmodifiable list to
        // ensure it will become immutable.
        return Collections.unmodifiableList(new ArrayList<>(numbers));
    }
}

Cyklus:

public class LoopFibonacciBuilder implements FibonacciBuilder {

    private List<Long> numbers = new ArrayList<>();

    @Override
    public FibonacciBuilder add(int index) {
        numbers.add(compute(validatedIndex(index)));
        return this;
    }

    private int validatedIndex(int index) {
        if (index < 1) {
            throw new IllegalArgumentException("Index of Fibonacci's sequence must be a positive number!");
        }
        return index;
    }

    private long compute(int index) {
        if (index <= 2) {
            return 1;
        }

        long previousValue = 1;
        long finalValue = 2;

        for (int i = 3; i < index; i++) {
            long newFinalValue = previousValue + finalValue;
            previousValue = finalValue;
            finalValue = newFinalValue;
        }

        return finalValue;
    }

    @Override
    public List<Long> build() {
        // First of all, we are creating a copy of internal list
        // to make sure further modifications will not affect builded list.
        // Second of all, to comply with specification in `FibonacciBuilder`
        // interface, we are wrapping copied list in unmodifiable list to
        // ensure it will become immutable.
        return Collections.unmodifiableList(new ArrayList<>(numbers));
    }
}

Binetov vzorec:

public class BinetFibonacciBuilder implements FibonacciBuilder {

    private static final double SQUARE_ROOT_5 = Math.sqrt(5);
    private static final double GOLDEN_RATIO = (1 + SQUARE_ROOT_5) / 2;

    private List<Long> numbers = new ArrayList<>();

    @Override
    public FibonacciBuilder add(int index) {
        numbers.add(compute(validatedIndex(index)));
        return this;
    }

    private int validatedIndex(int index) {
        if (index < 1) {
            throw new IllegalArgumentException("Index of Fibonacci's sequence must be a positive number!");
        }
        return index;
    }

    private long compute(int index) {
        return (long) (
                (Math.pow(GOLDEN_RATIO, index) - Math.pow(-GOLDEN_RATIO, -index))
                /
                SQUARE_ROOT_5
        );
    }

    @Override
    public List<Long> build() {
        // First of all, we are creating a copy of internal list
        // to make sure further modifications will not affect builded list.
        // Second of all, to comply with specification in `FibonacciBuilder`
        // interface, we are wrapping copied list in unmodifiable list to
        // ensure it will become immutable.
        return Collections.unmodifiableList(new ArrayList<>(numbers));
    }
}

Vďaka tomuto štýlu môžeme písať kód, ktorému je jedno, ktorú z konkrétnych implementácií sme sa rozhodli použiť. Demonštrovať to môžeme na nasledovnej ukážke:

void testBuilder(FibonacciBuilder builder) {
    builder
            .add(1)
            .add(5)
            .add(3)
            .add(1)
            .add(8)
            .add(2);

    System.out.println(builder.build());
}

testBuilder(new RecursiveFibonacciBuilder());
testBuilder(new LoopFibonacciBuilder());
testBuilder(new BinetFibonacciBuilder());

ktorá nám v JShell vypíše:

Defined method void testBuilder(FibonacciBuilder)

> testBuilder(new RecursiveFibonacciBuilder())
[1, 5, 2, 1, 21, 1]

> testBuilder(new LoopFibonacciBuilder())
[1, 5, 2, 1, 21, 1]

> testBuilder(new BinetFibonacciBuilder())
[1, 5, 2, 1, 21, 1]

Pozornému oku však okamžite udrie do očí, že všetky 3 implementácie sa líšia len samotným výpočtom, no inak sú totožné. Preto sa nesmieme báť a musíme do implementácie pridať ešte jednu úroveň abstrakcie, aby sme dodržali DRY princíp:

Abstraktná trieda:


















 







public abstract class AbstractFibonacciBuilder implements FibonacciBuilder {

    private List<Long> numbers = new ArrayList<>();

    @Override
    public FibonacciBuilder add(int index) {
        numbers.add(compute(validatedIndex(index)));
        return this;
    }

    private int validatedIndex(int index) {
        if (index < 1) {
            throw new IllegalArgumentException("Index of Fibonacci's sequence must be a positive number!");
        }
        return index;
    }

    protected abstract long compute(int index);

    @Override
    public List<Long> build() {
        return Collections.unmodifiableList(new ArrayList<>(numbers));
    }
}

Rekurzia:

public class RecursiveFibonacciBuilder extends AbstractFibonacciBuilder {

    @Override
    protected long compute(int index) {
        if (index <= 2) {
            return 1;
        }
        return compute(index - 1) + compute(index - 2);
    }
}

Cyklus:

public class LoopFibonacciBuilder extends AbstractFibonacciBuilder {

    @Override
    protected long compute(int index) {
        if (index <= 2) {
            return 1;
        }

        long previousValue = 1;
        long finalValue = 2;

        for (int i = 3; i < index; i++) {
            long newFinalValue = previousValue + finalValue;
            previousValue = finalValue;
            finalValue = newFinalValue;
        }

        return finalValue;
    }
}

Binetov vzorec:

public class BinetFibonacciBuilder extends AbstractFibonacciBuilder {

    private static final double SQUARE_ROOT_5 = Math.sqrt(5);
    private static final double GOLDEN_RATIO = (1 + SQUARE_ROOT_5) / 2;

    @Override
    protected long compute(int index) {
        return (int) (
                (Math.pow(GOLDEN_RATIO, index) - Math.pow(-GOLDEN_RATIO, -index))
                /
                SQUARE_ROOT_5
        );
    }
}

Pre overenie správnosti môžeme znova spustiť vyššie uvedený ukážkový kód.

# Použitie Director-a na riadenie vytvárania objektov

V niektorých literatúrach sa v rámci návrhového vzoru builder spomína ešte jeden komponent - Build Director, ktorý riadi Builder pri vytváraní objektov. Riaditeľa v tomto zmysle chápeme ako komponent, ktorý volá metódy builder-a a následne aj žiada o vytvorenie objektu. Doteraz sme takýto komponent nemali, pretože builder sme si riadili sami. Čo však, ak jeden a ten istý objekt potrebujeme vytvárať na mnohých miestach? V takom prípade si vytvoríme spomínaného build director-a, ktorý túto logiku zapuzdrí. Nasledujúci príklad ukazuje riaditeľa, ktorý vytvorí prvých 10 Fibonacciho čísel:

public class FirstTenFibonacciBuildDirector {

    private final FibonacciBuilder builder;

    public FirstTenFibonacciBuildDirector(FibonacciBuilder builder) {
        this.builder = builder;
    }

    public List<Long> construct() {
        for (int i = 1; i <= 10; i++) {
            builder.add(i);
        }
        return builder.build();
    }
}

Všimnime si niekoľko vecí:

  • Vytvárajúca metóda sa volá construct a nie build
  • Konkrétnu implementáciu builder-a predávame v konštruktore director-a
  • Názov má tvar [POPIS]BuildDirector, kde [POPIS] v krátkosti určuje, aký typ objektu director vytvára

Vytvorenú triedu v kóde potom používame nasledovne:

FibonacciBuilder builder = new LoopFibonacciBuilder();
FirstTenFibonacciBuildDirector director = new FirstTenFibonacciBuildDirector(builder);

List<Long> firstTen = director.construct();

V praxi sa však namiesto director-a využíva návrhový vzor Factory, ktorému je venovaná samostatná kapitola. Vytvárajúca metóda by tam mala nasledovnú signatúru:

public List<Long> makeFirstTenNumbers(FibonacciBuilder builder);

Poznámka

Dávajte si pozor, aby ste svoj kód zbytočne neprekomplikovali (anglicky tomu hovoríme over-engineering). Build Director použite naozaj len v prípade, ak to má reálny prínos pre váš kód. Niekedy je jednoduchšie riešenie lepšie, ako komplexné, ktoré trvá dlhšie pochopiť.

# Best practices

  • Ak vám stačí jediná implementácia Buildera, umiestnite ju do príslušnej materskej triedy a nechajte jej jednoduchý názov Builder
  • Nepoužívajte prefix set pre metódy Builder-a
  • Metódy (okrem vytvárajúcej) nech vždy vracajú aktuálnu inštanciu Buildera pre umožnenie reťazenia volaní
  • Metóda pre vytvorenie objektu materskej triedy nech sa volá build, resp. ak existujú alternatívy, nech sa vždy týmto slovom začína
  • Ak je to vhodné, vytvorte pomocné metódy na zlepšenie čitateľnosti kódu - napr. metódu today(), ktorá je alias pre date(new Date()); alebo manualGear(), ktorá je alias pre gearType(GearType.MANUAL)
  • Ak je to vhodné, uprednostnite návrhový vzor Factory pred použitím Builder Director-a

# Úlohy

# 1. Dokončite validáciu všetkých údajov

Podľa tabuľky na začiatku kapitoly dokončite validáciu aj zvyšných údajov. Snažte sa pri chybe napísať čo najvýstižnejší popis (v angličtine). Nezabudnite, že údaje model, fuelConsumption a price musia byť validované nielen v ich setter-och, ale aj v metóde build. Na (čiastočné) overenie svojho kódu môžete použiť nasledovné prípady, z ktorých každý musí zlyhať:

Car car1 = new Car.Builder().build();

Car car2 = new Car.Builder()
        .brand("  ")
        .model("S-Max")
        .fuelConsumption(5.8f)
        .price(39500)
        .fuelType(null)
        .engineDisplacement(-1997)
        .build();

Car car3 = new Car.Builder()
        .brand("Ford")
        .model("")
        .fuelConsumption(5.8f)
        .price(39500)
        .engineDisplacement(1997)
        .gears(0)
        .build();

Car car4 = new Car.Builder()
        .brand("Ford")
        .model("S-Max")
        .fuelConsumption(0)
        .price(39500)
        .powerHP(-5)
        .build();

Car car5 = new Car.Builder()
        .brand("Ford")
        .model("S-Max")
        .yearOfProduction(1865)
        .fuelConsumption(5.8f)
        .price(-3)
        .build();

# 2. Implementujte automatickú zmenu súvisiacich údajov

Trieda Car má niekoľko údajov, ktoré spolu aspoň čiastočne súvisia. Najevidentnejším je presný prepočet medzi výkonom v kilowattoch a koňoch, no napríklad pri elektrických autách máme (okrem pretekárskych) vždy len 1 prevodový stupeň.

Zakomponujte do triedy Builder automatické zmeny súvisiacich údajov, ak jeden z nich je zmenený. Môžete začať horeuvedenými príkladmi a skúsiť vymyslieť aj ďalšie, ak nejaké nájdete.

# 3. Vytvorte pomocné metódy na zlepšenie DX (developer experience)

Pamätajme - stroju je jedno, ako kód vyzerá, ale iným vývojárom nie. Preto sa snažíme písať kód tak, aby bola radosť s ním pracovať - zlepšujeme developer experience. Pri návrhovom vzoru builder sa väčšinou vieme s DX vyhrať. Ako príklad si uveďme tento zápis:

builder.fuelType(Car.FuelType.GASOLINE);

Keď to čítame, máme tam zbytočný šum v podobne Car.FuelType. Keď to píšeme, sú to zbytočné znaky navyše (aj keď nám IDE zvyknú v tomto smere pomôcť). Určite sa zhodneme, že niečo ako:

builder.gasolineFuel();

je krajšie a čitatelnejšie. Nejde pri tom o nič viac, ako len alias predchádzajúceho dlhého zápisu.

V rámci tejto úlohy vytvorte čo najviac (zmysluplných) pomocných metód, aby používanie triedy Builder bolo pre iných vývojárov priam zábavné. Nebojte sa byť kreatívny - vyskúšajte si vytvoriť pomocou aktuálnej verzie Builder triedy niekoľko áut a všimnite si, čo je zbytočne dlhé, čo sa opakuje a čo by sa dalo skrášliť. Existujúcu funkcionalitu nemeňte - využite ju!

# 4. Upravte BinetFibonacciBuilder tak, aby fungoval aj pre veľké čísla

Ako sme v rámci kapitoly spomínali, Binetov vzorec nám vypočíta Fibonacciho číslo presne len do určitého indexu - v prípade použitia double len do 70-tého miesta. Potom sa vypočítané hodnoty začnú postupne vzďaľovať. Demonštrovať to môžeme nasledujúcou metódou findError:

void findError() {
    FibonacciBuilder loop = new LoopFibonacciBuilder();
    FibonacciBuilder binet = new BinetFibonacciBuilder();

    IntStream.rangeClosed(1, 92).forEachOrdered(index -> {
        loop.add(index);
        binet.add(index);
    });

    // Find and report the first wrong number
    List<Long> loopList = loop.build();
    List<Long> binetList = binet.build();

    OptionalInt firstIncorrect = IntStream.range(0, 92)
            .filter(i -> !loopList.get(i).equals(binetList.get(i)))
            .findFirst();

    if (firstIncorrect.isPresent()) {
        int index = firstIncorrect.getAsInt();
        System.out.println(
                String.format("Found mismatch!\nIndex: %d\n\tLoop:  %d\n\tBinet: %d\n",
                        index, loopList.get(index), binetList.get(index)
                )
        );
    } else {
        System.out.println("No errors has been found! Both algorithms are precise for long integers!");
    }
}

findError();

Keď ju spustíme na pôvodnú implementáci, zistíme, že 71. Fibonacciho číslo už bude Binetovým vzorcom na rozdiel od cyklického algoritmu zle vypočítané:

Defined method void findError()

> findError()
Found mismatch!
Index: 71
    Loop:  498454011879264
    Binet: 498454011879265

Vašou úlohou je použiť zabudovanú Java triedu na prácu s obrovskými desatinnými číslami BigDecimal tak, aby vypočítané výsledky pre prvých 92 Fibonacciho čísel boli presné (92 len preto, lebo 93. je už väčšie, ako kapacita dátového typu long v Jave). Pri správnej implementácii vám metóda findError vypíše nasledovné:

Defined method void findError()

> findError()
No errors has been found! Both algorithms are precise for long integers!

# 5. Upravte FirstTenFibonacciBuildDirector tak, aby sa jedna inštancia dala použiť viackrát

Ak ste pozorne čítali kód triedy FirstTenFibonacciBuildDirector, zistili ste, že ak by sme sa pokúsili volať jej metódu construct viackrát, nedostaneme správny výsledok:

> List<Long> firstTen = director.construct();
Defined field List<Long> firstTen = [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

> List<Long> firstTenAgain = director.construct();
Defined field List<Long> firstTenAgain = [1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

Deje sa to preto, lebo pri volaní construct sa znova vykonajú operácie nad inštanciou builder-a. Upravte kód tak, aby sme mohli metódu volať veľakrát a vždy dostali správny výsledok. Ak sa však construct nezavolá vôbec, nad inštanciou builder-a by sa nemalo vykonať nič. Ak ste zmeny implementovali správne, mali by ste dostať nasledovný výsledok:

> List<Long> firstTen = director.construct();
Defined field List<Long> firstTen = [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

> List<Long> firstTenAgain = director.construct();
Defined field List<Long> firstTenAgain = [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

# 6. Navrhnite štruktúru a builder pre vážený orientovaný graf

Kým v doterajśích úlohách ste mali pracovať s existujúcim zadaním, v tejto úlohe dostanete voľnú ruku a budete sa môcť vyhrať a vyskúšať si princípy naučené v rámci kapitoly.

Vašou úlohou bude navrhnúť štruktúru a následne implementovať príslušný builder pre vážený orientovaný graf. Z matematiky poznáme, že orientovaný graf (digraf) je špecifický v tom, že jeho hrany majú orientáciu - hrana smeruje z jedného vrcholu do druhého. Vážený digraf je rozdielny len v tom, že hrany majú aj cenu. Môžeme to vidieť na nasledovnej ukážke:

Ukážka váženého orientovaného grafu

Takéto grafy majú v matematike a informatike mnohé využitia (napr. pri hľadaní najkratšej cesty medzi 2 mestami), preto vytvorenie vlastnej implementácie môže byť zaujímavé precvičenie.

Znova opakujeme, že v tejto úlohe máte voľnú ruku, no mali by ste sa držať nasledovných bodov:

  • Vrcholy sú reprezentované ľubovoľnými celými číslami. Musia byť unikátne - jedno číslo môže označiť najviac 1 vrchol. Rovnako jeden vrchol má práve 1 označenie.
  • Ceny hrán sú rovnako reprezentované celými číslami
  • Medzi 2 vrcholmi môžu byť najviac 2 hrany, nikdy však nie v rovnakom smere
  • Izolované vrcholy (do ktorých nevstupuje ani z ktorých nevystupujú žiadne hrany) sú povolené a musia sa dať vašim builder-om vytvoriť
  • Trieda reprezentujúca vážený digraf by mala byť nemenná (immutable) - po jej vytvorení z príslušného builder-a by sa jej hrany a vrcholy už nemali dať meniť

Poznámka

Je len na vás, či budete akceptovať aj záporné ceny hrán.

Na odrazenie môžete (ale nemusíte) použiť nasledujúcu kostru triedy WeightedDigraph:

public class WeightedDigraph {

    public boolean hasNode(int node);

    public boolean isNodeIsolated(int node);

    public boolean isEdge(int source, int destination);

    public int getEdgeCost(int source, int destination);

    public int getNodeInDegree(int node);

    public int getNodeOutDegree(int node);

    public Set<Integer> getNeighbours(int node);
}

Poznámka

Ak neviete, ako začat, skúste postupovať podobne, ako sme na to išli pri Car.Builder - najprv si nadefinujte štruktúru digrafu, potom vytvorte jednoduchý builder a až potom ho upravujte a zlepšujte.

Nad touto úlohou sa oplatí potrápiť - na jej základoch sa dajú vytvoriť ďalšie zaujímavé veci, napr. aj konečno-stavový automat (finite state machine).

# 7. Zovšeobecnite implementáciu váženého orientovaného grafu

Aktuálna implementácia nám umožňuje označiť vrcholy len číslom. Čo ak by sme však potrebovali vrcholy pomenovať písmenom, čitateľným názvom, alebo reprezentovať ich vlastnou triedou? Upravte implementáciu použitím generického programovania tak, aby vrcholy mohli byť reprezentované čímkoľvek podľa konkrétnej potreby ľubovoľného problému.

# 8. Zdokumentujte štruktúru a builder pre vážený orientovaný graf

Je vždy dôležité zdokumentovať svoj kód nielen pre vývojárov, ktorí budú vašu implementáciu používať, ale aj pre tých, ktorí ju budú ďalej vyvíjať. Preto si nájdite čas a zmysluplne okomentujte svoje vytvorené triedy a metódy pre príklad s váženým digrafom pomocou JavaDoc. Ak máte komplexnejšie metódy, ktorých logika nie je z popisu pre klientskú stranu jasná, resp. nie je evidentné, prečo sa niečo vykonáva, napíšte krátke vysvetlenie, čo robíte a prečo to robíte.

Príklad JavaDoc dokumentácie (z predchádzajúcich príkladov):

/**
 * Creates a list with calculated Fibonacci's numbers
 * 
 * @return new instance of immutable list of calculated numbers
 */
List<Long> build();

Príklad dokumentácie implementačného kódu (z predchádzajúcich príkladov):

@Override
public List<Long> build() {
    // First of all, we are creating a copy of internal list
    // to make sure further modifications will not affect builded list.
    // Second of all, to comply with specification in `FibonacciBuilder`
    // interface, we are wrapping copied list in unmodifiable list to
    // ensure it will become immutable.
    return Collections.unmodifiableList(new ArrayList<>(numbers));
}

# Riešenia úloh

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

# Pozri tiež

# Diskusia