poznáme.itProgramovaniePoužívate dedičnosť v objektovom svete správne?

Používate dedičnosť v objektovom svete správne?

Používate dedičnosť v objektovom svete správne? 2

Dedičnosť v objektovom svete býva častokrát používaná nevhodne. Keďže návodov na správne použitie dedičnosti je veľa, tento článok uvádza návody na nesprávne použitie dedičnosti (antipatterny). Zároveň vysvetlí, prečo je použitie dedičnosti nesprávne a ako by sa v danom prípade malo postupovať.

Dedičnosť do istej miery porušuje základnú črtu objektovo orientovaného prístupu – zapúzdrenosť. Predstavuje preto najsilnejší typ väzby medzi dvomi triedami. Používa sa na modelovanie špecializácie alebo generalizácie. Ak trieda B dedí od triedy A, znamená to, že B je špeciálnym prípadom triedy A. Alebo že A je zovšeobecnením B. Trieda A sa potom nazýva „bázová trieda“.

Používate dedičnosť v objektovom svete správne? 4

V UML sa dedičnosť zakresľuje dutou šípkou, ktorá vedie od špecializovanej triedy k bázovej triede.

Antipattern 1: vynímanie pred zátvorku

Používate dedičnosť v objektovom svete správne? 6

Trieda „Bod“ reprezentuje bod v dvojrozmernom priestore s atribútmi x a y. Kružnica je dvojrozmerný útvar so stredom v bode [x,y] a s polomerom. Atribúty x a y sú teda spoločné, kružnica k nim pridáva svoje vlastné rozšírenie v podobe atribútu polomer. Spoločné atribúty zvádzajú k nesprávnemu použitiu dedičnosti.

Uvedený model tried tvrdí, že „Kružnica je špeciálnym prípadom bodu“, resp. že „Zovšeobecnením pojmu kružnica vznikne pojem bod“. Ani jedno tvrdenie nemá zmysel a preto je dedičnosť v tomto prípade nesprávna.

Správny model je asociácia:

Používate dedičnosť v objektovom svete správne? 8Používanie dedičnosti na „vyňatie pred zátvorku“ je najčastejším spôsobom chybného použitia dedičnosti. Preto je vhodné pri každom použití dedičnosti zvážiť, či nejde len o asociáciu tried.

Antipattern 2: zovšeobecnené pojmy mimo domény

Podobná chyba ako v predošlom príklade nastáva aj v tomto:

Používate dedičnosť v objektovom svete správne? 10Je pravda, že žiak aj učiteľ sú špeciálne prípady človeka a zároveň človek je zovšeobecnením pojmov žiak a učiteľ. Pojmy žiak a učiteľ môžu pochádzať napríklad z domény evidencie ľudí v škole. Atribúty triedy Človek hovoria o potrebe evidovať osobné údaje žiakov a učiteľov. V tomto prípade nastala chyba v analýze. Namiesto toho, aby analytik správne odhalil a pomenoval pojem „osobné údaje“, pomohol si nevhodným zovšeobecnením „človek“, ktoré stojí mimo analyzovanej domény. Ide o rovnaký prípad ako v predošlom antipatterne. Úlohou triedy Človek je iba „vyňať pred zátvorku“ spoločné atribúty tried Žiak a Učiteľ.

Správnym riešením je opäť asociácia, možno dokonca kompozícia:

Používate dedičnosť v objektovom svete správne? 12Antipattern 3: porušenie „Liskov substitution principle“

„Liskov substitution principle“ je jeden zo základných princípov objektovo orientovaného prístupu a súčasťou akronymu „SOLID“. Hovorí o možnosti nahradenia bázovej triedy svojou špecializáciou. Porušenie tohto kritéria demonštruje vzťah dedičnosti medzi obdĺžnikom a štvorcom:

Používate dedičnosť v objektovom svete správne? 14Uvedený model tvrdí, že „Štvorec je špeciálnym prípadom obdĺžnika“, alebo že „Obdĺžnik je zovšeobecnením pre štvorec“. Obe tvrdenia sú matematicky správne, napriek tomu je použitie dedičnosti aj v tomto prípade nesprávne. Dôvod je, že trieda Obdĺžnik nie je zameniteľná za triedu Štvorec, čo je podmienka dedičnosti vyžadovaná substitučným princípom. Túto skutočnosť odhalí implementácia.

/** Trieda reprezentuje obdĺžnik definovaný dĺžkami svojich strán. */
public class Obdlznik {
	private final int stranaA;
	private final int stranaB;
	
	public Obdlznik(int stranaA, int stranaB) {
		this.stranaA = stranaA;
		this.stranaB = stranaB;
	}

	/** Metóda vráti obsah obdĺžnika. */
	public int obsah() {
		return stranaA * stranaB;
	}

}

/** Trieda reprezentuje štvorec ako špeciálny prípad obdĺžnika. */
public class Stvorec extends Obdlznik {

	// CHYBA: Porušenie "Liskov substitution principle"
	public Stvorec( int stranaA ) {
		super( stranaA, stranaA );
	}
}

Unit test pre triedu Obdĺžnik testuje fungovanie metódy “obsah”:

@Test
public void testObsahObdlznikaSoStranami3a4je12() {
	Obdlznik obdlznik = new Obdlznik(3, 4);
	int obsah = obdlznik.obsah();
	
	assertEquals(12, obsah);
}

Liskov substitution principle hovorí o tom, že ak sa bázová trieda nahradí špecializáciou, chovanie musí ostať zachované. Avšak v uvedenom príklade začne unit test hlásiť chybu:

@Test
public void testObsahObdlznikaSoStranami3a4je12() {
	Obdlznik obdlznik = new Stvorec( 3 );
	int obsah = obdlznik.obsah();
	
	assertEquals(12, obsah); // chyba: obsah stvorca je 9
}

K príkladu by sa dalo namietať, že ak by vstupný parameter do konštruktora objektu Stvorec bolo číslo odmocnina z 12, unit test by fungoval. Hlavným prehreškom voči substitučnému princípu je však použitie jednoparametrického konštruktora new Stvorec( 3 ), namiesto požadovaného new Stvorec( 3, 4). Trieda Stvorec nedokáže zmysluplne implementovať konštruktor s dvomi rôznymi vstupnými dĺžkami strán, ktoré sú použité v konštruktore triedy Obdlznik.

Tento typ dedičnosti porušuje Liskov substitution principle. A tak aj napriek intuitívnej správnosti dedičnosti je aj v tomto prípade jej použitie nevhodné. Vhodnejšie by bolo vyviesť metódu „obsah“ do spoločného rozhrania, ktoré je oboma triedami implementované rôznym spôsobom:

Používate dedičnosť v objektovom svete správne? 16

Liskov substitution principle sa týka zdedených tried, nie implementovaných rozhraní. Takže v unit teste už nevzniká nárok, aby bola funkčnosť platná pre Obdlznik zachovaná aj pre Stvorec. Z pohľadu dizajnu ide o dve nezávislé a nezameniteľné triedy.

Záver

Začínajúci programátor či dizajnér by mal vždy veľmi dobre zvážiť, či je použitie dedičnosti na mieste a či by v danom prípade nebolo lepšie využiť asociáciu. Určite neurobí chybu, ak sa bude snažiť vyhýbať sa použitiu dedičnosti.

Príklady správneho použitia dedičnosti sú uvedené v návrhových vzoroch (design patterns). Správne použitie dedičnosti je zamerané na chovanie, nie na atribúty. Pri zameraní na chovanie je však obvykle lepšie využívať interface-y namiesto konkrétnych tried. Rozhodujúcou motiváciou pre použitie dedičnosti je snaha využiť polymorfizmus.

Odporúčané čítanie:

http://www.oodesign.com/liskov-s-substitution-principle.html

Dobrý článok? Chceš dostávať ďalšie?

Už viac ako 6 200 ITečkárov dostáva správy e-mailom. Nemusíš sa báť, nie každé ráno. Len občasne.

Súhlasím so spracovaním mojich osobných údajov. ( Viac informácií. )

Tvoj email neposkytneme 3tím stranám. Posielame naňho len informácie z robime.it. Kedykoľvek sa môžeš odhlásiť.

Zdeno Jašek
Zdeno Jašek
Pracujem ako Solution Architect vo firme PosAm a programovaním sa zaoberám takmer 30 rokov. Prešiel som jazykmi Basic, Assembler, Pascal, Object Pascal, Lisp, Prolog, Magic, MUMPS, Clipper, Paradox a Java, z ktorých najmilšia mi je Java. Pracoval som hlavne ako softvérový architekt, ale aj ako programátor, analytik, dizajnér a projektový manažér. Pri vývoji softvéru sa mi najviac páči navrhovanie objektového dizajnu – obzvlášť pre zložité aplikácie. Svoje blogy chcem zamerať na postupy pri vytváraní objektového návrhu aplikácie a ich technologickej realizácii v podobe hexagonálnej architektúry a microservices.

Čítaj ďalej: