Cargo culty v Jave: objektovo-orientované programovanie

Zdeno Jašek

02.09.2020

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:

  1. Nesmie existovať objekt typu Person, ak nemá nastavený identifikátor a rodné číslo
  2. Dátum narodenia osoby musí zodpovedať nastavenému rodnému číslu
  3. Rodné číslo osoby sa nesmie zmeniť.
  4. 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:

  1. Trieda má obvykle konštruktor, ktorý definuje povinné atribúty, bez ktorých nemá zmysel inštanciu triedy vytvárať.
  2. 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.
  3. Trieda môže mať set-metódy doplnené o ďalšie vstupné parametre – napr. o používateľa, ktorý atribút zmenil.
  4. 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é.
  5. Niekedy majú get/set metódy v triede nastavenú nižšiu viditeľnosť, t.j. nie sú public.
  6. 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é.

Ďalšie blog posty, čo by ťa mohli zaujímať