1. Dědičnost a polymorfismus Cíl látky Cílem této kapitoly je představit klíčové pojmy dědičnosti a polymorfismu. Předtím však je nutné se seznámit se základními pojmy zobecnění neboli generalizace.
Komentář V této kapitole se používají dva na první pohled význammově stejné pojmy: operace a metoda. Pojem operace je požíván, když se vysvětlují diagramy tříd UML, zatímco pojem metoda se používá, když se jedná o komentování zdrojových kódů konkrétního programovacího jazyka.
1.1. Zobecnění (generalizace) K správnému pochopení dědičnosti a polymorfismu je nutné získat solidní základy v oblasti zobecnění. Zobecnění je relací mezi obecnějším elementem a elementem přesněji specifikovaným. Přičemž přesněji specifikovaný element je zcela konzistentní s obecnějším elementem, ale obsahuje více informací. Tyto dva elementy se řídí zákonem nahraditelnosti. Konkrétnější element se může použít všude tam, kde by mohl být použít element obecnější, aniž by se ohrozil chod systému.Tato relace je mnohem pevnější než je asociace. Odkazy na zákon nahraditelnosti: LSP (EN) [http://www.engr.mun.ca/~theo/Courses/sd/pub/sd-principles-3.pdf] LSP (CZ) [http://www.robertdresler.cz/2011/02/liskov-substitution-principle.html]
1.1.1. Zobecnění tříd Z koncepčního hlediska je zobecnění (generalizace) založeno na velice jednoduché myšlence - určitě každý chápe, že zvíře je zobecněním kočky, psa nebo koně. Dokonce zobecnění může být několikaúrovňové. Z předchozího příkladu se můžou pes a kočka považovat za domácí zvířata a ty například s volně žijícími zvířaty se teprve mohou zevšeobecnit do zvířat.
1
Obrázek 1.1. Hierarchie zobecňovaní Na obrázku 1.1 je třída Zvíře zcela určitě obecný pojem. Od tohoto pojmu jsou odvozeny potomci, podtřídy, které jsou specializovanými variantami obecného pojmu Zvíře. Podle zákona o nahraditelnosti lze použít jakékoliv potomky tříd Kočka, Pes nebo Kůň kdekoli, kde lze použít třídu Zvíře. Když se postupuje od konkrétního k obecnějšímu, je tento myšlenkový postup nazýván právě zobecňováním, ale lze postupovat i opačným směrem, to je od obecnějšího ke konkrétnějšímu, potom tento postup se nazývá specializace. Někdy se tyto dva pojmy spojují do slangového slova genspec. Při návrhu objektově orientovaného softwaru se používají metody, jak postupného zobecňování, tak metodu postupného upřesňování.
1.1.2. Dědičnost tříd Při uspořádání tříd do hierarchie zobecnění tak, jak je znázorněno na obrázku 1.1 se vytvoří dědičná vazba mezi třídami, v níž potomci dědí všechny charakteristické vlastnosti svého předka neboli nadtřídy. Jsou to: • Atributy, • operace, • relace. Potomci mohou ke zděděnému fondu vlastností přidat novou charakteristiku. Ale nejen to, mohou dokonce některé operace předefinovat.
Překrývání Z diagramu 1.2 lze odečíst, že podtřídy Čtverec a Kruh dědí všechny atributy, operace z nadtřídy Tvar. Charakteristiku nadtřídy Tvar v podtřídách není vidět, přesto je v těchto podtřídách implicitně obsažena. Tomu se říká, že třídy Čtverec a Kruh jsou typu Tvar. 2
Obrázek 1.2. Diagram tříd s dědičností Diagram 1.2 je sice v pořádku, ale takto navržený systém by správně nefungoval, protože operace kreslit() operace nadtřídy nic neví o svých podtřídách, jak by měla vykreslit podtřídy Čtverec nebo Kruh. To samé platí pro operace vypočtiObsah() a vypočtiObvod(). V případě podtřídy Kruh dokonce nadtřída nemůže ani vědět, že třída Kruh obsahuje atribut poloměr. Z předchozího jasně vyplývá, že potomci musí být schopni změnit implicitní chování svých rodičů. Třídy Čtverec a Kruh nutně musí implementovat vlastní operace kreslit(), vypočtiObsah() a vypočtiObvod(), které překrývají implicitní operace svého předka. Tyto operace se již mohou postarat o správnější chování. Následující diagram 1.3 již jasně demonstruje, že operace předka jsou překryty příslušnými operacemi potomků.
3
Obrázek 1.3. Překrytí operací předka Potomci překrývají (mění definici) zděděných operací tím, že definují novou operaci se stejnou signaturou. Programovací jazyky mohou signaturu operace definovat odlišným způsobem. Například v jazycích C++ a Java sice není návratový typ operace součástí signatury operace, ale přesto jej při překrývání je nelze změnit. Pokud dojde k překrytí operace předka operací potomka, která se liší návratovým typem, ohlásí překladač chybu.
Abstraktní operace a třídy Někdy je zapotřebí odložit implementaci operace a přenechat ji potomkům nadtřídy. Například operace Tvar::kreslit() je ideální ukázkou tohoto případu. V třídě Tvar nelze vytvořit žádnou smysluplnou implementaci zmiňované operace, protože se neví jaký tvar se má vykreslit. Pojem „vykreslit tvar“ je příliš abstraktní, než aby se mohl nějak uchopit jakoukoliv konkrétní implementací. Když je zapotřebí určit, že operace postrádá implementaci, definuje se jako abstraktní. V jazyce UML se to vyjadřuje tím, že název operace se napíše kurzivou.
4
Obrázek 1.4. Abstraktní třída a operace Jakmile je ve třídě alespoň jedna operace abstraktní, je taková třída neúplná a proto z ní nelze vytvořit instanci. Takové třídy se označují jako abstraktní. Pro název takové třídy platí v UML stejné pravidlo, jako u abstraktní operace. Název je zapsán zase kurzívou. Dokonce platí takové pravidlo, že pokud je třída označená jako abstraktní a současně nemá žádnou abstraktní operaci, nelze z této třídy vytvořit instanci. Třídy, které umožňují tvorbu svých instancí, se obecně označují jako třídy konkrétní (instanční). Užití abstraktních tříd a operací má nesporně dvě velké výhody: 1. Množinu abstraktních operací lze definovat v abstraktní nadtřídě, která musí být implementována všemi svými potomky. Tento postup lze považovat za definici určité dohody, kterou konkrétní potomci musí implementovat. 2. Může se vytvořit kód, který manipuluje se všemi potomky dané nadtřídy podle zákona nahraditelnosti. Tyto výhody jsou stěžejními principy polymorfismu, kterému je věnována další kapitola.
Stupně abstrakce Předměty na stejné úrovni zobecnění by se měly vyznačovat stejným stupněm abstrakce. 5
Příklad zdrojového kódu Výpis 1.1. Abstraktní třída Tvar public abstract protected Bod protected int protected int
}
class Tvar { počátek = (0,0); šířka; výška;
public abstract void kreslit(); public abstract float vypočtiObsah(); public abstract float vypočtiObvod(); Uvedením modifikátoru abstract před klíčovým slovem třídy class se vytváří abstraktní třídu. Z této třídy nelze vytvořit instanci. Atributy třídy Tvar jsou všechny označeny modifikátorem protected, aby bylo možné k nim přistupovat z potomků. Není to nejlepší řešení, protože se tím odkrývá implementace třídy Tvar. Tomuto problému je věnováno jedno z dalších témat. Jestliže se modifikátor abstract uvede v deklaraci metody, je potom tato metoda abstraktní, proto chybí tělo metody. Výpis 1.2. Konkrétní třída Kruh
public class Kruh extends Tvar { private int poloměr; public Kruh(Bod b, int r){ počátek = b; poloměr = r; } public void kreslit(){ ... } public float vypočtiObsah(){ return (float) (Math.PI*poloměr*poloměr); } public float vypočtiObvod(){ return (float)(2*Math.PI*poloměr); } } Protože u atributu počátek je modifikátor protected, lze k němu přistupovat z potomka. Není to dobré řešení. Správnější by bylo, aby tento atribut byl privátní a jeho nastavení by mělo být zabezpečeno pomocí parametru konstruktoru předka. Tělo metody kreslit() je zde vynecháno z důvodu zjednodušení a protože se nevztahuje k samotné problematice abstraktních tříd. Všechny metody v konkrétní třídě, které překrývají abstraktní metody zděděné od všech svých předků, musí mít definovány konkrétní těla metod. 6
1.2. Polymorfismus Polymorfismus znamená mnohotvarost. Polymorfní operace jsou operace s mnoha implementacemi. V příkladu třídy Tvar to byly všechny tři abstraktní operace, protože tyto operace mají v každém potomku jiné implementace. Operace tedy mají „mnoho forem“ a jsou proto polymorfní. Třídy Čtverec a Kruh z diagramu 1.4 dědí od třídy Tvar a poskytují implementace polymorfním operacím Tvar::kreslit(), Tvar::vypočtiObsah() a Tvar::vypočtiObvod(). Všichni potomci třídy Tvar musí poskytovat konkrétní operace kreslit(), vypočtiObsah() a vypočtiObvod(), neboť tyto operace jsou deklarovány v nadtřídě. To znamená, že z pohledu operací kreslit(), vypočtiObsah() a vypočtiObvod()se může se všemi potomky třídy Tvar jednat stejně. Množina abstraktních operací je tedy způsobem, jak definovat množinu operací, které musí implementovat všichni konkrétní potomci. Podstata polymorfismu - objekty různých tříd obsahují operace se stejnou signaturou, ovšem s jinou implementací.
1.2.1. Příklad polymorfismu Na diagramu 1.5 je třída KreslicíPlátno, která vlastní kolekci objektů typu Tvar. Že se jedná o kolekci je znázorněno hvězdičkou u šipky na konci čáry, která definuje asociaci mezi třídami KreslícíPlátno a Tvar. Takto, i když je to velmi zjednodušeno, pracuje většina grafických systémů.
Obrázek 1.5. Příklad diagramu tříd s polymorfismem Třída Tvar je abstraktní a proto nemůže z této třídy vzniknout žádná instance. Podle zákona o nahraditelnosti, lze však vytvářet instance z konkrétních potomků, které lze dosazovat všude tam, kde je volána třída Tvar. Na diagramu 1.6 je ukázka možného objektového modelu, který vznikl podle diagramu tříd 1.5. Objektový model ukazuje, že objekt :KresliciPlatno 7
obsahuje čtyři spojení na objekty t1, t2, t3 a t4. Objekty t1, a t3 jsou instancemi třídy Kruh a objektyt2 a t4 jsou instancemi třídy Čtverec. K čemu dojde, když objekt :KreslícíPlátno projde svoji kolekci? Každý takto vyvolaný objekt provede přesně to co je od něho očekávano. Objekt typu Kruh vykreslí kruh a objekt typu Čtverec vykreslí čtverec.
Obrázek 1.6. Příklad diagramu objektů z diagramu tříd1.5 Podtržení v objektovém diagramu 1.6 signalizuje, že se jedná o objekt. Podobný účel má dvojtečka před typem objektu.
8