Dedenie I.

Accendo > Publikované > OOP 3

Autor:                  Libor Bešenyi

Dátum:                2011/01/02

Zmena:                                2011/01/02

 

                Bez toho, aby sme mohli pokračovať v syntaktickej analýze asi sa nevyhneme dedeniu (aj keď som sa mu chcel venovať neskôr). Dedenie je považované veľkou skupinou programátorov za najlepšiu vlastnosť OOP a ja tu s kolegami nemôžem súhlasiť. Dedenie je fajn, ale môže byť aj cestou do pekla. Ja si myslím, že je preceňovaná tato vlastnosť kvôli tomu, že ostatné vlastnosti sú možno trocha komplikovanejšie a tak im mnohí venujú menej času (a námahy) – pričom robia chybu!

                O čom teda to dedenie je? Trieda dokáže zdieľať premenné, metódy a vlastne všetky časti. Výhodou je, že ak identifikujeme tieto často sa opakujúce časti, ušetríme si času ich písaním, ale taktiež ak chybu odstránime na najnižšej úrovni, do odvodených tried nemusíme zasahovať a opravovať všetko duplicitne.

                S otázkou duplicity musí byť ale každý vysporiadaný. Duplicita je síce viacej práce, ale niekedy sa jej nevyhneme pri komplikovaných systémoch, kde je lepšie existujúci kód skopírovať a nové časti doimplementovať ako do novej triedy (nezávisle na pôvodnej triede). Takto sa môžeme vyhnúť kritickým častiam systému, pretože rovnako ako opravenie chyby sa prenesie do všetkých tried, zavedenie novej chyby môže spôsobiť kolaps vo všetkých triedach čo ju dedia. V prekomplikovaných systémoch je niekedy ťažké urobiť analýzu dopadu zásahu do triedy a aj sa môže ťažko zmena testovať. Príkladom môže byť kód kompilovaný pre viacero platforiem (napr. COM Delphi, okresaný .Net 1 v PocketPC a webový server na .Net 3.5). Každá platforma predstavuje vlastné pravidlá a úprava nejakých tried na „nízkej“ úrovni môže spôsobiť chybu až pri preklade COM verzia (vynútený preklad) alebo logické chyby za chodu programov.

Takéto systémy zvyknút mať priveľa tried a výnimiek pre „daný“ prípad čo hovorí o tom, že trieda nebola vhodne navrhnutá a zmeny za tie roky z kódu urobili kĺbko niečoho, do čoho sa boja členovia tímu zasahovať, lebo vlastne už nikto nevie ako to funguje. Preto prosím, stále uvažujte, či výhody dedenia prevažujú v jednotlivých prípadoch, ak z OOP chceme využiť len túto vlastnosť. V časti polymorfizmus sa budeme ešte venovať príkladu nevhodnej implementácie hierarchy triedy.

Ak chceme nejakú triedu odvodiť od inej, musíme ju zadefinovať takto:

// *************************************************************************

// *** ZakladnaTrieda ******************************************************

// *************************************************************************

public class ZakladnaTrieda

{

// ---------------------------------------------------------------------

public string Text;

} // public class ZakladnaTrieda

 

// *************************************************************************

// *** OdvodenaTrieda ******************************************************

// *************************************************************************

public class OdvodenaTrieda : ZakladnaTrieda

{

} // public class OdvodenaTrieda

 

                Druhá, odvodená trieda zadefinovala svoju pôvodnú triedu ako ZakladnaTrieda. Ak teraz vytvoríme inštanciu triedy OdvodenaTrieda, napriek tomu, že tá nedefinuje premennú Text, ju bude mať dostupnú!

// ---------------------------------------------------------------------

private void button1_Click(object sender, EventArgs e)

{

OdvodenaTrieda trieda = new OdvodenaTrieda();

trieda.Text = "trieda"; // Text premenna je dostupna

}

 

                Že to nie je nič svetoborné? Tiež si myslím :) Ako by nám to pomohlo? Vhodným príkladom dedenia sú komponenty VLC z Delphi (kde sú ľahko dostupné zdrojové kódy čo neuveriteľne pomáha pri vývoji – že Microsoft – aj keď sa dajú kódy stiahnúť sa mi to radšej ani nechce používať, kým nemusím).

                No o čo teda pri komponentoch ide? Každý objekt, ktorý môžeme vidiet na formulári má určite vlastnosti, ktoré sa stále opakujú. Tých objektov je neuveriteľne veľa (textbox, combobox, listbox, label, checkbox, radiobutton...) a vlastnosti ako je napríklad pozícia na formulári je spoločnou časťou pre každý objekt. My si tieto vlastnosti predstavíme na podovnom príklade a to je generovanie HTML kódu.

                Pred tým by som bol ale rád povedať, že dedenie a base triedy (predkovia) vytvárajú v konečnom dôsledku jedinú triedu (tú najvyššiu) a vlastnosti sú akoby prekryté na fólií. Ak by sme každú triedu napísali na priesvitnú fóliu a od nej odvodenú triedu na inú fóliu, výsledná trieda bude vlastne obraz vzniknutý položením fólií na seba! Dedením teda triedu skladáme, nemá to nič spoločné s run-time výsledkom (len to pomáha programátorovi).

                Pozrime sa teda ako by sme mohli náš generátor HTML kódu rozanalyzovať. Vieme, že existujú dva typy elementov, blokové a neblokové. Príklady:

<blokovy></blokovy>

<neblokovy />

 

                HTML generátor bude mať v našom prípade jednoduchú funcionality (kvôli demonštrácii jednotlivých praktík). Dedenie by sme teda mohli navrhnúť tak, že bude existovať spoločná trieda HtmlElement a odvodené triedy HtmlElementBlokovy a HtmlElementNeblokovy. Od tíchto trieda budeme dediť reálne Elementy HtmlDiv, HtmlSpan, HtmlTable, HtmlInput, ... takže hierarchia by mohla vyzerať takto:

                Tak a teraz sa musíme zamyslieť, či má význam mať tak rozsiahlu vrstvu dedenia. Ak by sme vynechali HtmlElement museli by sme editovať všetko dva krát pre elementy blokové aj neblokové (názov, štýl, ...) taktiež z hľadiska polymorfizmu by sme mali problém (vysvetlíme neskôr). Tak čo tak zrušiť druhú vrstvu a len predefinovať či je element blokový ako bool JeBlokovyElement. Tu by sme však narazili na inú nezrovnalosť, blokové elementy môžu obsahovať iné elementy a potom by sme museli riešiť výnimky ak by sme v kód chceli pridať element do neblokového. Takéto podmienky sú väčšinou predzvesťou chybnej architekúry tried. Na druhej strane zatiaľ sa nám javí posledná vrstva ako zbytočná duplicita, pretože jediné čo by tieto objekty definovali je názov. Takže zatiaľ zrušíme poslednú vrstvu a pridáme položku názov, do zákaldnej triedy, ktorú zdedíme:

// *************************************************************************

// *** HtmlElement *********************************************************

// *************************************************************************

public class HtmlElement

{

// ---------------------------------------------------------------------

public string Nazov;

} // public class HtmlElement

 

// *************************************************************************

// *** HtmlElementBlokovy **************************************************

// *************************************************************************

public class HtmlElementBlokovy : HtmlElement

{

} // public class HtmlElementBlokovy

 

// *************************************************************************

// *** HtmlElementNeblokovy ************************************************

// *************************************************************************

public class HtmlElementNeblokovy : HtmlElement

{

} // public class HtmlElementNeblokovy

 

                Aby sme ešte mohli vidieť dedenie kódu, zadefinujme aj metódu Generuj(), ktorá vytvorí kód podľa názvu (keďže obe triedy definujú rôzne vykreslenie tak musíme triedu zadefinovať dva krát):

// *************************************************************************

// *** HtmlElementBlokovy **************************************************

// *************************************************************************

public class HtmlElementBlokovy : HtmlElement

{

// ---------------------------------------------------------------------

// --- HtmlElementBlokovy Metody ---------------------------------------

// ---------------------------------------------------------------------

public string Generuj()

{

return string.Format(@"<{0}></{0}>", Nazov);

}

} // public class HtmlElementBlokovy

 

// *************************************************************************

// *** HtmlElementNeblokovy ************************************************

// *************************************************************************

public class HtmlElementNeblokovy : HtmlElement

{

// ---------------------------------------------------------------------

// --- HtmlElementBlokovy Metody ---------------------------------------

// ---------------------------------------------------------------------

public string Generuj()

{

return string.Format(@"<{0} />", Nazov);

}

} // public class HtmlElementNeblokovy

 

                Na otestovanie použije demo aplikáciu s talačidlom a textboxom (nastavený multiline, zakázaný wordwrap a vynútený scrollbar – samozrejme to nie je podstatné):

// ---------------------------------------------------------------------

private void button1_Click(object sender, EventArgs e)

{

HtmlElementBlokovy div = new HtmlElementBlokovy();

div.Nazov = "div";

textBox1.Text += div.Generuj() + Environment.NewLine;

 

HtmlElementNeblokovy span = new HtmlElementNeblokovy();

span.Nazov = "span";

textBox1.Text += span.Generuj() + Environment.NewLine;

}

 

                Výsledok je zrejmí:

<div></div>

<span />

 

                Tu narážeme na prvú nevýhodu nášho návrhu. Je v plnej moci ako objekt použije programátor. Ak zadefinuje span ako blokový element, aplikácia nevie, že sa jedná o chybu. Ak by sme nechali spodnú vrstvu, museli by sme zadefinovať všetky HTML elementy, ale programátor využívajúci náš kód by už takýto omyl (úmyselný alebo nevedomý) neurobil. Toto je samozrejme vecou priorít a záleží táto dilema projekt od projektu na človeku, ktorý dané riešenie navrhuje.

Konštruktor a trieda Object

                Keď vytvárame                inštanciu triedy, napíšeme zázračné slovíčko „new“. Čo to vlastne znamená? Jedná sa o špeciálnu metódu, ktorá ma charakter statickej (pretože ju voláme bez toho, aby inštancia vznikla – viď kapitolu s volaním členov triedy ak neexistuje). Kde však táto metóda je?

// *************************************************************************

// *** Trieda **************************************************************

// *************************************************************************

public class Trieda

{

} // public class Trieda

 

Každá C# trieda je automaticky (ak nebolo definované ináč) odvodená od triedy „Object“ alebo aj „object“ (a keďže jedna z tried musí byť „prvou“ tak vlastne všetky triedy majú na najnižšej úrovni triedu object). Dokonca aj ordinálne typy ako string, int su odvodené od tejto triedy – len sa asi z historických dôvodov správajú ináč ako objekt – nie sú definované smerníkom, ale odkazujú priamo na hodnotu – pozri kapitolu smerník na smerník. Teda jednoducho povedané, že tieto triedy sú absolútne totožné (len iný zápis toho istého výstupu):

// *************************************************************************

// *** Trieda **************************************************************

// *************************************************************************

public class Trieda : object

{

} // public class Trieda

 

                To, že nemusíme pisať object je len vlastnosťou prekladača – ten ho pri prekladaní za nás automaticky dopĺni. Už vieme ako dokážeme dediť jednotlivé veci medzi triedami, tak čo pre nás ukrýva object trieda? Postavme kurzor na „object“ a stlačme F12, prenesieme sa do definicie triedy (jednodušene):

 

public class Object

{

      public Object();

public virtual bool Equals(object obj);

public static bool Equals(object objA, object objB);

public virtual int GetHashCode();

public Type GetType();

protected object MemberwiseClone();

public static bool ReferenceEquals(object objA, object objB);

public virtual string ToString();

}

 

A tu je náš skrytý konštruktor. Aj keď má iný názov je to kvôli tomu, že triedy musia mať odlišné názvy a práve názov triedy je vyhradený konštruktorom. Na vplyv tohto tzv. defaultného konštruktora vplyv nemáme, ale vieme si ukázať, ako sa správa defaultný konštruktor v tejto hierarchii:

 

// *************************************************************************

// *** Trieda **************************************************************

// *************************************************************************

public class Trieda

{

// ---------------------------------------------------------------------

// --- Trieda Konstruktor ----------------------------------------------

// ---------------------------------------------------------------------

public Trieda()

{

MessageBox.Show("Trieda");

}

} // public class Trieda

 

// *************************************************************************

// *** TriedaB *************************************************************

// *************************************************************************

public class TriedaB : Trieda

{

} // public class TriedaB

 

// *************************************************************************

// *** TriedaC *************************************************************

// *************************************************************************

public class TriedaC : TriedaB

{

// ---------------------------------------------------------------------

// --- TriedaC Konstruktor ----------------------------------------------

// ---------------------------------------------------------------------

public TriedaC()

{

MessageBox.Show("TriedaC");

}

} // public class TriedaC

 

TriedaC trieda = new TriedaC();

 

                Ak teda vytvoríme podobné konštruktory a ich dedíme, volajú sa automaticky v hierarchickom poradi: Object(), Trieda(), TriedaC(). Ak v nejakom medzi článku nevyužijeme konštruktor, to neznamená, že sme ho zakázali! Toto môže trocha pliesť, pretože táto vlastnosť sa týka jedine defaulntých konštruktorov (teda tých bez parametrov). Ak ho nepredpisujú aj zdedené triedy, tak proste zaniká a už nie je možné ho ďalej volať z vyšších tried a zasa opačne, nie je možné vyhnúť sa volaniu defaultného konštruktora. Viac o preťažovaní konštruktorov si povieme možno neskôr, teraz to nie je vôbec dôležité – pretože dané správanie nám vie pripomenú aj prekladač.

                Načo sú dobre konštruktory? Vedia inicializovať komplikovanejšie hodnoty, môžu vykonať nejakú inicializačnú rutinu a predpisujú akési „rozhranie“ triedy. Ak pracujeme v tíme, vieme konštruktorom povedať „hej, daj si pozor na toto!“. Vo všeobecnosti platí, že konštruktor by nemal byť zložitý – zložitosť priťahuje chyby a výnimka v konštruktore by nemala nastať (viac si povieme v kapitole s pamäťovými leakmi).

                Ak by sme do naších HTML tried doplnili ďalšie vlastnosti, ktoré majú spoločné Html elementy ako ID, CSS, tak programátor by nemusel vedieť (ak by ich bolo priveľa), čo je pre triedu dôležité. Samozrejme názov je hádam najdôležitejší, tak ho umiestnime napr. do konštruktora:


    // *************************************************************************

// *** HtmlElement *********************************************************

// *************************************************************************

public class HtmlElement

{

// ---------------------------------------------------------------------

public string Nazov;

 

// ---------------------------------------------------------------------

// --- HtmlElement Konstruktor -----------------------------------------

// ---------------------------------------------------------------------

public HtmlElement(string nazov)

{

Nazov = nazov;

}

} // public class HtmlElement

 

                Ak sa snažíme použiť nový konštruktor v aplikácii, tam to nefunguje:

 

// ---------------------------------------------------------------------

private void button1_Click(object sender, EventArgs e)

{

HtmlElementBlokovy div = new HtmlElementBlokovy("div");

textBox1.Text += div.Generuj() + Environment.NewLine;

 

HtmlElementNeblokovy span = new HtmlElementNeblokovy("span");

textBox1.Text += span.Generuj() + Environment.NewLine;

}

 

                Správa nám hovorí, že konštruktor neexistuje! A to je presne to, o čom bol predchádzajúci odsek. Akonáhle pridáme parametre konštruktoru, už nie je defaultny a teda sa nededí implicitne. Preto ho musíme zadefinovať aj v horných triedach:

                Ak pridáme konštruktor:

 

// ---------------------------------------------------------------------

// --- HtmlElementBlokovy Konstruktor ----------------------------------

 // ---------------------------------------------------------------------

public HtmlElementBlokovy(string nazov)

{

Nazov = nazov;

}

 

                Trieda je nepreložiteľná, pretože kompilátor zistil, že už daný konšturktor sa v hierarchii vyskytuje. Musíme tieto dva konštruktory „spárovať“. To sa robí tak, že sa zavolá pôvodný konštruktor cez kľúčové slovo base:

 

// ---------------------------------------------------------------------

// --- HtmlElementBlokovy Konstruktor ----------------------------------

// ---------------------------------------------------------------------

public HtmlElementBlokovy(string nazov)

: base(nazov)

{

Nazov = nazov;

}

 

                Tu vieme použiť parametre z definície, alebo ich vieme vynútiť (napr. „div“) a tak by sme vedeli zavolať nedefaultný konštruktor z defaultného (nevoláme parameter):

 

HtmlElementBlokovy div = new HtmlElementBlokovy();

textBox1.Text += div.Generuj() + Environment.NewLine;

 

                Lebo sa parameter vyunúti už v triede:

 

// ---------------------------------------------------------------------

// --- HtmlElementBlokovy Konstruktor ----------------------------------

// ---------------------------------------------------------------------

public HtmlElementNeblokovy()

: base("div")

{

}

 

                Tento prípad ale tentoraz nevyužijeme a len proste pošleme do predka aj parameter, ktorý príde. Otázka teraz je, či použiť aj konštruktor v predkovy alebo len definovať dva konštruktory na úrovní „výkonných“ trieda. Ťažko povedať... ak máme definovaný parameter v predkovy tak asi aj jeho nastavenie by malo byť aj tam. Taktiež toto rozhranie napovedá o charaktere triedy HtmlElement – tá si vynucuje názov, ostatné triedy by to „nemalo“ trápiť. Takže výsledný kód môže vyzerať takto:

 

// *************************************************************************

// *** HtmlElement *********************************************************

// *************************************************************************

public class HtmlElement

{

// ---------------------------------------------------------------------

public string Nazov;

public string Id;

public string Css;

 

// ---------------------------------------------------------------------

// --- HtmlElement Konstruktor -----------------------------------------

// ---------------------------------------------------------------------

public HtmlElement(string nazov)

{

Nazov = nazov;

}

} // public class HtmlElement

 

// *************************************************************************

// *** HtmlElementBlokovy **************************************************

// *************************************************************************

public class HtmlElementBlokovy : HtmlElement

{

// ---------------------------------------------------------------------

// --- HtmlElementBlokovy Konstruktor ----------------------------------

// ---------------------------------------------------------------------

public HtmlElementBlokovy(string nazov)

: base(nazov)

{

}

 

// ---------------------------------------------------------------------

// --- HtmlElementBlokovy Metody ---------------------------------------

// ---------------------------------------------------------------------

public string Generuj()

{

return string.Format(@"<{0}></{0}>", Nazov);

}

} // public class HtmlElementBlokovy

 

// *************************************************************************

// *** HtmlElementNeblokovy ************************************************

// *************************************************************************

public class HtmlElementNeblokovy : HtmlElement

{

// ---------------------------------------------------------------------

// --- HtmlElementBlokovy Konstruktor ----------------------------------

// ---------------------------------------------------------------------

public HtmlElementNeblokovy(string nazov)

: base(nazov)

{

}

 

// ---------------------------------------------------------------------

// --- HtmlElementBlokovy Metody ---------------------------------------

// ---------------------------------------------------------------------

public string Generuj()

{

return string.Format(@"<{0} />", Nazov);

}

} // public class HtmlElementNeblokovy

 

       A použitie:

 

// ---------------------------------------------------------------------

private void button1_Click(object sender, EventArgs e)

{

HtmlElementBlokovy div = new HtmlElementBlokovy("div");

textBox1.Text += div.Generuj() + Environment.NewLine;

 

HtmlElementNeblokovy span = new HtmlElementNeblokovy("span");

textBox1.Text += span.Generuj() + Environment.NewLine;

}