7. diel - Dedičnosť a polymorfizmus
V minulej lekcii, C# - Aréna s bojovníkmi, sme dokončili našu arénu, simulujúcu zápas dvoch bojovníkov.
Dnes si opäť rozšírime znalosti o objektovo orientovanom programovaní. V
úvodnej lekcii do OOP sme si hovorili, že OOP stojí na troch základných
pilieroch: zapuzdrenie, dedičnosti a
polymorfizmu. Zapúzdrenie a používanie modifikátora
private
nám je už dobre známe. Dnes sa pozrieme na zvyšné dva
piliere.
Dedičnosť
Dedičnosť je jedna zo základných vlastností OOP a slúži na tvorenie nových dátových štruktúr na základe starých. Vysvetlime si to na jednoduchom príklade:
Budeme programovať informačný systém. To je celkom reálny príklad, aby
sme si však učenie spríjemnili, bude to informačný systém pre správu
zvierat v ZOO Náš systém
budú používať dva typy užívateľov: používateľ a administrátor.
Užívateľ je bežný ošetrovateľ zvierat, ktorý bude môcť upravovať
informácie o zvieratách, napr. ich váhu alebo rozpätie krídiel.
Administrátor bude môcť tiež upravovať údaje o zvieratách a navyše
zvieratá pridávať a mazať z databázy. Z atribútov bude mať navyše
telefónne číslo, aby ho bolo možné kontaktovať v prípade výpadku
systému. Bolo by určite zbytočné a neprehľadné, keby sme si museli
definovať obe triedy úplne celé, pretože mnoho vlastností týchto dvoch
objektov je spoločných. Užívateľ aj administrátor budú mať určite meno,
vek a budú sa môcť prihlásiť a odhlásiť. Nadefinujeme si teda iba triedu
User
(nepôjde o funkčnú ukážku, dnes to bude len teória,
programovať budeme nabudúce):
class User { private string name; private string password; private int age; public bool LogIn(string password) { // ... } public bool LogOut() { // ... } public void SetWeight(Animal animal) { // ... } // ... }
Triedu som len naznačil, ale iste si ju dokážeme dobre predstaviť. Bez
znalosti dedičnosti by sme triedu Admin
definovali asi takto:
class Admin { private string name; private string password; private int age; private string phoneNumber; public bool LogIn(string password) { // ... } public bool LogOut() { // ... } public void SetWeight(Animal animal) { // ... } public void AddAnimal(Animal animal) { } public void RemoveAnimal(Animal animal) { } // ... }
Vidíme, že máme v triede množstvo redundantného (duplikovaného) kódu.
Akékoľvek zmeny musíme teraz vykonávať v oboch triedach, kód sa nám
veľmi komplikuje. Namiesto toho použijeme dedičnosť. Definujeme teda triedu
Admin
tak, aby dedila z triedy User
. Atribúty a
metódy užívateľa teda už nemusíme znovu definovať, jazyk C# nám ich do
triedy sám dodá:
class Admin: User { private string phoneNumber; public void AddAnimal(Animal animal) { } public void RemoveAnimal(Animal animal) { } // ... }
Vidíme, že na zdedenie sme použili operátor :
. V anglickej
literatúre nájdete dedičnosť pod slovom inheritance.
V príklade vyššie nebudú v potomkovi prístupné privátne atribúty, ale
iba atribúty a metódy s modifikátorom public
. Atribúty a
metódy s modifikátorom private
sú chápané ako špeciálna
logika konkrétnej triedy, ktorá je potomkovi utajená, aj keď ju vlastne
používa, nemôže ju meniť. Aby sme dosiahli požadovaný výsledok,
použijeme nový modifikátor prístupu protected
,
ktorý funguje rovnako, ako private
, ale dovoľuje tieto atribúty
dediť. Začiatok triedy User
by teda vyzeral takto:
class User { protected string name; protected string password; protected int age; // ...
Keď si teraz vytvoríme inštancie užívateľa a administrátora, obaja
budú mať napr. atribút name
a metódu LogIn()
. C#
triedu User
zdedí a doplní nám automaticky všetky jej
atribúty.
Výhody dedenia sú jasné, nemusíme opisovať obom triedam tie isté atribúty, ale stačí dopísať len to, v čom sa líšia. Zvyšok sa zdedí. Prínos je obrovský, môžeme rozširovať existujúce komponenty o nové metódy a tým ich znovu využívať. Nemusíme písať množstvo redundantného (duplikovaného) kódu. A hlavne - keď zmeníme jediný atribút v materskej triede, automaticky sa táto zmena všade zdedí. Nedôjde teda k tomu, že by sme to museli meniť ručne v 20 triedach a niekde na to zabudli a spôsobili chybu. Sme ľudia a chybovať budeme vždy, musíme teda používať také programátorské postupy, aby sme mali možnosť chybovať čo najmenej.
O materskej triede sa niekedy hovorí ako o predkovi (tu User
) a
o triede, ktorá z nej dedí, ako o potomkovi (tu Admin
). Potomok
môže pridávať nové metódy alebo si prispôsobovať metódy z materskej
triedy (pozri ďalej). Môžete sa stretnúť aj s pojmami nadtrieda a
podtrieda.
Ďalšou možnosťou, ako objektový model navrhnúť, by
bolo zaviesť materskú triedu User
, ktorá by slúžila iba na
dedenie. Z triedy User
by potom dedila ako trieda
Attendant
, tak Admin
. To by sa však vyplatilo až pri
väčšom počte typov užívateľov. V takom prípade hovoríme o hierarchii
tried, budeme sa tým zaoberať ku koncu tejto sekcie. Náš príklad bol
jednoduchý a preto nám stačili iba dve triedy. Existujú tzv.
návrhové vzory, ktoré obsahujú osvedčené schémy
objektových štruktúr pre známe prípady použitia. Záujemcovia ich nájdu
popísané v sekcii Návrhové vzory. Je to
veľmi zaujímavá, avšak už pokročilejšia problematika. V objektovom modelovaní sa dedičnosť znázorňuje graficky
ako prázdna šípka smerujúca k predkovi. V našom prípade by grafická
notácia vyzerala takto:
Dátový typ pri dedičnosti
Obrovskou výhodou dedičnosti je, že keď si vytvoríme premennú s
dátovým typom materskej triedy, môžeme do nej bez problémov ukladať aj jej
potomkov. Je to dané tým, že potomok obsahuje všetko, čo obsahuje
materská trieda, spĺňa teda "požiadavky" (presnejšie obsahuje rozhranie)
dátového typu. A k tomu má oproti materskej triede niečo navyše. Môžeme
si teda urobiť pole typu User
a v ňom mať ako používateľov,
tak administrátorov. S premennou to teda funguje takto:
User user = new User("John Newman", 33); Admin admin = new Admin("Jack White", 25); // Now we assign the administrator to the variable of the User type: user = admin; // It's fine since the User class is its parent class // If we try in conversely, we'll cause an error: admin = user;
V C# je mnoho konštrukcií, ako operovať s typmi inštancií pri dedičnosti. Podrobne sa na ne pozrieme počas kurzu, teraz si ukážme len to, ako môžeme overiť typ inštancie v premennej:
User user = new Admin("Jack White", 25); if (user is Admin) Console.WriteLine("The user given has been identified as an administrator"); else Console.WriteLine("The user given is not an administrator");
Pomocou operátora is
sa môžeme opýtať, či je objekt
daného typu. Kód vyššie otestuje, či je v premennej user
užívateľ alebo jeho potomok administrátor.
Jazyky, ktoré dedičnosť podporujú, buď vedia dedičnosť jednoduchú, kde trieda dedí len z jednej triedy, alebo viacnásobnú, kde trieda dedí hneď z niekoľkých tried naraz. Viacnásobná dedičnosť sa v praxi príliš neosvedčila. Časom si povieme prečo a ukážeme si i to, aj ako ju obísť. C# podporuje iba jednoduchú dedičnosť, s viacnásobnou dedičnosťou sa môžete stretnúť napr. v C++.
Polymorfizmus
Nenechajte sa vystrašiť príšerným názvom tejto techniky, pretože je v
jadre veľmi jednoduchá. Polymorfizmus umožňuje používať jednotné
rozhranie na prácu s rôznymi typmi objektov. Majme napríklad mnoho objektov,
ktoré reprezentujú nejaké geometrické útvary (kruh, štvorec,
trojuholník). Bolo by určite prínosné a prehľadné, keby sme s nimi mohli
komunikovať jednotne, hoci sa líšia. Môžeme zaviesť triedu
GeometricShape
, ktorá by obsahovala atribút color
a
metódu Render()
. Všetky geometrické tvary by potom dedili z
tejto triedy jej interface (rozhranie). Objekty kruh a štvorec sa ale iste
vykresľujú inak. Polymorfizmus nám umožňuje prepísať si
metódu Render()
pri každej
podtriede tak, aby robila, čo chceme. Rozhranie tak zostane
zachované a my nebudeme musieť premýšľať, ako sa to pri onom objekte
volá.
Polymorfizmus býva často vysvetľovaný na obrázku so zvieratami, ktoré
majú všetky v rozhraní metódu Speak()
, ale každé si ju
vykonáva po svojom:
Podstatou polymorfizmu je teda metóda alebo metódy, ktoré majú všetci potomkovia definované s rovnakou hlavičkou, ale iným telom.
Polymorfizmus si spolu s dedičnosťou vyskúšame v budúcej lekcii, Aréna s mágom (dedičnosť a polymorfizmus),
na bojovníkoch v našej aréne. Pridáme mága, ktorý si bude metódu
Attack()
vykonávať po svojom pomocou many, ale inak zdedí
správanie a atribúty bojovníka. Zvonku teda vôbec nepoznáme, že to nie je
bojovník, pretože bude mať rovnaké rozhranie. Bude to zábava