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“.
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
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ží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:
Je 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:
Antipattern 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:
Uvedený 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:
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