# Prototyp

Zatiaľ sme si v rámci kreačných návrhových vzorov ukázali, ako zjednodušiť a obaliť vytváranie komplexných objektov (builder) a ako zovšeobecniť vytváranie skupiny rovnakých alebo príbuzných objektov (factory).

Teraz sa pozrieme na jednoduchú alternatívu pre oba vzory - prototyp. Jeho princíp spočíva v tom, že si vytvoríme jednu inštanciu želanej triedy so všeobecnými parametrami, ktorú nazveme prototyp, z ktorého budeme klonovať nové inštancie, ktoré budeme následne ďalej používať, pričom pôvodného prototypu sa nedotkneme a necháme ho nezmenený v pôvodnom stave.

Tento návrhový vzor sa hodí pri triedach, ktoré sú menné (mutable), teda ich stav vieme zmeniť aj po vytvorení inštancie.

Poznámka

Toto je posledná kapitola, v ktorej budeme pracovať s príkladmi pre vytváranie áut. Je preto odporúčané, aby ste si predtým prešli predchádzajúce kapitoly Builder a Factory.

# Modelová situácia

Doteraz sme v rámci domény áut pracovali len s nemennými (immutable) objektami. Prirodzene, nie všetko môže byť statické a preto v istom momente musíme opäť pracovať s objektami, ktoré si držia stav. Pre tento účel sa pozrieme na implementáciu požičovne - tá si musí manažovať zoznam dostupných áut, zoznam požičaných áut a pre spestrenie budeme počítať aj zisk z pôžičiek. Jedna z možných implementácií vyzerá nasledovne:

public class CarRental {

    private List<Car> availableCars = new ArrayList<>();
    private List<Car> rentedCars = new ArrayList<>();
    private int revenue = 0;

    public List<Car> getAvailableCars() {
        return availableCars;
    }

    public List<Car> getRentedCars() {
        return rentedCars;
    }

    public int getRevenue() {
        return revenue;
    }

    public void addCar(Car car) {
        availableCars.add(car);
    }

    public void addCars(List<Car> car) {
        availableCars.addAll(car);
    }

    public int rentCar(Car car, int days) {
        if (!availableCars.contains(car)) {
            throw new IllegalArgumentException("Given car is not available for rental");
        }

        if (days <= 0) {
            throw new IllegalArgumentException("Cannot rent a car for less than a day!");
        }

        int rentalPrice = calculateRentalPricePerDay(car) * days;

        availableCars.remove(car);
        rentedCars.add(car);
        revenue += rentalPrice;

        return rentalPrice;
    }

    public void returnCar(Car car) {
        if (!rentedCars.contains(car)) {
            throw new IllegalArgumentException("Given car has not been rented!");
        }

        rentedCars.remove(car);
        availableCars.add(car);
    }

    public int calculateRentalPricePerDay(Car car) {
        return (int) Math.ceil(car.getPrice() / 1000.0);
    }
}

Poďme si v rýchlosti prebehnúť, čo sa v metóde deje:

  • Všetky členské premenné sú menné - ich stav vieme zmeniť. Aj keby sme označili availableCars a rentedCars ako final, ich vnútorný stav bude stále menný (budeme vedieť pridávať a odoberať prvky).
  • Pre vonkajší svet sme otvorili len gettery členských premenných. Ich vnútorný stav tak chceme ovládať len z vnútra triedy.
  • Pridať autá do zoznamu dostupných na prenájom dokážeme len cez metódy addCar a addCars.
  • Presúvať autá medzi stavom na prenájom a v prenájme je možné len metódami rentCar a returnCar, ktoré zároveň zabezpečujú konzistenciu vnútorného stavu.
  • Metóda rentCar zároveň vráti cenu prenájmu
  • Pre vonkajší svet sme otvorili aj pomocnú metódu calculateRentalPricePerDay, slúžiacu na výpočet ceny prenájmu daného auta na jeden deň. Takto dovoľujeme odhadnúť celkovú sumu ešte pred jeho prenajatím.

Na tejto implementácii aplikujeme vzor prototyp.

# Kopírovacia metóda

Ak by sme si chceli vytvoriť novú požičovňu s ponukou áut, takéto vytváranie by mohlo vyzerať nasledovne:

CarRental rental = new CarRental();
CarFactory skodaFactory = new SkodaFactory();

rental.addCar(FordFactory.sMax());
rental.addCar(FordFactory.fiesta());
rental.addCar(skodaFactory.suv());
rental.addCar(skodaFactory.sedan());

Tento príklad je ešte malý, ale ako vidíte, s počtom áut a výrobcou bude rásť aj jeho komplexita. Môžeme uvažovať o tom, že zoznam áut by sme mali uložený ako konštantu. Kód by sa nám o niečo zredukoval:

// This would be defined somewhere only once
CarFactory skodaFactory = new SkodaFactory();
List<Car> generalCarsList = Arrays.asList(
	FordFactory.sMax(),
	FordFactory.fiesta(),
	skodaFactory.suv(),
	skodaFactory.sedan()
);

// Creation of new rentals
CarRental rental = new CarRental();
rental.addCars(generalCarsList);

To je už o niečo lepšie, no pokiaľ by sme mali viacero atribútov, ktoré by sme museli nastavovať, opäť by naša komplexita vzrástla. Nechajme teda naše pokusy stranou a poďme sa pozrieť na to, ako tento problém rieši návrhový vzor prototyp.

Začneme veľmi jednoducho - vytvoríme si predpripravenú inštanciu požičovne, ktorú nazveme rentalPrototype:

CarRental rentalPrototype = new CarRental();
CarFactory skodaFactory = new SkodaFactory();

rentalPrototype.addCar(FordFactory.sMax());
rentalPrototype.addCar(FordFactory.fiesta());
rentalPrototype.addCar(skodaFactory.suv());
rentalPrototype.addCar(skodaFactory.sedan());

Ako ste si isto všimli, toto je identický kód s prvým príkladom, len s tým rozdielom, že sme premenovali jednu premennú. Kde sa teda nachádza návrhový vzor? V tom, že v prvý príklad demonštroval, ako by sme vytvárali každú novú inštanciu požičovne, kým tento demonštruje, ako vytvoríme všeobecný vzor pre ostatné. V tejto chvíli nám však chýba posledná vec - spôsob, akým by sme mohli prototyp naklonovať / nakopírovať. Na tento účel vytvoríme metódu copy s nasledujúcou signatúrou:

public CarRental copy();

Ktorú v kóde budeme používať nasledovne:

CarRental rental = rentalPrototype.copy();

Poznámka

Ak by ste preskúmali triedu Object, z ktorej dedia všetky triedy v Jave, zistili by ste, že obsahuje chránenú metódu clone. Tá na kopírovanie objektov používa zabudovanú "mágiu". Keďže je však s ňou niekoľko problémov a mnohé zdroje ju neodporúčajú používať, rozhodli sme sa ukázať si vlastné riešenie, nad ktorým máme plnú kontrolu. Ak máte záujem prečítať si o klonovaní v Jave viac, v sekcii Pozri tiež nájdete podrobnejší článok.

Cieľom metódy copy je vytvoriť novú inštanciu triedy CarRental, ktorá však bude mať identický stav - hodnoty jej vnútorných premenných budú rovnaké. Na ziačiatok môžeme skúsiť naivne vytvoriť niečo nasledovné:

public CarRental copy() {
    CarRental copy = new CarRental();

    copy.availableCars = availableCars;
    copy.rentedCars = rentedCars;
    copy.revenue = revenue;

    return copy;
}

Na oko to vyzerá dobre. Treba si však uvedomiť jednu zásadnú vec. Premenná rental je int - a to je primitívny dátový typ, takže pri priradení v copy metóde sa do nového objektu sa kopíruje jeho hodnota, nie referencia. Premenné availableCars a rentedCars sú však typu List - a to je referenčný typ, takže pri priradení sa kopíruje jeho referencia (ukazovateľ - pointer), nie jeho vnútorná hodnota. Čo to znamená v praxi? Ak by sme vytvorili kópiu objektu a ju upravili, upravil by sa aj spätne prototyp. Demonštrovať to môžeme na nasledujúcom príklade v JShell:




 
 






 
 

 
 

> CarRental prototype = new CarRental();
Defined field CarRental prototype = sk.tuke.fei.kpi.oop.chapters.creational.prototype.CarRental@58f838eb

> prototype.getAvailableCars().size()
0

> CarRental copy = prototype.copy();
Defined field CarRental prototype = sk.tuke.fei.kpi.oop.chapters.creational.prototype.CarRental@45c0df8a

> copy.addCar(FordFactory.sMax());

> copy.getAvailableCars().size();
1

> prototype.getAvailableCars().size();
1

Aby sme tomu predišli, musíme nakopírovať aj samotné zoznamy áut. Na ich naplnenie môžeme použiť metódu addAll. Nakoľko zoznamy obsahujú inštancie nemennej (immutable) triedy Car, tie už kopírovať nemusíme.




 
 
 
 

 
 





public CarRental copy() {
    CarRental copy = new CarRental();

    List<Car> copiedAvailableCars = new ArrayList<>();
    copiedAvailableCars.addAll(availableCars);
    List<Car> copiedRentedCars = new ArrayList<>();
    copiedRentedCars.addAll(rentedCars);

    copy.availableCars = copiedAvailableCars;
    copy.rentedCars = copiedRentedCars;
    copy.revenue = revenue;

    return copy;
}

V tejto chvíli sa už bude metóda copy správať tak, ako by sme očakávali. Zároveň sme tým úspešne dokončili imlementáciu vzoru prototyp.

# Kopírovací konštruktor

Alternatívou ku kopírovacej metóde je tzv. kopírovací konštruktor. Funkcionalita a účel sú rovnaké, jediný rozdiel je ten, že sa nevolá žiadna metóda, ale objekt na kopírovanie sa predá ako parameter konštruktora triedy. Tento spôsob používa aj štandardná knižnica Javy - napríklad je tak možné vytvoriť nový zoznam s identickými prvkami:

List<Car> copy = new ArrayList<>(original);

Tento princíp môžeme uplatniť na zjednodušenie našej kopírovacej metódy:




 
 





public CarRental copy() {
    CarRental copy = new CarRental();

    copy.availableCars = new ArrayList<>(availableCars);
    copy.rentedCars = new ArrayList<>(rentedCars);
    copy.revenue = revenue;

    return copy;
}

Niečo podobné si pridáme do triedy požičovne aj my - vytvoríme konštruktor, ktorý zoberie ako jediný argument už existujúcu inštanciu inej požičovne a prekopíruje si jej atribúty:

 



 





public CarRental() {
    // Default constructor
}

public CarRental(CarRental source) {
    availableCars = new ArrayList<>(source.availableCars);
    rentedCars = new ArrayList<>(source.rentedCars);
    revenue = source.revenue;
}

Všimnite si jednu dôležitú vec - okrem kopírovacieho sme vytvorili aj prázdny konštruktor bez argumentov. Pokiaľ nedefinujeme žiadny, Java nám tento implicitne vytvorí, no akonáhle zavedieme aspoň 1 vlastný, už sa automaticky nevygeneruje. A nakoľko nemôžeme požičovňu len kopírovať, ale chceme ju aj, prirodzene, vytvoriť od základov, musíme si takýto prázdny konštruktor manuálne pridať.

Naša posledná úprava bude spočívať v dodržaní DRY princípu. Aby sme logiku kopírovania nemali na dvoch odlišných miestach, upravíme copy metódu, aby delegovala túto funkcionalitu na konštruktor:

public CarRental copy() {
    return new CarRental(this);
}

# Ďalšie použitia kopírovania

Okrem návrhového vzoru prototyp sa kopírovanie používa aj na iné účely, s ktorými sa môžete stretnúť.

# Defenzívna kópia

Jedným z dôvodov, prečo je nemennosť (immutability) taká výhodná, je to, že sa nemusíme báť, že externý kód zmení stav našich tried. Pokiaľ však pracujeme s menným kódom, môže sa naozaj stať, že interný stav požičovne by mohol byť modifikovaný inak (iným kódom), ako si to prajeme. Pozrime sa na nasledujúci kód:

rental.getRentedCars().add(FordFactory.sMax());

Neprenajali sme si žiadne auto, no predsa sa jedno nové vyskytlo v zozname požičaných. Ako by sme sa vedeli niečomu takému brániť? Môžeme upraviť getter tak, aby sme nevracali vnútornú premennú, ale jej kópiu:

public List<Car> getRentedCars() {
    return new ArrayList<>(rentedCars);
}

Takýto prístup voláme defenzívne kopírovanie.

# Kopírovanie nemenných štruktúr v iných jazykoch

Doteraz sme si kopírovanie ukazovali len na menných objektoch, no má zmysel uvažovať o niečom podobnom aj pri nemenných? V Jave nanešťastie nie, nakoľko by to bolo nepraktické, no v niektorých programovacích jazykoch máme zabudovanú možnosť kopírovania nemenných inštancií objektov za účelom vytvorenia novej inštancie s čiastočne pozmenenými atribútmi.

Príkladom jazykov s touto funkcionalitou sú Kotlin a Scala. Oba pre špeciálne triedy (data class a case class) automaticky vygenerujú rôzne metódy, vrátane copy, ktorá umožňuje vytvoriť kópiu z nemennej inštancie, pričom zmení len špecifikované údaje. Vyzerá to nasledovne:

Kotlin:

data class User(val name: String, val age: Int)

val user = User("John Doe", 45)
val olderUser = user.copy(age = 50)

Scala:

case class User(name: String, age: Int)

val user = User("John Doe", 45)
val olderUser = user.copy(age = 50)

# Best practices

  • Kopírovacia metóda by sa nemala volať clone (ktorá je súčasťou každého objektu, zdedená od Object), ale napríklad copy
  • Pri používaní kopírovacieho konštruktoru myslite na to, že nie je vhodný, ak od danej triedy dedia ďalšie
  • Prototyp nemá veľký význam pri nemenných (immutable) triedach, pokiaľ na to nemá vstavanú podporu jazyka

# Úlohy

# 1. Vytvorte factory triedu pre požičovňu áut s použitím prototypov

Aj keď sme na začiatku kapitoly spomínali, že vzor Prototyp je jednoduchšia alternatíva k Factory a Builder-u, neznamená tom, že sa nedajú kombinovať.

Vytvorte factory, ktorá bude obsahovať naslednovné metódy:

  • ford, ktorá vráti požičovňu s autami značky Ford
  • skoda, ktorá vráti požičovňu s autami značky Škoda
  • general, ktorá vráti požičovňu s autami značiek Ford aj Škoda

Prirodzene, použite pri tom vzor prototyp, ktorý sme si počas kapitoly pripravili. Forma implementácie factory (factory metóda, factory trieda, statická factory) je už len na vás.

# 2. Zabráňte úprave stavu prototypu

Jedna z malých chybičiek krásy nášho prototypu je to, že je stále upraviteľný. To nás vystavuje nebezpečenstvu, že ak by sme sa dostali k inštancii prototypu a zabudli ho okopírovať, začali by sme používať a meniť jeho stav, čo by sa nám pri ďalšom kopírovaní vypomstilo.

Vašou úlohou je implementovať metódu prototype, ktorá vráti nemennú inštanciu triedy CarRental s rovnakými atribútmi.

Poznámka

To, že vnútri objektu nie sú atribúty označené ako final neznamená, že objekt nemôže byť nemeniteľný - dôležité je, aby sa jeho stav nemenil. Skúste teda porozmýšľať, ako jednoducho zabránite, aby deštruktívne metódy nedokázali upraviť vnútorný stav inštancie vrátenej triedou prototype.

# 3. Vytvorte kopírovateľnú reprezentáciu firemnej štruktúry

Uplatniť vzor prototyp pre CarRental bolo jednoduché, nakoľko išlo o primitívnu štrukúturu, no ako by to vyzeralo, ak by sme pracovali s niečim komplexnejším? V tejto úlohe budeme implementovať reprezentáciu firmy/organizácie, ktorá má svoje oddelenia a tie svojich priradených zamestnancov.

Dátová štruktúra by mala vyzerať nasledovne:

company
  - name
  - departments
    - name
    - members (employees)
      - name
      - surname
      - position
      - salary

A mala by mať podporu pre nasledovné operácie:

  • Plnú kopírovateľnosť (cez konštruktor alebo kopírovaciu metódu, výber je na vás)
  • Zamestnať zamestnanca do konkrétneho existujúceho oddelenia
  • Prepustiť zamestnanca bez znalosti, v ktorom oddelení pracuje
  • Zvýšiť (zmeniť) zamestnancovi plat
  • Zmeniť pracovnú pozíciu zamestnanca

Poznámka

Vo vašej implementácii skúste urobiť reprezentáciu zamestnanca nemennou triedou a vyskúšajte si tak prácu s ňou pri zmene platu či pracovnej pozície.

# Riešenia úloh

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

# Pozri tiež

# Diskusia