/ janturon.cz / Od kodéra k analytikovi / Skládání nebo Dědičnost

Skládání nebo Dědičnost

Základní princip znovupoužitelnosti spočívá v úpravě již existující třídy (místo psaní nové třídy s většinou stejných vlastností). K tomu je zapotřebí umět vhodným způsobem tyto úpravy provádět (viz Návrhové vzory) metodou Skládání nebo dědičnosti, podle toho, jakou mají třídy mezi sebou vazbu (viz Vztahy mezi objekty).

Zatímco skládání je možné použít univerzálně, dědičnost je přehlednější, ale použitelná jen u silných vazeb. Bude-li se kód dále vyvíjet (většina případů), neměli bychom se spokojit "pouze" s funkčním kódem. Právě dopsaný bezchybný kód je vhodné refaktorovat tak dlouho, dokud nedospějeme k závěru "jednodušeji a přehledněji už to nelze zapsat". Obchodní specialisté v tom vidí zbytečnou činnost, která nepřináší zákazníkovi žádnou další hodnotu. Je třeba jim vysvětlit, že to výrazně zlevní jakékoliv budoucí úpravy.

Skládání

Skládání se používá pro objekty se slabou vazbou (agregace, asociace, kompozice).

// C++ class A { public: void Method() { ... } }; class B { A* a; public: public B(A* a) : a(a) { } public void Method() { a->Method(); ... } }; // Javascript function A() { this.Method = function() { ... } } function B(a) { this.Method = function() { a.Method(); ... } }

Typické použití je u nezávislých objektů, které mají společnou metodu. Třeba v nížeuvedeném příkladě mají pták a letadlo podobnou metodu pro let. Vytvoříme z nich třídy implementující totéž rozhraní, a potom u nich můžeme měřit let stejným způsobem. Bylo by ale nesmyslné pokoušet se z ptáka zdědit letadlo nebo naopak.

// C++ #include <string> #include <cstdio> using namespace std; class Bird { bool gender; int distance; public: Bird(bool male) : distance(0), gender(male) { } bool isMale() { return gender; } bool isFemale() { return !gender; } int getDistance() { return distance; } void Fly(int d) { distance += d; } }; class Airplane { string type; public: Airplane(string type) { this->type = type; } string getType() { return type; } int Flight(int speed, int hours) { return speed * hours; } }; class ICanFly { public: virtual void Fly(int speed, int hours)=0; virtual int getDistance()=0; }; class MyBird : public ICanFly { Bird* bird; public: MyBird(Bird* b) : bird(b) { } int getDistance() { return bird->getDistance(); } void Fly(int speed, int hours) { bird->Fly(speed * hours); } }; class MyAirplane : public ICanFly { Airplane* airplane; int distance; public: MyAirplane(Airplane* a) : airplane(a), distance(0) { } int getDistance() { return distance; } void Fly(int speed, int hours) { distance+= airplane->Flight(speed, hours); } }; int main() { Airplane* boeingParam = new Airplane("Boeing 777"); ICanFly* boeing = new MyAirplane(boeingParam); Bird* jayParam = new Bird(true); ICanFly* jay = new MyBird(jayParam); boeing->Fly(820, 2); jay->Fly(30, 3); boeing->Fly(900, 6); printf("Boeing flew %dkm\n", boeing->getDistance()); printf("Male jay flew %dkm\n", jay->getDistance()); return 0; }

Alternativní způsob zápisu je zdědit MyBird z Bird a MyAirplane z Airplane a zároveň na ně implementovat rozhraní ICanFly. Tím ale zesložiťujeme už existující objekty a nárůst složitosti je nežádoucí. V uvedeném příkladě třídy Bird a Airplane obsahují členy popisující jejich objekty, a nic víc. Třídy MyBird a MyAirplane přizpůsobují tyto objekty pro jednu konkrétní činnost reprezentovanou rozhraním ICanFly, a nic víc. Místo My... tříd, které umí totéž, co původní třídy a něco navíc, máme složitost rozdělenu na činnost, základní třídy a přizpůsobené třídy a je snadno přehledné, co jednotlivé části dělají. V alternativním zápisu by mohl vývojář použít MyAirplane i někde jinde, třeba proto, že mu nové metody více vyhovují. Tím ale vzniká zmatek ve vazbách přizpůsobených objektů vyžadující dumání nad každou budoucí úpravou, což je mnohem dražší, než tato čtvrthodinová úvaha.

Javascript nevyžaduje rozhraní: jako slabě typovaný jazyk umožňuje dodat členy pomocí prototypování za běhu, proto vyhodnocování, obsahuje-li funktor člen, probíhá až za běhu, ne v době kompilace. Přesto je vhodné rozhraní simulovat pomocí vyjímek, protože je tak do kódu vepsána činnost, která se s přizpůsobenými objekty provádí:

// Javascript function Bird(gender) { var distance = 0; this.isMale = function() { return gender; } this.isFemale = function() { return !gender; } this.getDistance = function() { return distance; } this.Fly = function(d) { distance += d; } } function Airplane(type) { this.getType = function() { return type; } this.Flight = function(speed,hours) { return speed * hours; } } function ICanFly() { this.Fly = function(speed,hours) { throw "not implemented"; } this.getDistance = function() { throw "not implemented"; } } function IBird(bird) { ICanFly.call(this); this.Fly = function(speed,hours) { bird.Fly(speed*hours); } this.getDistance = function() { return bird.getDistance(); } } function IAirplane(airplane) { ICanFly.call(this); var distance = 0; this.Fly = function(speed,hours) { distance+= speed*hours; } this.getDistance = function() { return distance; } } var boeingParam = new Airplane("Boeing 777"); var boeing = new IAirplane(boeingParam); var jayParam = new Bird(true); var jay = new IBird(jayParam); boeing.Fly(820, 2); jay.Fly(30, 3); boeing.Fly(900, 6); alert("Boeing flew "+boeing.getDistance()+" km"); alert("Male jay flew "+jay.getDistance()+" km");

Dědičnost

// C++ class A { public void Method() { ... } }; class B : public A { public void Method() { A::Method(); ... } }; // Javascript function A() { this.Method = function() { ... } } function B() { A.call(this); this._Method = this.Method; this.Method = function() { this._Method(); ... } }

Používá se všude tam, kde jeden objekt je specializace jiného objektu. Jinými slovy tam, kde u potomka jsou smysluplné VŠECHNY metody rodiče. Dobrým vodítkem k rozpoznání je to, jestli potomek dokáže smysluplně použít jméno předka ve svém názvu. V příkladu níže by se klidně třída Airliner mohla jmenovat AirplaneForPassengers.

// C++ #include <string> #include <cstdio> using namespace std; class Airplane { string type; public: Airplane(string type) : type(type) { } string getType() { return type; } int Fly(int speed, int hours) { return speed * hours; } }; class Airliner : public Airplane { int seats; public: Airliner(string type, int seats) : Airplane(type), seats(seats) { } int getSeats() { return seats; } }; int main() { Airplane* mig21 = new Airplane("Mig-21"); Airplane* boeing = new Airliner("Boeing 777", 300); int mig21Dist = 0; int boeingDist = 0; boeingDist+= boeing->Fly(800, 2); mig21Dist+= mig21->Fly(1800, 1); boeingDist+= boeing->Fly(600, 1); printf("Boeing flew %dkm\n", boeingDist); printf("Mig-21 flew %dkm\n", mig21Dist); return 0; } // Javascript function Airplane(type) { this.getType = function() { return type; } this.Fly = function(speed,hours) { return speed * hours; } }; function Airliner(type,seats) { Airplane.call(this,type); this.getSeats = function() { return seats; } }; var mig21 = new Airplane("Mig-21"); var boeing = new Airliner("Boeing 777", 300); var mig21Dist = 0; var boeingDist = 0; boeingDist+= boeing.Fly(800, 2); mig21Dist+= mig21.Fly(1800, 1); boeingDist+= boeing.Fly(600, 1); alert("Boeing flew "+boeingDist+"km"); alert("Mig-21 flew "+mig21Dist+"km");

Porovnejte složitost s příkladem uvedeným u skládání. Pokud by ale třída Airliner už dědila z jiné třídy (například Vehicle), nebylo by možné ve většině jazyků tento přístup použít. Jinými slovy, pokud bychom časem potřebovali, aby Airliner dědila z jiné třídy, máme smůlu.

Vícenásobná dědičnost není implementovaná v jazycích C# a Java, což jsou dnes komerčně nejúspěšnější vysokoúrovňové jazyky umožňující snadné použití návrhových vzorů - proto bývá jejich implementace pomocí dědičnosti vzácná: smysl návrhového vzoru je univerzální postup při návrhu a vše, co lze zapsat dědičností, lze zapsat i skládáním. Naopak to neplatí, přesto by se dědičnost neměla ignorovat - už jen proto, že pomocí ní lze psát mnohem úspornější a srozumitelnější kód. Zkušený programátor by měl zvážit obě možnosti.