Cargo cult
V južnom Pacifiku na ostrove Tanna žije primitívny kmeň, ktorý uctieva američana Johna Fruma. Aby ho k sebe privolali, postavili si lietadlá a obrovské vysielače. Vyzbrojili sa bajonetmi a pochodujú v šíkoch pod americkou zástavou. Presne takto to odpozorovali, keď bol John Frum na ostrove Tanna: stačí pochodovať s bajonetmi, otáčať s vysielačmi a mať pri sebe lietadlá. A stane sa zázrak! Pristane lietadlo naložené potravinami, oblečením a iným tovarom (cargo).
John však neprichádza. Možno preto, lebo lietadlá, vysielače a bajonety sú z bambusu a trávy. Takže členovia cargo cultu robia síce všetko správne, dokonale napodobňujú činnosť amerických jednotiek, ktoré kedysi na ostrove žili, ale výsledok sa nedostaví.
https://www.smithsonianmag.com/history/in-john-they-trust-109294882/
Mechanické napodobňovanie postupov bez porozumenia robí z programátorov členov cargo-cultu. Koľko bambusu a trávy obsahuje váš zdrojový kód?
Cargo cult objektovo-orientovaného programovania
„Každá trieda musí mať všetky svoje atribúty „private“ a k ním vygenerované get/set metódy.“
Dodržiavanie tejto zásady bez toho, aby programátori rozumeli jej dôvodom, robí z programátorov členov cargo cultu.
V niektorých situáciách je výhodnejšie používať dátové štruktúry namiesto tried. Pre ne však neplatia pravidlá o zapúzdrenosti atribútov v triedach.
Vysvetlením je objektovo-orientované programovanie
Základom objektovo-orientovaného prístupu je zodpovednosť. Aby trieda udržala zodpovednosť za svoje dáta, musí mať prístup k nim pod kontrolou – čiže dáta musia byť zapúzdrené. Preto je nutné definovať atribúty ako privátne.
Ak trieda poskytuje ku každému atribútu aj get/set metódy, k žiadnej zapúzdrenosti nedochádza. Samotná trieda potom nie je v zmysle dizajnu trieda, ale dátová štruktúra.
Trieda verzus dátová štruktúra
Trieda (class):
- primárne definovaná cez zodpovednosť
- obsahuje dáta, ale aj chovanie, ktorým implementuje zodpovednosť
Dátová štruktúra (record)
- primárne definovaná cez ukladané dáta
- obsahuje iba atribúty
Primárny rozdiel medzi triedou a dátovou štruktúrou spočíva v spôsobe návrhu. Ide o rozdiel, ktorý nemusí byť na prvý pohľad zrejmý. Mnohé triedy z objektovo-orientovaného dizajnu môžu v konečnom dôsledku skončiť s veľmi podobným zdrojovým kódom ako pri návrhu dátových štruktúr. Obvykle však ide o okrajové triedy – číselníky a pomocné pojmy.
Praktická ukážka – Osoba (Person)
Osoba má v doméne tieto atribúty:
- registrationNumber – rodné číslo
- birthDate – dátum narodenia
- givenName – krstné meno
- familyName – priezvisko
Pre účely identifikácie osoby slúži jednoznačný umelý identifikátor „personId“ typu Long.
Person ako dátová štruktúra (cargo cult)
import java.time.LocalDate;
public class Person {
private Long personId;
// rodne cislo
private String registrationNumber;
private LocalDate birthDate;
private String givenName;
private String familyName;
//getters
public Long getPersonId() { return personId; }
public String getRegistrationNumber() { return registrationNumber;}
public LocalDate getBirthDate() { return birthDate; }
public String getGivenName() { return givenName; }
public String getFamilyName() { return familyName; }
// setters
public void setBirthDate(LocalDate birthDate) { this.birthDate = birthDate;}
public void setGivenName(String givenName) { this.givenName = givenName;}
public void setFamilyName(String familyName) { this.familyName=familyName;}
public void setRegistrationNumber(String registrationNumber) {
this.registrationNumber = registrationNumber;
}
}
(Kód je zámerne stlačený kvôli priestoru.)
Dátová štruktúra má všetky atribúty prístupné cez get/set metódy. Kód obsahuje veľa nadbytočného balastu („boilerplate code“) – sú to práve get/set metódy automaticky generované nástrojom. Z pohľadu dizajnu je lepšie dátovú štruktúru v Jave písať naozaj ako dátovú štruktúru, t.j.:
import java.time.LocalDate;
public class Person {
public Long personId;
// rodne cislo
public String registrationNumber;
public LocalDate birthDate;
public String givenName;
public String familyName;
}
Herézia! Atribúty triedy nesmú byť „public“! Domorodci z ostrova Tannu sú zdesení!
Presne tak: atribúty triedy musia byť private. Ale ak z pohľadu dizajnu nie je Person navrhnutá ako trieda, ale ako dátová štruktúra, tak sa na ňu pravidlá pre triedy nevzťahujú. A z pohľadu zrozumiteľnosti je tento kratší zápis bez get/set metód lepšie čitateľný. Z pohľadu používania sa nijako nelíši od triedy Person, ktorá má síce všetky atribúty private, ale zároveň prístupné cez get/set metódy.
Person ako trieda
import java.time.LocalDate;
public final class Person {
private final PersonId personId;
// rodne cislo
private final RegistrationNumber registrationNumber;
private LocalDate birthDate;
private String givenName;
private String familyName;
public Person(PersonId personId, RegistrationNumber registrationNumber) {
this.personId = personId;
this.registrationNumber = registrationNumber;
this.birthDate = registrationNumber.date();
}
public PersonId getPersonId() { return personId; }
public String getRegistrationNumber() {
return registrationNumber.text();
}
public String getGivenName() { return givenName; }
public String getFamilyName() { return familyName; }
public LocalDate getBirthDate() { return birthDate; }
public void setGivenName(String givenName) { this.givenName = givenName;}
public void setFamilyName(String familyName) {this.familyName = familyName; }
}
Pri objektovo-orientovanom dizajne si dizajnér neustále kladie otázku: „Čo je zodpovednosťou danej triedy?“ Triedy, ktoré predstavujú objekty z reálneho sveta (ako je Person), obvykle majú jednoduchú zodpovednosť: zabezpečiť dátovú konzistenciu. Táto zodpovednosť je v prípade triedy Person rozbitá na toto chovanie:
- Nesmie existovať objekt typu Person, ak nemá nastavený identifikátor a rodné číslo
- Dátum narodenia osoby musí zodpovedať nastavenému rodnému číslu
- Rodné číslo osoby sa nesmie zmeniť.
- Z bodov 1-3 vyplýva, že ani dátum narodenia osoby sa nesmie zmeniť.
Rozbíjanie zodpovednosti v objektovo-orientovanom dizajne viedlo k definovaniu triedy RegistrationNumber, ktorá v doméne reprezentuje rodné číslo. Ako vidno z návrhu, má metódy date() a text() na vrátenie dátumu z rodného čísla.
Triedu Person by bolo možné navrhnúť tak ako v prípade dátovej štruktúry – čiže s atribútom registrationNumber typu String. Tým pádom by trieda Person na seba prevzala zodpovednosti súvisiace s vyhodnocovaním rodného čísla – t.j. načítanie dátumu.
Lenže: single responsibility principle sa v dobrom dizajne neporušuje!
Formálny pohľad na rozdiely
Hlavný rozdiel medzi dátovou štruktúrou a triedou spočíva v prístupe k ich dizajnu. Rozdielny prístup v dizajne však vedie aj k formálnym odlišnostiam:
- Trieda má obvykle konštruktor, ktorý definuje povinné atribúty, bez ktorých nemá zmysel inštanciu triedy vytvárať.
- Trieda obvykle nemá všetky set-metódy, lebo niektoré atribúty sa počas životného cyklu objektu nesmú zmeniť. Povinné atribúty sú častokrát nastavené iba raz na začiatku.
- Trieda môže mať set-metódy doplnené o ďalšie vstupné parametre – napr. o používateľa, ktorý atribút zmenil.
- Trieda častokrát nemá ani všetky get-metódy, lebo niektoré atribúty slúžia iba na interné spracovanie logiky vo vnútri triedy a nie sú publikované.
- Niekedy majú get/set metódy v triede nastavenú nižšiu viditeľnosť, t.j. nie sú public.
- Niekedy sú návratové typy get-metód iné než je interný typ daného atribútu v triede (pozri registrationNumber).
Pre get/set metódy teda platí, že zatiaľ čo pri dátových štruktúrach sú tieto metódy automaticky generované (resp. neexistujú), pri objektovom dizajne je každá get/set metóda dôsledkom požiadavky v doméne, t.j. dôsledkom implementácie use-case-u alebo user-story. A signatúra metódy nemusí mechanicky zodpovedať typu atribútu.
Dátové štruktúry
Dátové štruktúry majú v programovaní svoje miesto. Nemá zmysel programovať každú množinu atribútov automaticky ako triedu. Príkladom používania dátových štruktúr sú tzv. „Data Transfer Object“. Ide o dátové štruktúry, ktoré sú súčasťou API pri programovaní REST-ových alebo SOAP-ových služieb a ich účelom je zabezpečiť jednoduchú konverziu dát na JSON alebo XML. Tieto dátové štruktúry je vhodné implementovať pomocou public atribútov, čiže bez get/set metód.
Používate JavaBeans?
Písanie get/set metód zaviedol JavaBeans štandard, ktorý sa dnes už veľmi nepoužíva. Práve get/set metódy je jediné, čo z tohto štandardu zostalo. Štandard definoval pravidlá, ktoré má programátor dodržiavať, ak píše JavaBean. Dnes sa takmer všetky triedy automaticky píšu ako JavaBean-y bez toho, aby programátori tušili, že to robia.
Takže tu je ten bambus …
Sumár
Používanie get/set metód je najrozšírenejší cargo cult v Jave. Ich použitie samo o sebe nie je zlé ani chybné, pokiaľ programátori rozumejú jeho účelu:
- Pri triedach v zmysle objektovo-orientovaného dizajnu je každá get/set metóda dôsledkom používateľskej požiadavky. Tieto metódy preto nie je možné automaticky generovať pre všetky atribúty naraz.
- Pri dátových štruktúrach je prípustné a vhodné mať všetky atribúty verejne prístupné.