7. díl - Dědičnost a polymorfismus

C++ Objektově orientované programování Dědičnost a polymorfismus

V minulém tutoriálu o C++ jsme dokončili naši arénu, simulující zápas dvou bojovníků. Dnes si opět rozšíříme znalosti o objektově orientovaném programování. V úvodním dílu do OOP jsme si říkali, že OOP stojí na třech základních pilířích: zapouzdření, dědičnosti a polymorfismu. Zapouzdření a používání modifikátoru private nám je již dobře známé. Dnes se podíváme na zbylé dva pilíře.

Dědičnost

Dědičnost je jedna ze základních vlastností OOP a slouží k tvoření nových datových struktur na základě starých. Vysvětleme si to na jednoduchém příkladu:

Budeme programovat informační systém. To je docela reálný příklad, abychom si však učení zpříjemnili, bude to informační systém pro správu zvířat v ZOO :) Náš systém budou používat dva typy uživatelů: uživatel a administrátor. Uživatel je běžný ošetřovatel zvířat, který bude moci upravovat informace o zvířatech, např. jejich váhu nebo rozpětí křídel. Administrátor bude moci také upravovat údaje o zvířatech a navíc zvířata přidávat a mazat z databáze. Z atributů bude mít navíc telefonní číslo, aby ho bylo možné kontaktovat v případě výpadku systému. Bylo by jistě zbytečné a nepřehledné, kdybychom si museli definovat obě třídy úplně celé, protože mnoho vlastností těchto 2 objektů je společných. Uživatel i administrátor budou mít jistě jméno, věk a budou se moci přihlásit a odhlásit. Nadefinujeme si tedy pouze třídu Uzivatel (nepůjde o funkční ukázku, dnes to bude jen teorie, programovat budeme příště):

class Uzivatel
{
private:
        string jmeno;
        string heslo;
        int vek;
public:
        bool Prihlasit(string heslo)
        {
                ...
        }

        bool Odhlasit()
        {
                ...
        }

        void NastavVahu(Zvire zvire)
        {
                ...
        }

        ...
}

Třídu jsem jen naznačil, ale jistě si ji dokážeme dobře představit. Bez znalosti dědičnosti bychom třídu Administrator definovali asi takto:

class Administrator
{
private:
        string jmeno;
        string heslo;
        int vek;
        string telefonniCislo;
public:
        bool Prihlasit(string heslo)
        {
                ...
        }

        bool Odhlasit()
        {
                ...
        }

        void NastavVahu(Zvire zvire)
        {
                ...
        }

        void PridejZvire(Zvire zvire)
        {

        }

        void VymazZvire(Zvire zvire)
        {

        }

        ...
}

Vidíme, že máme ve třídě spoustu redundantního (duplikovaného) kódu. Jakékoli změny musíme nyní provádět v obou třídách, kód se nám velmi komplikuje. Nyní použijeme dědičnost, definujeme tedy třídu Administrator tak, aby z třídy Uzivatel dědila. Atributy a metody uživatele tedy již nemusíme znovu definovat, C++ nám je do třídy sám dodá:

class Administrator: public Uzivatel
{
private:
        string telefonniCislo;
public:
        void PridejZvire(Zvire zvire)
        {

        }

        void VymazZvire(Zvire zvire)
        {

        }

        ...
}

Vidíme, že ke zdědění jsme použili operátor ":". V anglické literatuře najdete dědičnost pod slovem inheritance.

V příkladu výše nebudou v potomkovi přístupné privátní atributy, ale pouze atributy a metody s modifikátorem public. Private atributy a metody jsou chápány jako speciální logika konkrétní třídy, která je potomkovi utajena, i když ji vlastně používá, nemůže ji měnit. Abychom dosáhli požadovaného výsledku, použijeme nový modifikátor přístupu protected, který funguje stejně, jako private, ale dovoluje tyto atributy dědit. Začátek třídy Uzivatel by tedy vypadal takto:

class Uzivatel
{
protected:
        string jmeno;
        string heslo;
        int vek;

        ...

Když si nyní vytvoříme instance uživatele a administrátora, oba budou mít např. atribut jmeno a metodu Prihlasit(). C++ třídu Uzivatel zdědí a doplní nám automaticky všechny její atributy.

Výhody dědění jsou jasné, nemusíme opisovat oběma třídám ty samé atributy, ale stačí dopsat jen to, v čem se liší. Zbytek se podědí. Přínos je obrovský, můžeme rozšiřovat existující komponenty o nové metody a tím je znovu využívat. Nemusíme psát spousty redundantního (duplikovaného) kódu. A hlavně - když změníme jediný atribut v mateřské třídě, automaticky se tato změna všude podědí. Nedojde tedy k tomu, že bychom to museli měnit ručně u 20ti tříd a někde na to zapomněli a způsobili chybu. Jsme lidé a chybovat budeme vždy, musíme tedy používat takové programátorské postupy, abychom měli možností chybovat co nejméně.

O mateřské třídě se někdy hovoří jako o předkovi (zde Uzivatel) a o třídě, která z ní dědí, jako o potomkovi (zde Administrator). Potomek může přidávat nové metody nebo si uzpůsobovat metody z mateřské třídy (viz dále). Můžete se setkat i s pojmy nadtřída a podtřída.

Další možností, jak objektový model navrhnout, by bylo zavést mateřskou třídu Uzivatel, která by sloužila pouze k dědění. Z Uzivatel by potom dědili Osetrovatel a z něj Administrator. To by se však vyplatilo při větším počtu typů uživatelů. V takovém případě hovoříme o hierarchii tříd, budeme se tím zabývat ke konci této sekce. Náš příklad byl jednoduchý a proto nám stačily pouze 2 třídy. Existují tzv. návrhové vzory, které obsahují osvědčená schémata objektových struktur pro známé případy užití. Zájemci je naleznou popsané v sekci Návrhové vzory, je to však již pokročilejší problematika a také velmi zajímavá. V objektovém modelování se dědičnost znázorňuje graficky jako prázdná šipka směřující k předkovi. V našem případě by grafická notace vypadala takto:

Dědičnost objektů – grafická notace

Datový typ při dědičnosti

Obrovskou výhodou dědičnosti je, že když si vytvoříme proměnnou s datovým typem mateřské třídy, můžeme do ni bez problému ukládat i její potomky. Je to dané tím, že potomek obsahuje vše, co obsahuje mateřská třída, splňuje tedy "požadavky" (přesněji obsahuje rozhraní) datového typu. A k tomu má oproti mateřské třídě něco navíc. Můžeme si tedy udělat pole typu Uzivatel a v něm mít jak uživatele, tak administrátory. S proměnnou to tedy funguje takto:

Uzivatel u = new Uzivatel("Jan Novák", 33);
Administrator a = new Administrator("Josef Nový", 25);
// Nyní do uživatele uložíme administrátora:
u = a;
// Vše je v pořádku, protože uživatel je předek
// Zkusíme to opačně a dostaneme chybu:
a = u;

V C++ je mnoho konstrukcí, jak operovat s typy instancí při dědičnosti, podrobně se na ně podíváme během seriálu.

Jazyky, které dědičnost podporují, buď umí dědičnost jednoduchou, kde třída dědí jen z jedné třídy, nebo vícenásobnou, kde třída dědí hned z několika tříd najednou. Vícenásobná dědičnost se v praxi příliš neosvědčila, časem si řekneme proč a ukážeme si i jak ji obejít, C++ ji ovšem podporuje.

Polymorfismus

Nenechte se vystrašit příšerným názvem této techniky, protože je v jádru velmi jednoduchá. Polymorfismus umožňuje používat jednotné rozhraní pro práci s různými typy objektů. Mějme například mnoho objektů, které reprezentují nějaké geometrické útvary (kruh, čtverec, trojúhelník). Bylo by jistě přínosné a přehledné, kdybychom s nimi mohli komunikovat jednotně, ačkoli se liší. Můžeme zavést třídu GeometrickyUtvar, která by obsahovala atribut barva a metodu vykresli. Všechny geometrické tvary by potom dědily z této třídy její interface (rozhraní). Objekty kruh a čtverec se ale jistě vykreslují jinak. Polymorfismus nám umožňuje přepsat si metodu vykresli u každé podtřídy tak, aby dělala, co chceme. Rozhraní tak zůstane zachováno a my nebudeme muset přemýšlet, jak se to u onoho objektu volá.

Polymorfismus bývá často vysvětlován na obrázku se zvířaty, která mají všechna v rozhraní metodu Speak(), ale každé si ji vykonává po svém.

Polymorfismus

Podstatou polymorfismu je tedy metoda nebo metody, které mají všichni potomci definované se stejnou hlavičkou, ale jiným tělem. Polymorfismus si spolu s dědičností vyzkoušíme příště na bojovnících v naší aréně. Přidáme mága, který si bude metodu Utoc() vykonávat po svém pomocí many, ale jinak zdědí chování a atributy bojovníka. Zvenčí tedy vůbec nepoznáme, že to není bojovník, protože bude mít stejné rozhraní. Bude to zábava :)


 

  Aktivity (4)

Článek pro vás napsal David Čápka
Avatar
Autor pracuje jako softwarový architekt a pedagog na projektu ITnetwork.cz (a jeho zahraničních verzích). Velmi si váží svobody podnikání v naší zemi a věří, že když se člověk neštítí práce, tak dokáže úplně cokoli.
Unicorn College Autor se informační technologie naučil na Unicorn College - prestižní soukromé vysoké škole IT a ekonomie.

Jak se ti líbí článek?
Ještě nikdo nehodnotil, buď první!


 


Miniatura
Předchozí článek
OOP v C++ - Dokončení arény
Miniatura
Následující článek
Výjimky v C++

 

 

Komentáře
Zobrazit starší komentáře (23)

Avatar
Jindřich Máca
Tým ITnetwork
Avatar
Jindřich Máca:

Mohou je použít špatně a dokonce je i špatně modifikovat v souvislosti s tím, jak byli zamýšlené v předkovi. To byla celá ta pointa o zakázání úprav a návrhu. :)

A jak už jsem psal, princip lokality je v OOP určitě dobré používat, např. v rámci jednotlivých metod. To si ale nijak s dědičností neodporuje. Pokud nechceš používat toto programovací paradigma (OOP), existují jazyky, které ho vůbec nemají nebo ho nevyžadují aplikovat. Pokud ale používáš jazyk, který na něm staví, je dědičnost jeho důležitou součástí.

Co se týče toho přístupu ke kódu, tak mám známého, který tahle občas i pracuje. :D Celý týden nic moc, v pátek si dá 2 - 3 kousky a v sobotu je hotovo. A tenhle člověk píše luxusní OOP v Javě EE, kde se rozhodně nachází spousta dědičnosti. A nikde žádný problém...

Takže závěrem, mysli si o dědičnosti co chceš. Ostatně já ani nevím, jaké máš zkušenosti s programováním a vývojem softwaru, třeba Tě právě vedou k těmto závěrům. Moje praxe a také praxe spousty programátorů z mého okolí mi však ukazuje, že pokud je použita správě a s rozmyslem, je dědičnost naprosto adekvátní koncept, který se dokonce vyplatí používat. ;)

Editováno 1. prosince 20:29
 
Odpovědět 1. prosince 20:28
Avatar
taco
Člen
Avatar
taco:

Jak již řekl kolega, pokud tohle platí, tak máš dědičnost blbě.

Přesně tak. Zdálo se mi, že ty by si to tak dělal. A proto argumentuji, že by to bylo blbě.

není moc možností jak to vyřešit lépe

Já bych řekl, že je jich dost: helper či injektáž toto zajistí, a podle mého rozhodně lépe.

Nacpat základní fyziku a vykreslování to base třídy mi přijde jako dobrý nápad.

Mě ne. Já bych za toto vraždil. Snad nejhorší možný způsob řešení tohoto problému.

Základní fyziku a vykreslování bych umístil do obslužného objektu reprezentující vykreslení (jeden) a logiku vztahů (druhý). To už je takovej evegreen. Někteří si myslí, že máslo by mělo být schopné se dát do ledničky. Ale toto nesouvisí s dědičností ale s návrhem OOP. A to už bych sem netahal.

 
Odpovědět 1. prosince 21:38
Avatar
taco
Člen
Avatar
Odpovídá na Jindřich Máca
taco:

Má pointa spočívá ve tvrzení, že dědičnost nebejvá použita správně. Že architektura je vždycky navržena blbě. A že pokud tomu přidáš dědičnost za účelem reusable kódu, tak sis zalil nohy do betonu.

Dědičnost je výborný koncept pro určení významu co je co, a co kdo od čeho očekává. Ale díky tvrzení:

Nacpat základní fyziku a vykreslování to base třídy mi přijde jako dobrý nápad.

si se zděšením vzpomínám na své staré projekty.

Za mě je to asi všechno. Přeji vám hodně úspěchů, a ať s vámi kód spolupracuje:
https://www.rarous.net/…-part-1.aspx
:-)

 
Odpovědět 1. prosince 21:45
Avatar
David Čápka
Tým ITnetwork
Avatar
Odpovídá na taco
David Čápka:

No počkej, to jsi mi to moc nevysvětlil :)

Základní fyziku a vykreslování bych umístil do obslužného objektu reprezentující vykreslení (jeden) a logiku vztahů (druhý).

Takže mějme třídu Kreslic, ta má metodu vykresli() s parametrem objekt. Jak zajistíš, aby každý objekt měl třeba souřadnice X a Y bez dědičnosti? Budeš v každém objektu znovu zakládat proměnné a implementovat nějaký interface? Vezmi si, že vykresli() předpokládá, že objekt má třeba 20 proměnných, se kterými pracuje.

Ještě si neodpustím otázku, za co bys vraždil. Když fyziku zapouzdřím do privátních metod, tak ji potomek může jen používat jako blackbox, je to principiálně to samé, jako by byla v separátním objektu.

Editováno 2. prosince 9:14
Odpovědět  +1 2. prosince 9:08
Miluji svou práci a zdejší komunitu, baví mě se rozvíjet, děkuji každému členovi za to, že zde působí.
Avatar
taco
Člen
Avatar
Odpovídá na David Čápka
taco:

Jak zajistíš, aby každý objekt měl třeba souřadnice X a Y bez dědičnosti?

Je nutné tam dávat dědičnost? Nestačí interface? Na vynucení.

Vezmi si, že vykresli() předpokládá, že objekt má třeba 20 proměnných, se kterými pracuje.

Tím, že to schováš do nějakého předka, tak jen zamaskuješ, že jsi to nějak špatně navrhl. 20proměnných je dost síla. Ale stát se samozřejmě může, že nějaký element má dvacet vlastností. V takovém případě bych to asi vytknul do spešl objektu (nebo více, předpokládám, že to jsou skupiny vlastností), který by se staral o tuto sumu vlastností.

Když fyziku zapouzdřím do privátních metod, tak ji potomek může jen používat jako blackbox, je to principiálně to samé, jako by byla v separátním objektu.

Nemohu souhlasit.

Ty každému tomu objektu vynucuješ nějaké chování i když ho ve skutečnosti nemusí chtít. To je ideový problém.

Druhý problém, praktický, je ten, že na to chování pak nesmíš šáhnout. Což není reálné. Když to budeš mět v separátním objektu, tak si s tím můžeš všemožně hrát. Můžeš tomu třeba předsadit facade, která bude rozlišovat podle typy objektu, a na jeden dva specielní změníš chování, a nebo si to zase rozmyslíš.

Tím, že dáš nějaké chování do předka, tak plošně zvyšuješ komplexitu všech potomků. A to takovým způsobem, že musíš každýho jednoho potomka otestovat, protože se nedá spolehnout na chování předka. Což je v případě dědičnosti nezjednodušitelné. Zatímco v případě separátního objektu/service to můžeš vyzobávat podle potřeby.

 
Odpovědět 2. prosince 13:22
Avatar
taco
Člen
Avatar
Odpovídá na David Čápka
taco:

Já bych samozřejmě v některých případech tu dědičnost použil, po důkladném zvážení pro a proti. Ale oficiálně to nechci přiznat, protože lidi z toho slyší jen to "dědičnost použil" ale už nad tím nepřemejšlí. Vůbec. Nikdo.

Teď jsem koukal na jednu knihovnu. Ten programátor, co to psal byl rozhodně velice šikovný a zkušený. Prostě napsaný to bylo parádnicky, funkčně, mocně, prakticky bez bugů. Bylo vidět, že tam i chtěl udělat jakýsi systém typů a tak vůbec. Jenže se to vůbec nedalo použít (ten systém typů). Protože ty třídy použil naprosto nesmyslně jen podle toho, kde chtěl použít nějakou funkcionalitu z jiné části a nikoliv podle toho, jak to sématicky vycházelo. Což ve výsledku znamená, že tu knihovnu mohu použít jen tak jak ji napsal, ale rozšiřovat, nebo nějak stavět na tom už nemůžu. Takže z toho bude fork.

 
Odpovědět 2. prosince 13:37
Avatar
David Čápka
Tým ITnetwork
Avatar
Odpovídá na taco
David Čápka:

Produkuješ zbytečně mnoho textu, psal jsem ti, že interface to neřeší a ty odpovíš, že to uděláš přes interface :) Ne, neuděláš. Nemám již čas to důkladně číst, nicméně jsem tam zahlédl, že bys společné vlastnosti vyčlenil do samostatného objektu. Skvěle, to je asi první funkční nahrazení dědičnosti, které jsi tu za celou tu diskuzi předložil a s tím naprosto souhlasím. Každý objekt ve hře by si tedy instancioval nějaký svůj kontejner s proměnnými (20 proměnných je naopak málo, bylo by tam např. x, y, vspeed, hspeed, direction, texture, gravity, weight, friction, atd...). Problém rozhraní je, že se udeleguješ. IMHO bys musel pro každou tuto vlastnost psát getter do každého objektu znovu, abys zavolal metodu na té vnitřní instanci. Nebo mi něco uniká? Getter má 3 řádky, Setter také, krát 20 krát 100 objektů, to je 12.000 řádků balastu jen proto, že místo dědění deleguješ. Nebo mi něco uniká?

Odpovědět 2. prosince 15:11
Miluji svou práci a zdejší komunitu, baví mě se rozvíjet, děkuji každému členovi za to, že zde působí.
Avatar
taco
Člen
Avatar
Odpovídá na David Čápka
taco:

Nemáš čas to důkladně číst a pak se ptáš, zda ti něco uniká? To jako vážně?

Interface to řeší, protože to vynucuje. Já to chci vynutit, já to nechci naimplementovat.

Problém rozhraní je, že se udeleguješ.

Takhle rozhranní nefunguje.

IMHO bys musel pro každou tuto vlastnost psát getter do každého objektu znovu, abys zavolal metodu na té vnitřní instanci. Nebo mi něco uniká? Getter má 3 řádky, Setter také, krát 20 krát 100 objektů, to je 12.000 řádků balastu jen proto, že místo dědění deleguješ.

Takhle to vůbec nefunguje.

Skvěle, to je asi první funkční nahrazení dědičnosti, které jsi tu za celou tu diskuzi předložil

Dvakrát jsem uváděl, jak bych to implementoval, a ty jsi si toho teď všiml. Uváděl jsem několik způsobů. Jsem uražen :-P Každopádně, pokud nechceš číst, co ti píšu, tak si to měl říct hned. Se nemusím obtěžovat.

Bye

Editováno 2. prosince 15:51
 
Odpovědět 2. prosince 15:50
Avatar
David Čápka
Tým ITnetwork
Avatar
Odpovídá na taco
David Čápka:

Zas jsi vůbec jsi neodpověděl na to, jak bys to naimplementoval. Ano, takový text nemám čas číst, protože na první pohled poznám, že v něm nic moc užitečného není. Vlastně mi je líto, že jsem tu ztratil již tak hodinu, což je pro mne několik tisíc korun, vypadalo to jako že s něčím přínosným přijdeš. Zkus se naučit spíše lépe vyjadřovat nebo přiznat, že jsi se spletl. Zeptám se znovu a naposledy, protože jsem již téměř přesvědčen, že jen chodíš okolo horké kaše a nedokážeš odpovědět.

Mějme počítačovou hru, kde je 100 různých objektů, všechny by měly mít nějakých 20 defaultních vlastností + nějaká vnitřní logika. V příkladu udělám jen 3 vlastnosti a 2 potomky, aby to bylo krátké.

Dědičnost

Při použití dědičnosti je bázová třída následující (napíšu to v Javě, aby to bylo kratší):

public class GameObject {
        private int x;
        private int y;
        private int hspeed;
        public int getX() {
                return x;
        }
        public void setX(int x) {
                this.x = x;
        }
        public int getY() {
                return y;
        }
        public void setY(int y) {
                this.y = y;
        }
        public int getHspeed() {
                return hspeed;
        }
        public void setHspeed(int hspeed) {
                this.hspeed = hspeed;
        }
        public final void move() { // Ukázka defaultního chování, které nelze přepsat
                x += hpeed;
        }
}

A potomci:

public class Car extends GameObject {
        private String manufacturer;
        public int getManufacturer() {
                return manufacturer;
        }
        public void setManufacturer(int manufacturer) {
                this.manufacturer = manufacturer;
        }
}

public class Animal extends GameObject {
        private int health = 100;
        // ...
}

Teď mi napiš, prosím, jak bys to implementoval bez dědičnosti. Nebo zas odcituj nějakou mou větu a napiš nějaké další nepodložené tvrzení, ať je jasné, že nevíš.

Editováno 2. prosince 19:02
Odpovědět  +1 2. prosince 18:58
Miluji svou práci a zdejší komunitu, baví mě se rozvíjet, děkuji každému členovi za to, že zde působí.
Avatar
taco
Člen
Avatar
Odpovídá na David Čápka
taco:

Když bych dostal takto přesně určené zadání, tak bych to udělal podobně jako ty.

 
Odpovědět 2. prosince 19:36
Děláme co je v našich silách, aby byly zdejší diskuze co nejkvalitnější. Proto do nich také mohou přispívat pouze registrovaní členové. Pro zapojení do diskuze se přihlas. Pokud ještě nemáš účet, zaregistruj se, je to zdarma.

Zobrazeno 10 zpráv z 33. Zobrazit vše