# POJO - Plain Old Java Object

Pri používaní rôznych frameworkov a vzorov môžeme zabudnúť na to, že občas všetko, čo potrebujeme, je jednoduchá trieda, ktorá slúži len ako dátová štruktúra. Také niečo voláme POJO - Plain Old Java Object, teda jednoduchý Java objekt.

POJO sú jednoduché triedy preto, lebo neobsahujú žiadnu logiku. Nerozširujú žiadnu inú triedu (resp., ak chceme byť korektný, implicitne rozširujú (dedia) len od Object), neimplementujú žiadne netriviálne rozhrania a nie sú ani nijako špeciálne anotované. POJO využívame najmä ak potrebujeme vytvoriť komplexnú dátovú štruktúru z jednoduchších. Podľa potreby ich môžeme vyžadovať hneď pri vytvorení objektu v konštruktore, alebo môžeme ponúknúť aj prázdny konštruktor a dovoliť hodnoty zmeniť/nastaviť neskôr. Tento prístup je výhodné použiť aj v prípade, kedy trieda / metóda potrebuje na svojom vstupe väčší počet vstupných parametrov (5+) - vtedy POJO tieto parametre obalí a pošlú sa tak metóde ako jeden celok. Dáta v POJO môžeme taktiež validovať, a teda garantovať pri prístupe ich integritu. Takáto validácia tvorí základ návrhového vzoru Value Object.

Napriek tomu, že tento vzor obsahuje slovo Java v názve, v žiadnom prípade nie je pre Javu exkluzívny. Niekedy sa dokonca J v názve prekladá ako JavaScript. Treba však mať na pamäti, že rôzne jazyky môžu mať svoj vlastný špecifický spôsob, ako ekvivalent POJO-a implementovať - napr. C# má struct, Scala case class a Kotlin data class.

# Modelová situácia

Nakoľko je POJO natoľko elementárny, že sa vyskytuje skoro všade, je ťažké ukázať všetky jeho použitia. Preto uvádzame len niektoré základné na jeho demonštráciu a pochopenie.

# Dátová štruktúra

POJO ako dátovú štruktúru demonštrujeme na tom najjednoduchšom príklade - vytvoríme triedu reprezentujúcu matematický bod v dvojrozmernom priestore. Ten sa skladá z dvoch zložiek - x-ovej a y-novej súradnice, ktoré môžeme reprezentovať primitívnym dátovým typom int. Zároveň, keďže z matematického hľadiska nemá zmysel bod po definovaní modifikovať, triedu spravíme nemennou (immutable). Ako sme sa v minulej kapitole dozvedeli, to zabezpečí, že súradnice po vytvorení inštancie triedy už nebude možné meniť:

public class Point {

    private final int x;
    private final int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }
}

Poznámka

V praxi by sme pravdepodobne chceli reprezentovať súradnice dátovým typom alebo štruktúrou, ktorá podporuje desatinné čísla. Avšak kvôli nepresnosti (resp. obmedzenej presnosti) float a double s nimi vznikajú problémy, ktoré sme pre jednoduchosť nechceli v tejto kapitole riešiť. Pokiaľ potrebujete presné rátanie na desatiné miesta (napr. keď pracujete s peniazmi), použite triedu BigDecimal.

Ak máme 2 body, ktoré majú zhodné súradnice, radi by sme povedali, že objekty sú zhodné. Neskúseného programátora by lákalo napísať niečo nasledovné:




 

Point a = new Point(1, 1);
Point b = new Point(1, 1);

a == b

Tento príklad sa však vždy vyhodnotí ako false, nakoľko operátor == pri objektoch skúma referenčnú rovnosť - inými slovami, či premenná a a b ukazujú na rovnaké miesto v pamäti. To však nechceme - my musíme skúmať štrukturálnu rovnosť. To sa dá docieliť pomocou metódy equals, dostupnej na každom objekte v Jave:




 

Point a = new Point(1, 1);
Point b = new Point(1, 1);

a.equals(b);

Poznámka

Alternatívne môžeme test rovnosti zapísať aj ako Objects.equal(a, b). To je vhodné najmä v prípadoch, kedy jedna z premenných a alebo b môže nadobúdať hodnotu null.

Ak to však spustíme, zistíme, že aj tento výraz sa vyhodnotí ako false. To je preto, lebo Java nevie, kedy sú inštancie našej vlastnej triedy Point rovné a kedy nie. Musíme ju to takpovediac naučiť. Z predchádzajúcej kapitoly už vieme, že to vieme docieliť vytvorením vlastnej equals metódy. Tentokrát necháme IDE vygenerovať equals za nás (komentáre sú naše):

@Override
public boolean equals(Object o) {
    // Is given object the same instance as this?
    if (this == o) {
        return true;
    }

    // Is given object instance of the same class?
    if (o == null || getClass() != o.getClass()) {
        return false;
    }
    
    // Are other Point's attributes the same?
    Point point = (Point) o;
    return x == point.x &&
            y == point.y;
}

Teraz nám už a.equals(b) vyhodnotí ako true. Preto nám napr. nasledujúci kód zbehne tak, ako by sme čakali:













 
 

> Point a = new Point(1, 1);
Defined field Point a = sk.tuke.fei.kpi.oop.chapters.structural.pojo.Point@589838eb

> Point b = new Point(1, 1);
Defined field Point b = sk.tuke.fei.kpi.oop.chapters.structural.pojo.Point@6500df86

> List<Point> list = new ArrayList<>();
Defined field List<Point> list = []

> list.add(a);
> list.remove(b);

> System.out.print(list.size());
0

Dôvod je ten, že metóda remove zmaže prvý prvok v liste, ktorý sa rovná poskytnutému parametru pri porovnaní cez equals. Existujú však dátové štruktúry, ktoré porovnávajú prvky na základe ich hašu (hash). Ak skúsime použiť našu triedu spolu s HashSet a HashMap:







 
 







 
 

> Set<Point> set = new HashSet<>();
Defined field Set<Point> set = []

> set.add(a);
> set.add(b);

> System.out.println(set.size());
2

> Map<Point, String> set = new HashMap<>();
Defined field Map<Point, String> set = {}

> map.put(a, "A");
> map.put(b, "B");

> System.out.println(map.size());
2

V oboch prípadoch by sme čakali, že množina aj mapa budú mať len po 1 prvku, no napriek tomu boli zapísané oba. Ako teda zaručíme správnu funkcionalitu? Našťastie, niet nič ľahšieho - prepíšeme metódu hashCode v triede Point:

@Override
public int hashCode() {
    return Objects.hash(x, y);
}

Objects.hash je zabudovaná statická metóda, ktorá akceptuje variabilný zoznam parametrov a z nich vygeneruje 1 spoločný haš. Ak by sme teda mali viacero premenných, napr. tretiu súradnicu y, stačilo by ju pridať do zoznamu parametrov ako Objects.hash(x, y, z). Teraz nám už po skompilovaní bude predchádzajúci príklad fungovať podľa očakávania:

> System.out.println(set.size());
1

> System.out.println(map.size());
1

Poznámka

Implementácia HashSet používa interne HashMap na ukladanie svojich prvkov. Tie sú vkladané ako kľúče spolu s maketou (dummy object), ktorý len signalizuje, že daná hodnota je v množine prítomná.

Čaká nás posledné vylepšenie. Teraz, ak skúsime v JShell definovať jednotlivé body a napr. aj list bodov, dostaneme nič nehovoriaci popis:

> Point a = new Point(1, 1);
Defined field Point a = sk.tuke.fei.kpi.oop.chapters.structural.pojo.Point@3e1

> Point b = new Point(2, 2);
Defined field Point b = sk.tuke.fei.kpi.oop.chapters.structural.pojo.Point@401

> Point c = new Point(3, 3);
Defined field Point c = sk.tuke.fei.kpi.oop.chapters.structural.pojo.Point@421

> List<Point> list = Arrays.asList(a, c, b);
Defined field List<Point> list = [sk.tuke.fei.kpi.oop.chapters.structural.pojo.Point@3e1, sk.tuke.fei.kpi.oop.chapters.structural.pojo.Point@421, sk.tuke.fei.kpi.oop.chapters.structural.pojo.Point@401]

Ako by sme docielili niečo nasledovné?

> System.out.println(new Point(10, 2));
(10; 2)

Ako sme sa v krátkosti dozvedeli už v prechádzajúcej kapitole, všetko, čo nám treba urobiť, je prepísať metódu toString v triede Point. IDE nám opäť ponúka automatické vygenerovanie, ktoré zahrnie všetky vnútorné premenné (resp. tie, ktoré si vyberieme):

@Override
public String toString() {
    return "Point{" +
            "x=" + x +
            ", y=" + y +
            '}';
}

new Point(10, 2) sa nám teraz vypíše ako Point{x=10, y=2}. Aj keď to nie je zlé, je to zbytočne dlhé a napr. pri listoch by to mohlo byť neprehľadné. Preto sa nesmieme báť a upraviť vygenerovaný kód podľa seba:

@Override
public String toString() {
    return "(" + x + "; " + y + ")";
}

Poznámka

V predchádzajúcej kapitole sme sa dozvedeli, že dátový typ String je nemenný (immutable), teda vyššie uvedený kód by mal vygenerovať 5 rôznych inštancií reťazca, pričom použitý by bol len posledný a zvyšné 4 by boli stratené a neskôr spracované garbage collector-om. Kompilátor však na také myslí a pred kompiláciou vymení kód za nasledujúci efektívnejší: new StringBuilder('(').append(x).append("; ").append(y).append(')').toString().

Keď si teraz spustíme ukážkový kód, JShell nám vypíše pekné a prehľadné hodnoty:

> Point a = new Point(1, 1);
Defined field Point a = (1; 1)

> Point b = new Point(2, 2);
Defined field Point b = (2; 2)

> Point c = new Point(3, 3);
Defined field Point c = (3; 3)

> List<Point> list = Arrays.asList(a, c, b);
Defined field List<Point> list = [(1; 1), (3; 3), (2; 2)]

# Zapuzdrenie parametrov metódy

Niekedy sa stáva, že metóda si vyžaduje veľké množstvo parametrov. Častokrát to znamená, že kód je pravdepodobne zlý a treba ho refaktorovať, no občas sa nájdu aj legitímne prípady, napr.:

List<User> searchUsers(String name, String surname, int ageFrom, int ageTo, String orderBy, boolean ascending)

Okrem toho, že signatúra je obrovská a v kóde potom nie je na prvý pohľad jasné, ktorý parameter má ktorú hodnotu, ak by ste chceli vyhľadávať len s jedným obmedzením, zvyšné atribúty by ste museli nastaviť na nejakú predvolenú hodnotu, napr.:

List<User> users = searchUsers(null, null, Integer.MIN_VALUE, 30, "age", true);

Nehovoriac o tom, že ak by ste chceli rozšíriť vyhľadávanie aj na ďalšie atribúty, museli by ste upravovať každé jedno volanie metódy v celom kóde. Aby sme tieto problémy vyriešili, vytvoríme si pomocnú POJO triedu s názvom SearchContext:

public class SearchContext {

    private static final String[] ORDERING_FIELDS = {
            "name", "surname", "age"
    };

    private String name = null;
    private String surname = null;
    private int ageFrom = Integer.MIN_VALUE;
    private int ageTo = Integer.MAX_VALUE;
    private String orderBy = null;
    private boolean ascending = true;

    public void setName(String name) {
        this.name = name != null ? name.trim() : null;
    }

    public void setSurname(String surname) {
        this.surname = surname != null ? surname.trim() : null;
    }

    public void setAgeFrom(int ageFrom) {
        this.ageFrom = ageFrom;
    }

    public void setAgeTo(int ageTo) {
        this.ageTo = ageTo;
    }

    public void setOrderBy(String orderBy) {
        // Chances are you would probably want to store
        // such a value using enum, but we wanted to
        // show how POJO can help you validate data
        if (orderBy != null && Arrays.stream(ORDERING_FIELDS).noneMatch(orderBy::equals)) {
            throw new IllegalArgumentException("Invalid ordering field!");
        }
        this.orderBy = orderBy;
    }

    public void setAscending(boolean ascending) {
        this.ascending = ascending;
    }

    // Getters ommited for brevity
}

Následne upravíme signatúru vyhľadávajúcej metódy:

List<User> searchUsers(SearchContext context)

Hotovo - metóda je sprehľadnená a práca s ňou je podstatne jednoduchšia a flexibilnejšia. Zároveň si všimnime niekoľko vecí:

  • Triedu sme tentokrát náročky nechali mennú (mutable), aby data mohli byť flexibilne nastavené podľa potreby aj na viacerých miestach. V ďalších kapitolách sa dozvieme, ako by sme vedeli podobnú triedu urobiť nemennú (immutable), no zároveň ju skonštruvať postupne (pozri kreačný návrhový vzor Builder).
  • Atribúty name a surname v setter-och automaticky ošetrujeme metódou trim, aby sme odstránili biele znaky (whitespace characters), ktoré pri vyhľadávaní nezavážia (predpokladáme, že sa tam vyskytli chybou používateľa pri zadávaní)
  • Atribút orderBy v setter-i overujeme, že je len z rozsahu platných hodnôt
  • Neprepísali sme equals, hashCode ani toString, nakoľko to pre nás nemá žiaden benefit

# Zoskupenie spoločných dát procesu

Podobne, ako pri metóde s veľa rôznymi parametrami, aj niektoré procesy sa skladajú z množstva menších dátových štruktúr, ktoré sa oplatí obaliť v jednej veľkej. Pekný príklad je HTTP žiadosť (HTTP request) na zobrazenie nejakej časti webovej stránky. Takáto žiadosť obsahuje nasledovné (ich podrobnejší význam budeme nateraz ignorovať):

  • Metóda (v našom prípade GET pre zobrazenie stránky)
  • URI (Unified resource identifier - jednotný identifikátor prostriedku; v našom prípade relatívna URL adresa pre danú stránku)
  • Verzia protokolu (dnes už štandardne len 1.1)
  • Hlavička a jej polia
  • Telo žiadosti (v našom prípade prázdne)

Okrem toho by sme v rámci žiadosti mohli držať aj nasledovné dáta:

  • Cookies (automaticky odosiela prehliadač)
  • Relácia používateľa (angl. session; na rozdiel od cookies je uložená na serveri)
  • Aktuálne prihlásený používateľ (vhodné najmä ak chceme získať chránené dáta konkrétneho používateľa)

Je evidentné, že všetko musíme držať v jednom objekte, ktorý vhodne nazveme Request:

public class Request {

    private final Method method;
    private final String uri;
    private final String protocolVersion;
    private final Map<String, String> headers;
    private final String body;

    // As cookies, session and user information
    // would be another complex objects, we are
    // leaving them out from this example

    public Request(Method method, String uri, String protocolVersion, Map<String, String> headers, String body) {
        this.method = Objects.requireNonNull(method);
        this.uri = Objects.requireNonNullElse(uri, "/");
        this.protocolVersion = Objects.requireNonNullElse(protocolVersion, "1.1");
        this.headers = Objects.requireNonNullElse(headers, new HashMap<>());
        this.body = body;
    }

    // Getters ommited for brevity

    public enum Method {
        GET, POST, PUT, DELETE, PATCH,
        HEAD, TRACE, OPTIONS, CONNECT
    }
}

Tento zvalidovaný objekt by sme následne obdržali v príslušnom kontroleri (vo webových frameworkoch je to miesto, kde sa HTTP požiadavky spracúvajú). Naše spracovanie by mohlo byť napr. nasledovné:

public class PageController {

    public String getPage(Request request) {
        String language = request.getHeaders().get("Accept-Language");
        String title = "sk".equalsIgnoreCase(language) ? "Ahojte!" : "Hello!";
        
        return String.format("<h1>%s</h1>", title);
    }
}

Poznámka

Ak by sme v Request objekte zahrnuli aj reláciu a aktuálne prihláseného používateľa, mohli by sme sa rozhodnúť neurobiť ich final a teda umožniť ich modifikovať aj po vytvorení objektu žiadosti. Bolo by to v poriadku - na jednej strane by sme mali k dispozícii stále zvladilované dáta, na druhej by sme využívali flexibilitu premenných pre atribúty, ktoré sa počas životného cyklu HTTP žiadosti môžu meniť.

# Best practices

  • POJO nesmie dediť od inej triedy, nesmie implementovať rozhranie (s výnimkou niektorých špeciálnych ako Comparable či Serializable) ani byť anotovaný
  • Atribúty sú privátne, prístup k nim je umožnený len cez getter-y a setter-y
  • Pokiaľ to nie je nevyhnutné, treba udržať triedu nemennú (immutable)
  • Ak je to možné, validovať hodnoty atribútov už pri ich nastavení a teda garantovať ich integritu
  • Je vhodné prepísať metódy equals, hashCode a toString vlastnými implementáciami (resp. nechať ich vygenerovať vývojovým prostredím)

# Úlohy

# 1. Vytvorte dátovú štruktúru pre kružnicu

Bod je primitívny objekt - čo však niečo trošku zložitejšie? Vašou úlohou bude vytvoriť triedu Circle - POJO pre kružnicu. O nej vieme, že má množstvo vlastností, no naozaj nám ich treba ukladať všetky? Váš vytvorený objekt by mal spĺňať nasledovné:

  • Vo svojej implementácii použite triedu Point
  • Trieda musí byť nemenná (immutable)
  • Prepíšte logiku metód equals, hashCode a toString
  • Z objektu kružnice musí byť možné získať nasledovné vlastnosti:
    • Stred
    • Priemer a polomer
    • Obvod
    • Povrch
  • Kružnice musia byť navzájom porovnávateľné vzhľadom na ich veľkosť, aby ich napr. bolo možné zoradiť v liste. Aby ste to docielili, vhodne implementujte rozhranie Comparable<T>.

# 2. Vytvorte dátovú štruktúru pre vektor

Vektor zdieľa veľa podobného s bodom. Oba sú definované dvojicou súradníc, majú rovnaký tvar pri výpise. Podobné je dokonca aj generovanie hašu či porovnávanie. Vytvorte triedu Vector, ktorá bude reprezentovať matematický vektor a bude dediť od Point, ktorý sme vytvorili v rámci kapitoly. Vo svojej implementácií zabezpečne nasledovné:

  • Vektor dedí od bodu (keďže aj bod je POJO, toto je prijateľné)
  • Musí zostať nemenný (immutable)
  • Musí podporovať nasledovné operácie:
    • Getter vlastnej veľkosti (magnitúdy)
    • Sčítanie 2 vektorov
    • Odpočítanie 2 vektorov
    • Násobenie skalárom (v našom prípade len celým číslom)
    • Skalárny súčin 2 vektorov
    • Vzdialenosť 2 vektorov
  • Musí byť možné skonštruovať vektor pomocou 2 bodov
  • Bod a vektor s rovnakými súradnicami nesmú byť metódou equals vyhodnotené ako rovnaké (v oboch smeroch, teda point.equals(vector) aj vector.equals(point)), takisto metóda hashCode musí v tomto prípade vrátiť rozdielny hash (musí platiť point.hashCode() != vector.hashCode())

Nebojte sa vhodne upraviť existujúcu implementáciu triedy Point, ak vám to pomôže.

# 3. Zoptimalizujte výpočty pomocných metód pre kružnicu a vektor

Aktuálne triedy Circle a Vector poskytujú niekoľko vlastností len tak, že ich vypočítajú z jednoduchších atribútov a výsledok vrátia. Ak by sme však o dané vlastnosti žiadali z rovnakého objektu veľakrát za sebou, zbytočne by sme ich museli zakaždým prepočítavať. Upravte tieto výpočty tak, aby sa po prvom výpočte uložili v privátnej premennej v danej triede a pri každom ďalšom volaní sa vracal výsledok z nej.

Aby ste udržali nemennosť (immutability) daných tried, uistite sa, že tieto premenné vyrovnávajúcej pamäte nastavujete naozaj len na daných miestach a že ich nie je možné meniť žiadnym iným spôsobom.

Poznámka

Takéto ukladanie výsledkov do vyrovnávacej pamäte je častokrát zbytočné a len zahlcuje kód. V praxi sa snažte tento prístup používať len vtedy, ak je to nevyhnutné a naozaj to prinesie želaný výsledok. Spravidla platí, že problémy s výkonom sa riešia až vtedy, keď sa vyskytnú, nie skôr.

# Riešenia úloh

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

# Pozri tiež

# Diskusia