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

Zdeno Jašek

13.08.2018

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

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