# Value Object

Keď sa pozrieme na dáta z pohľadu počítača, vidíme ich ako postupnosť jedničiek a núl - teda len ako číslo, a to bezohľadu na to, či dané dáta reprezentujú pravdivostnú hodnotu, číslo, text či komplexné objekty.

Poďme o úroveň vyššie. Z pohľadu programovacieho jazyka máme na samom základe tiež len primitívne hodnoty - čísla, logické hodnoty, znaky a reťazce. Sú už o niečo komplexnejšie, vieme nimi prirodzenejšie vyjadriť naše dáta, avšak stále sú na veľmi nízkej úrovni. Určite sa zhodneme, že napr. vek a poschodie v budove sú 2 úplne rozličné veci, no v kóde by sme oba vyjadrili dátovým typom int. To môže dať naoko predstavu, že sú nejako porovnateľné, alebo že sú rovnakého druhu, no v reálnom svete by nám niečo také ani len nenapadlo.

Poznámka

Dátový typ String, teda reťazec znakov, nie je primitívny dátový typ, avšak v tejto kapitole ho budeme považovať za rovnaký základný stavebný blok, ako skutočne primitívne typy (int, double, boolean a podobne).

Pokladá sa nám teda otázka - Prečo nemôžeme takéto intuitívne rozlišovanie objektov rôznych druhov zaviesť aj v našom kóde?. Odpoveď je jednoduchá - môžeme, a aj to urobíme! V rámci tejto kapitoly využijeme naše znalosti o nemenných POJO objektoch, aby sme vytvorili tzv. Value Objects - triedy obaľujúce primitívne dátové typy, aby v kóde lepšie vyjadrovali hodnotu, ktorú vnútorne obsahujú. Zistíme, ako transformovať nasledujúci nízkourovňový zápis:

int age = 5;
int floor = -1;

Na expresívnejšiu verziu na vyššej úrovni:

Age age = new Age(5);
Floor floor = new Floor(-1);

Okrem zmysluplnejšieho kódu získame aj možnosť garantovať integritu dát ich validáciou pri vytváraní objektu a skrytie internej implementácie (čo sa hodí najmä pri komplexných objektoch ako napr. dátum).

Value Object a všeobecne tento typ prístupu k dátam tvorí dôležitú časť tzv. doménovo riadeného (domain driven) dizajnu softvéru.

# Modelová situácia

V rámci ukážky modelovej situácie sa najprv pozrieme na jednoduché príklady, aké sme už v úvode spomenuli, a postupne prejdeme na komplexnejšie ukážky.

# Jednoduchý Value Object

Bez zbytočných slov sa vráťme k príkladu z úvodu - k údaji o veku. Najzákladnejšia forma tejto triedy by bola nasledovná:

public class Age {
    
    private final int age;

    public Age(int age) {
        this.age = age;
    }
    
    public int value() {
        return age;
    }
}

Aktuálne sme implementovali len to najzákladnejšie:

  • Nemennú premennú age, ktorá interne drží hodnotu
  • Konštruktor, ktorý túto hodnotu nastaví
  • Getter, ktorým sa dostaneme k hodnote zvonku

My však už poznáme POJO, a teda vieme, že máme ešte pred sebou kus práce. Začneme validáciou dát - mať vek o zápornej hodnote predsa nedáva zmysel! Preto to v konštruktore ošetríme:


 
 
 




public Age(int age) {
    if (age < 0) {
        throw new IllegalArgumentException("Age must be a natural number or zero!");
    }

    this.age = age;
}

Postupujeme ďalej. Implementujme hashCode, equals a toString podľa predchádzajúcich kapitol:

@Override
public boolean equals(Object o) {
    if (this == o) {
        return true;
    }

    if (o == null || getClass() != o.getClass()) {
        return false;
    }

    Age other = (Age) o;
    return age == other.age;
}

@Override
public int hashCode() {
    return Objects.hash(getClass(), age);
}

@Override
public String toString() {
    return "Age(" + age + ")";
}

Možno trošku prekvapivo, ale máme hotovo - triedu Age už môžeme používať! Na demonštráciu si vytvoríme hierarchicky vyššiu triedu o človeku s názvom Person, ktorá zatiaľ bude obsahovať len jeho vek:

public class Person {

    private final Age age;

    public Person(Age age) {
        this.age = age;
    }

    public Age getAge() {
        return age;
    }
}

K tejto triede sa vrátime ešte v rámci úloh.

# Implementovanie základných rozhraní

V rámci štandardnej knižnice Javy máme niekoľko užitočných rozhraní, ktoré je vhodné poznať. Na triede Age si ukážeme a vysvetlíme ich použitie.

# Serializable

Ak by sme mali údaje len vo forme objektov v operačnej pamäti, veľmi by nám to z dlhodobého hľadiska nepomohlo - v istom momente ich budeme potrebovať uložiť na trvácnejšie miesto - potrebujeme ich perzistovať. Transformácia dát z operačnej pamäte do perzistentnej formy voláme serializácia, opačný proces voláme deserializácia.

Konkrétnymi metódami, ako to urobiť, sa zaoberať nebudeme. Čo si však povedať musíme je to, že nie všetky triedy v Jave sú serializovateľné. Máme však garantované, že primitívne dátove typy a String serializovateľné sú, no všetky naše triedy, ktoré si vytvoríme, nie sú. Preto, ak by sme sa pokúsili o perzistenciu dát, dostali by sme nasledujúcu chybu:

java.io.NotSerializableException: sk.tuke.fei.kpi.oop.chapters.structural.valueObject.Age

Tá nám hovorí, že trieda Age nie je serializovateľná a preto sa Java rozhodla, že nebude v procese pokračovať. Ako to môžeme vyriešiť? Veľmi jednoducho - implementujeme rozhranie Serializable.

public class Age implements Serializable

Čo sa stalo? Takto sme povedali Jave, že "pozreli sme sa na to a je okej, môže to byť serializované". Z našej strany nie je nutné nič viac robiť, všetko bude pre triedu Age fungovať, ako má. Nezabudnite však, že aby bola trieda skutočne serializovateľná, musia túto podmienku spĺňať aj jej vnútorné atribúty. To nás bude zaujímať neskôr pri materskej triede Person.

Poznámka

Rozhranie Serializable je prázdne - je to tzv. marker. O ňom ako o návrhovom vzore sa dozviete v nasledujúcich kapitolách.

# Comparable

Pracovať so zoznamom dát je úplne bežné. Napríklad so zoznamom čísel. Častokrát však poradie v zozname nie je garantované a čísla môžu byť rozhádzané. Preto ich niekedy, keď je to potrebné, musíme zoradiť. Demonštrovať si to môžeme na nasledujúcom príklade v JShell:




 




> List<Integer> list = Arrays.asList(5, 9, 1, 900, 23);
Defined field List<Integer> list = [5, 9, 1, 900, 23]

> list.sort(Comparator.naturalOrder());

> System.out.println(list);
[1, 5, 9, 23, 900]

Rozhranie List obsahuje metódu na zoradenie sort. Tá prijíma jediný argument - Comparator, ktorý metóde povie, ako má list zoradiť. My použijeme už existujúcu preddefinovanú stratégiu Comparator.naturalOrder. Tá použije zabudované porovnávanie celých čísel - teda list zoradí vzostupne.

Určite odporúčame pozrieť sa na triedu Comparator a všetky spôsoby, ako si môžete vybudovať stratégiu pre zoradenie. Nateraz si však ukážeme len dropnú úpravu kódu pre zostupné zoradzovanie:

> list.sort(Comparator.reverseOrder())

> System.out.println(list)
[900, 23, 9, 5, 1]

Keď sa vrátime k našej triede Age, určite uznáme, že má zmysel uvažovať o zoradzovaní veku. Pokiaľ si upravíme premennú list na to, aby obsahovala inštancie Age a pokúsime sa tento list zoradiť, dostaneme nasledujúcu chybu:

> List<Age> list = Arrays.asList(new Age(55), new Age(4), new Age(23), new Age(17));
Defined field List<Age> list = [Age(55), Age(4), Age(23), Age(17)]

> list.sort(Comparator.naturalOrder());
ERROR: incompatible types: inference variable T has incompatible bounds
    upper bounds: java.lang.Comparable<? super T>
    lower bounds: sk.tuke.fei.kpi.oop.chapters.structural.valueObject.Age
Rejected list.sort(Comparator.naturalOrder())

Dôvod je ten, že sme Jave nevysvetlili, ako sa trieda Age zoradzuje. Na to použijeme rozhranie Comparable, ktoré má jedinú metódu compareTo:

 




 













public class Age implements Serializable, Comparable<Age> {

    // Rest of the class omitted for brevity

    @Override
    public int compareTo(Age other) {
        Objects.requireNonNull(other, "Can't compare to null");

        if (age < other.age) {
            return -1;
        } else if (age > other.age) {
            return 1;
        } else {
            return 0;
        }
    }
}

Všimnime si, že compareTo prijíma argument Age a vracia hodnotu int. Typ argumentu sme si povedali my, návratová hodnota je daná. Tá sa riadi podľa nasledujúcej logiky:

  • Ak je aktuálna inštancia menšia ako daná v argumentoch, vráť záporné číslo
  • Ak je aktuálna inštancia väčšia ako daná v argumentoch, vráť kladné číslo
  • Ak sú inštancie rovnaké, vráť nulu

Nakoľko je však porovnávanie celých čísel štandardná vec, nemusíme použiť našu implementáciu, ale môžeme úlohu delegovať Javouvskej knižnici, konkrétne statickej metóde compare v triede Integer. Ak používate IntelliJ, možno vám túto úpravu prostredie ponúklo samo:




 


@Override
public int compareTo(Age other) {
    Objects.requireNonNull(other, "Can't compare to null");
    return Integer.compare(age, other.age);
}

Ak teraz skúsite zoradiť list vekov podľa našej ukážky, bude už fungovať správne.

# Komplexný Value Object

Väčšina našich Value Object-ov bude obaľovať len jednoduchú hodnotu. Čo však robiť, ak máme skupinu úzko súvisiacich údajov? Zoberme si ako príklad meno. Ak by sme všetko mali štandardne naplocho v triede, mohlo by to vyzerať takto:

public class Person {
    private final Titles titlesBeforeName;
    private final Name forename;
    private final Name surname;
    private final Titles titlesAfterName;
    private final Age age;
}

Skúsme to proovnať s nasledujúcim zápisom:

public class Person {
    private final Name name;
    private final Age age;
}

Lepšie a jasnejšie. Poďme sa pozrieť na našu novú triedu Name a postupne si ju vybudujme, podobne ako pri Age.










 
 




public class Name {

    private final String titlesBeforeName;
    private final String forename;
    private final String surname;
    private final String titlesAfterName;

    public Name(String titlesBeforeName, String forename, String surname, String titlesAfterName) {
        this.titlesBeforeName = titlesBeforeName;
        this.forename = Objects.requireNonNull(forename, "Forename cannot be null!");
        this.surname = Objects.requireNonNull(surname, "Surname cannot be null!");
        this.titlesAfterName = titlesAfterName;
    }
}

Všetky hodnoty sme teda skryli do 1 triedy, v ktorej povinne vyžadujeme len forename a surname. Keďže veľa ľudí nemá titul pred menom, nieto ešte za menom, vytvoríme ďalšie 2 alternatívne konštruktory pre zjednodušenie vytvárania inštancií:

public Name(String titlesBeforeName, String forename, String surname) {
    this(titlesBeforeName, forename, surname, null);
}

public Name(String forename, String surname) {
    this(null, forename, surname, null);
}

Prirodzene, potrebujeme sa k jednotlivým údajom dostať. Preto napíšeme gettery:


 











 






public Optional<String> getTitlesBeforeName() {
    return Optional.ofNullable(titlesBeforeName);
}

public String getForename() {
    return forename;
}

public String getSurname() {
    return surname;
}

public Optional<String> getTitlesAfterName() {
    return Optional.ofNullable(titlesAfterName);
}

public String getFullName() {
    return forename + ' ' + surname;
}

Všimnite si však, čo sme urobili pri getteroch getTitlesBeforeName a getTitlesAfterName. Vieme, že tieto atribúty môžu nadobúdať hodnoty null v prípade, že daná osoba žiadne nemá. Ak by sme vrátili ich hodnotu tak, aká je, museli by sme potom v klientskom kóde ošetrovať tento prípad. Obalením do Javouvsej triedy Optional, presne určenej na tieto situácie, vieme vyjadriť absenciu hodnoty viacej objektovo. Čo to znamená v praxi si môžeme ukázať na nasledujúcej metóde value. Jej cieľom je vrátiť celé meno aj s titulmi v klasickom formáte:




 

 




public String value() {
    StringBuilder builder = new StringBuilder();

    getTitlesBeforeName().ifPresent(titles -> builder.append(titles).append(' '));
    builder.append(forename).append(' ').append(surname);
    getTitlesAfterName().ifPresent(titles -> builder.append(", ").append(titles));

    return builder.toString();
}

Pri postupnom vytváraní textu sme nemuseli použiť žiadne vetvenia cez podmienky. Jednoducho sme použili metódu Optional.ifPresent, ktorá zavolá danú lambda funkciu len v prípade, ak hodnota v inštancii Optional nie je null. To zlepšuje nielen čitateľnosť, ale lepšie vyjadruje našu myšlienku.

Zvyšok metód, ktoré treba implementovať, sme už rozoberali, preto k nim nie je nutný ďalší komentár:

@Override
public boolean equals(Object o) {
    if (this == o) {
        return true;
    }

    if (o == null || getClass() != o.getClass()) {
        return false;
    }

    Name other = (Name) o;
    return Objects.equals(titlesBeforeName, other.titlesBeforeName) &&
        forename.equals(other.forename) &&
        surname.equals(other.surname) &&
        Objects.equals(titlesAfterName, other.titlesAfterName);
}

@Override
public int hashCode() {
    return Objects.hash(getClass(), titlesBeforeName, forename, surname, titlesAfterName);
}

@Override
public String toString() {
    return "Name(" + value() + ")";
}

@Override
public int compareTo(Name other) {
    Objects.requireNonNull(other, "Cannot compare to null!");
    return getFullName().compareTo(other.getFullName());
}

# Best practices

  • Jeden value object má držať len jednu hodnotu, resp. jednu zloženú hodnotu rozdelenú na jej časti (napr. dátum rozdelený na rok, mesiac, deň, hodinu, minútu a sekundu)
  • Za názvom triedy je možné pridať všeobecnú koncovku, napr. Value, ValueObject alebo Property. Buďte však konzistentní - koncovku dávajte všade a tú istú, alebo nikde žiadnu.
  • Value Object musí byť nemenný (immutable)
  • Prepíšte metódy equals, hashCode a toString vlastnou implementáciou
  • Môžu implementovať niektoré pomocné rozhrania (Serializable, Comparable)

# Úlohy

# 1. Nahraďte triedu Age konkrétnejšiou BirthDate

Naša trieda Age je síce dobrá na vyjadrenie veku, ale v skutočnosti to nie je najlepšia reprezentácia tohto údaju. Vek je totižto len závislá premenná od dátumu narodenia. Vašou úlohou preto bude vytvoriť triedu BirthDate, ktorá bude spĺňať tieto podmienky:

  • Bude obaľovať Javouvskú reprezentáciu dátumu LocalDate
  • Bude mať metódu, ktorá vráti vek na základe aktuálneho dátumu (LocalDate.now())
  • Prepíšte metódy equals, hashCode a toString vlastnou implementáciou
  • Implementuje Serializable a Comparable

Keď ju budete mať hotovú, nahraďte ju v triede Person za aktuálny atribút Age.

Poznámka

Pôvodnú triedu Age môžete pokojne nechať nezmenenú, ak by ste sa do nej chceli neskôr pozrieť.

# 2. Vytvorte ďalšie vlastnostni pre triedu Person

Pre osobu už máme vytvorené meno a dátum narodenia, ale ešte nám zostáva pridať niekoľko ďalších údajov. Vytvorte Value Object triedy pre nasledovné dáta:

  • Rodné číslo (ID)
  • Pohlavie
  • Adresa

Postupujte podobne ako sme to robili v tejto kapitole. Ak je hodnota jednoduchá, nie je problém, ak je komplexná, mali by sme sa dostať aj k jej jednotlivým zložkám, nie len k celkovej.

# 3. Doplňte chýbajúce metódy a rozhrania pre triedu Person

Kapitolu zakončíme tým, že aj samotnú triedu Person premeníme na Value Object, a to tak, že:

  • Prepíšete metódy equals, hashCode a toString vlastnou implementáciou
  • Implementujete Serializable a Comparable

# Riešenia úloh

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

# Pozri tiež

# Diskusia