/ janturon.cz / Od kodéra k analytikovi / Vztahy mezi objekty

Vztahy mezi objekty

Vztahy graficky znázorněny podle Diagramu tříd jazyka UML

Asociace, Agregace, Kompozice

jsou vztahy mezi objekty. Asociace je nejvolnější, kompozice nejužší vztah. Ještě těsnější vazbu vyjadřuje dědičnost popsaná ve vedlejším sloupci.

uml

Asociace

Objekty ve vztahu asociace mohou existovat nezávisle na sobě. Loď má posádku. Posádka může existovat i bez lodi, loď může existovat i bez posádky. Jeden člověk může sloužit na více lodích, jedna loď může mít více členů posádky, proto poměr m ku n.

// A je asociováno s B (šipka z A do B) class A { ... } class B { void method(A a) { ... } };

Agregace

Jeden objekt potřebuje k existenci jiný. Využívaný objekt může existovat nezávisle. Plachetnice má plachty a potřebuje je ke svému fungování. Plachty jsou uzpůsobené k výměně.

// A je agregováno do B class A { ... }; class B { A *a; public: B(A* a) { this->a = a; } void setA(A* a) { this->a = a; } };

Kontext

je informace o agregujícím objektu předaná jako parametr agregovanému objektu. Agregující objekt tak slouží jako sdílené úložiště informací agregovaných objektů. Příkladem je grafický kontext (komponenta, do níž agregovaný objekt vykresluje grafiku) nebo návrhový vzor Visitor.

struct A { B* context; }; class B { A *a; public: B(A* a) { this->a = a; a->context = this; } };

Pozor: nesprávné použití kontextu může mít za následek antivzor Feature Envy.

Kompozice

Jeden objekt potřebuje k existenci jiný. Využívaný objekt nemůže existovat nezávisle. Loď má kormidlo a potřebuje ho ke svému fungování. Kormidlo je v lodi napevno.

Směr agregace a kompozice je určen způsobem využití. Loď využívá kormidlo, Plachetnice využívá plachty, ne naopak. Lze samozřejmě implementovat tutéž aplikaci neintuitivním způsobem, který nedodržuje obchodní logiku. Udržitelnost a rozšiřitelnost takovéto aplikace ale bude nízká.

// A je komponováno do B class A { ... } // dynamická varianta class B { A *a; public: B() { a = new A; } ~B() { delete a; } }; // statická varianta class B { A a; public: B() : a() { } };

Implementace

class Kormidlo { ... }; class Plachty { ... }; class Clovek { string name; vector<Lod> sluzba; ... }; class Lod { vector<Clovek> posadka; Kormidlo* kormidlo; public: Lod() { kormidlo = new Kormidlo(); } ~Lod() { delete kormidlo; } ... }; class Plachetnice : public Lod { Plachty* plachty; public: Plachetnice(Plachty* plachty) { this->plachty = plachty; } ... };

Výšeuvedený příklad porušuje zásadu Jediný zdroj pravdy: pokud člověk nastoupí na loď, je nutno tuto informaci zanést jak do objektu člověka, tak do objektu lodě. To samé platí pro vystoupení z lodi. Pokud bychom tuto změnu provedli pouze v jednom objektu, aplikace by se dostala do nekonzistentního stavu: člověk by měl v záznamech, že na lodi pracuje, ale v lodi by nebyl vedený (nebo naopak). Při jakékoliv manipulaci s daty bychom si toto museli pamatovat. Čím více takovýchto nutností něco si pamatovat, tím větší šance, že na něco zapomeneme. (Šance rapidně rostou, pokud projekt přebírá jiný programátor.) Tento problém řeší návrhový vzor Interceptor.

Interceptor

U asociací m:n je Interceptor zvláštní třída, která tuto vazbu (coupling) provádí a udržuje aplikaci v konzistentním stavu. V příkladě bychom mohli odstranit pole sluzba a posadka a zavést místo nich Interceptor PosadkaNaLodi:

class PosadkaNaLodi { vector<Lod> lode; vector<Clovek> posadka; public: void prihlasNaLod(Clovek clovek, Lod lod); void odhlasZLodi(Clovek clovek, Lod lod); void pridejLod(Lod lod); void odeberLod(Lod lod); void pridejCloveka(Clovek clovek); void odeberCloveka(Clovek clovek); };

A upravit místo v UML diagramu následovně:

 

uml2

 

Člověk může být s lodí asociován i jiným způsobem: třeba jako pasažér či technik (obojí ve vztahu m:n vyžadující propojovací objekt), jako kapitán či vlastník (ve vztahu 1:n - loď může mít jen jednoho kapitána, kapitán může velet na více lodích), nebo jako člověk, který na lodi trvale bydlí (ve vztahu n:1 - na jedné lodi může být více squatterů).

Existují i případy asociace 1:1, například manželství, které je v euroamerické kultuře chápáno jako vztah jednoho muže s jednou ženou.

Závislost Dependency

Pro vztahy, které nejsou postiženy žádným z uvedených způsobů, se používá prázdná šipka značící obecnou závislost. Implementačně to bývá nejčastěji použití jiné třídy v argumentu metody.

 

uml3

 

class Plachty { float area; public: Plachty(Size size) { area = size.Width * size.Height; } ... };

Narozdíl od asociace, size není uložena do atributů třídy Plachty.

Neveřejná, vícenásobná a virtuální dědičnost

Zatímco při dynamické vazbě (asociace, agregace a kompozice) se za běhu nejprve vytvoří objekty, a pak mohou být různě poskládány, dědičnost je statická (vazba se vytvoří při kompilaci): před spuštěním programu je kompilátorem určeno, že objekt plachetnice bude do konce běhu spojen s jedním konkrétním objektem lodi a žádným způsobem na tom nelze nic změnit. Výhodou tohoto omezení je menší množství kódu při správném použití a chyby odhalené už v době kompilace. Správné použití je vztah JE: zde (objekt) Plachetnice JE (týž objekt, specializací) lodi, zatímco identita plachet či kormidla není určena objektem, na kterém je používána.

Neveřejná dědičnost je vztah JE VYTVOŘEN POMOCÍ. Použijeme ji tehdy, má-li být potomek specializací rodiče, ale některé členy rodiče mají být znepřístupněny (což je porušení principu záměny podle Liškové). Mějme například třídu Obdélník. Platí, že třída Čtverec je specializací Obdélníku. Zároveň ale ve čtverci nedefinujeme šířku a výšku, ale stranu. Implementace by mohla vypadat takto:

class Rectangle { public: float Width, Height; Rectangle(float width, float height) : Width(width), Height(height) { } float Area() { return Width * Height; } }; class Square : private Rectangle { public: float Side; Square(float side) : Rectangle(side,side), Side(side) { } float Area() { return Rectangle::Area(); } };

Všechny členy Rectangle jsou do Square zděděny jako privátní, bez ohledu na jejich modifikátor u rodičů. Metoda Area() byla zveřejněna i v potomkovi, protože se počítá stejně. Objekt čtverce byl vytvořen pomocí konstruktoru obdélníku dosazením stejné hodnoty pro šířku i výšku. K šířce a výšce už ale ve čtverci nelze veřejně přistupovat, zastupuje je veřejný člen side.

Protože všechny geometrické útvary mají svůj obsah, mohli bychom jej vypočítat univerzálně pomocí společného rozhraní. Vložme do něj i název třídy title:

class Shape { public: string Title; Shape(string Title) : Title(title) { } float Area() { return 0; } }; class Rectangle : Shape { public: float Width, Height; Rectangle(float width, float height) : Width(width), Height(height), Shape("obdelnik") { } float Area() { return Width * Height; } };

Pokud bychom ale vytvořili objekt Shape pomocí konstruktoru Rectangle či Square, nedopadl by výsledek podle očekávání:

Shape* rect4x3 = new Rectangle(4,3); rect4x3->Area(); // 0!

Chceme-li vyjádřit, že metoda Area() má být v Shape překrytá libovolným potomkem, deklarujeme ji klíčovým slovem virtual. Pokud navíc místo těla metody zapíšeme =0, znamená to ryze virtuální metodu, tedy povinnost definovat její tělo v potomkovi (s tím tedy nemožnost vytvořit objekt pouze pomocí konstruktoru Shape:

class Shape { public: string Title; Shape(string Title) : Title(title) { } virtual float Area()=0; // ryze virtuální }; Shape* rect4x3 = new Rectangle(4,3); rect4x3->Area(); // 12

Pokud bychom totéž chtěli provést u třídy Square, která už dědí ze třídy Rectangle, museli bychom vyřešit tzv. diamantový problém:

diamantový problém

Ze Square nyní existují dvě cesty ke třídě Shape, ze které překrýváme virtuální metodu Area(). Každá cesta může překrytí provést různým způsobem a kompilátor neví, kterou cestou se vydat (dojde k chybě při překladu). Proto použijeme virtuální dědičnost, která znamená, že všechny členy se zdědí přímo.

Pokud zde Rectangle zdědí Shape virtuálně, pak si to kompilátor vyloží, jako by metoda Area() byla definována přímo v Rectangle. Pokud je Shape zděděn i v Rectangle virtuálně, pak ve výsledku Square překrývá metodu Area() z Rectangle, žádné dvojznačnosti nejsou a diamantový problém byl tak vyřešen.

diamantový problém

Řešení diamantového problému v tomto příkladě vypadá takto:

class Shape { public: string Title; Shape(string title) : Title(title) { } virtual float Area() const=0; }; class Rectangle : public virtual Shape { public: float Width, Height; Rectangle(float width, float height) : Shape("obdelnik"), Width(width), Height(height) { } float Area() const { return Width * Height; } }; class Square : private Rectangle, public virtual Shape { public: float Side; Square(float side) : Rectangle(side,side), Shape("ctverec"), Side(side) { } float Area() const { return Rectangle::Area(); } }; Shape* square = new Square(5); square->Area(); // 25 square->Title; // ctverec