/ janturon.cz / Od kodéra k analytikovi / Principy objektového programování

Principy objektového programování

Základní rysy

Dynamická vazba - Polymorfismus - Zapouzdření - Dědičnost

Dynamická vazba Dynamic dispatch

Vazbou se v tomto kontextu myslí spojení mezi voláním funkce a kódem, který vykonává. Při statické vazbě je toto spojení vytvořeno v době kompilace, při dynamické až za běhu programu, konkrétně v místě volání. Nejčastějším příkladem je rozhraní.

class Human { public: virtual string Info()=0; }; class Male : public Human { public: string Info() { return "I am male"; } }; class Female : public Human { public: string Info() { return "I am female"; } }; int main() { cout << "Are you male? (y/n)"; bool male = cin.get()=='y'; Human* h; if(male) h = new Male(); else h = new Female(); cout << h->Info(); return 0; }

Staticky nelze určit, jaký kód má být vykonán voláním h->Info();. To, jestli se použite metoda třídy male nebo female, lze určit za běhu: k úspěšnému vykonání programu je zapotřebí dynamická vazba.

Polymorfismus Polymorphism

Mnohotvarost znamená zápis více definicí (tzv. přetížení). To, která se použije, se určí z kontextu, ve kterém se kód nachází.

// operátorový polymorfismus string s = "1" + "1"; // "11" int i = 1 + 1; // 2 // přetěžování int inc(int a) { return a+1; } string inc(string a) { return a+"1"; } cout << inc(2); // 3 cout << inc("2"); // 21

Zapouzdření Encapsulation

Je skrytí stavu objektu (tedy hodnot jeho proměnných) a jejich zpřístupnění pouze pomocí metod. Tím se předchází nekonzistenci dat, která by neuváženou přímou změnou mohla nastat.

class Average { public: int Count; float Value; Average() : Value(),Count() { } void Add(float number) { Value = (Count*Value+number)/++Count; } };

Následující kód počítá průměr (za chodu):

Average data; data.Add(1); data.Add(2); cout << data.Value; // 1.5

Při pokusu o přímou změnu Count nebo Value se objekt dostane do nekonzistentního stavu a začne vracet špatné výsledky. Vykonávání programu by mělo skončit chybou. Proto se atributy znepřístupňují pro volání mimo třídu, přístup k nim je umožněn pouze přes metody. Zapouzdření uvedené třídy by vypadalo následovně:

class Average { int count; float value; public: Average() : value(),count() { } void Add(float number) { Value = (count*value+number)/++count; } float GetValue() { return value; } void SetValue(float newValue) { value = newValue; count = 1; } };

Přestože přímá změna některých atributů nemusí objekt uvést do nekonzistence, z důvodu znovupoužitelnosti se zapouzdřují všechny atributy. To ale vede k velkému množství triviálních setterů (mutators) a getterů (accessors), což je antivzor boilerplate. Tuto nepříjemnost modernější jazyky řeší pomocí vlastností (Properties) - viz implementované vzory.

Dědičnost Inheritance

Dědičnost eliminuje opakující se kód přebíráním neprivátních členů z jedné třídy (rodiče) do druhé (potomka). Mezi příbuznými se tak vytváří statická vazba: změna rodiče ovlivní všechny jeho potomky. C++ narozdíl od Javy a C# podporuje vícenásobnou a neveřejnou dědičnost, kterou tyto jazyky obchází kompozicí. Správný objektový vztah se řídí těmito pravidly:

Podrobnosti viz v kapitole Vztahy mezi objekty.

Principy

SOLID: SRP - OCP - LSP - ISP - DIP

GRASP: High Cohesion - Low Coupling - Indirection - Information Expert - Service - Polymorphism - Controller

Single Responsibility Principle - Jediná zodpovědnost

Značí, že ucelený blok kódu (třída, funkce) by měl dělat pouze jednu věc a tu by měl dělat dobře. Tedy na první pohled i bez komentáře by mělo být jasné, co blok provádí. Kód celé funkce by se měl vždy vejít na obrazovku. Počet parametrů funkce a atributů třídy by neměl překročit 5. Funkci i třídu musí být možné výstižně a krátce pojmenovat. Nedodržení těchto zásad znamená porušení principu. Složitější funkčnost by měla být realizována kompozicí jednodušších, viz vzor Chain of responsibility.

Open/Closed Principle - Princip otevřenosti a uzavřenosti

Zdrojový kód odladěného bloku, který už se používá jinde, by už neměl být nikdy měněn (uzavřenost k úpravám), ale měl by být snadno rozšiřitelný pomocí návrhových vzorů (otevřenost k rozšíření). Toho lze docílit vhodnou volbou vstupů a výstupů. Například tento kód:

void greetings() { cout << "Hello world"; }

není otevřený k rozšiřitelnosti, nelze ho použít ve formulářové aplikaci. Správně navržený a rozšířený kód by vypadal nějak takto:

string greetings() { return "Hello, world"; } // rozšíření string myGreetings() { return "I said: "+greetings(); } // různé použití cout << myGreetings(); MessageBox::Show(myGreetings()); fputs(myGreetings(),MYFILE);

Liskov Substitution Principle - Záměna podle Liškov(é)

Barbora Liškov(á), jedna z obdivuhodných žen v IT, definovala tento princip jako Pokud S je podtypem [=zděděno z] T, pak objekty typu T mohou být nahrazeny objekty typu S [beze změny kódu].

To lze do kodérštiny volně přeložit jako Rodiče lze kdekoliv nahradit potomkem, aniž by to způsobilo chybu. Z toho plyne, že tam, kde se očekává typ rodiče, můžeme použít i typ potomka. Tento princip neplatí u neveřejné dědičnosti.

Interface Segregation Principle - Vyčleňování rozhraní

Říká, že rozhraní by mělo být co nejstručnější: mělo by pomáhat porozumění abstrakce, ne komplikovat implementaci. Typickým příkladem porušení je rozhraní MouseListener z balíku java.awt.event. Obsahuje pět metod pro události myši nad objektem. Většinou se ale implementuje pouze metoda mouseClicked, rozhraní pak nutí třídu napsat čtyři prázdné metody. Proto byla napsána třída MouseAdapter, jejímž zděděním tyto prázdné metody třídě dodáme. To je však na úkor přehlednosti, boilerplate a znemožňuje zdědit jinou třídu, neboť Java vícenásobnou dědičnost nepodporuje.

Dependency Inversion Principle - Obrácená závislost

Z pohledu funkčnosti kódu jsou jednoduché objekty volány složitějšími a složitějšími. Z pohledu návrhu musí být závislost obrácená: ze složitějších (tedy abstraktnějších) objektů jsou tvořeny jednodušší, méně abstraktní až po konkrétní (tedy provádějící základní instrukce).

Při návrhu z abstrakce plynou detaily, ne z detailů jejich abstrakce.

GRASP - Návrh

General Responsibility Assignment Software Patterns je sada doporučení, jak mezi objekty rozdělit zodpovědnost.

Soudržnost High Cohesion

Objekt by měl mít jednu zodpovědnost a obsahovat data, která spolu úzce souvisí:

struct Time { int hour, minute, second; // úzce souvisí s Time Hand minuteHand, hourHand; // čas může být bez ručiček };

Nezávislost Low Coupling

Třídy by na sobě měly být co nejméně závislé, aby změna jedné třídy neovlivnila jinou třídu:

struct AnalogWatch { Time t; // změna Time změní AnalogWatch = závislost Hand minuteHand, hourHand; };

Zprostředkování Indirection

Spojení dvou neprovázaných objektů (low coupling) by měl zajišťovat třetí, zprostředkující objekt, aby oba objekty mohly zůstat soudržné. Mějme například dvě nezávislé třídy Time a Date a chování (metodu) addHour.

struct Time { int hour, minute, second; bool addHour() { ... } // true: >24 }; struct Date { int day, month, year; bool addDay() { ... } };

dobře: addHour pracuje s oběma třídami, zapsána v propojovacím objektu:

struct DateTime { Date d; Time t; bool addHour() { if(t.addHour()) d.addDay(); } };

špatně: addHour uzpůsobena pro práci s Date - Time nezávisí na Date, proto by neměl obsahovat žádné metody s ním pracující:

struct Time { int hour, minute, second; bool addHour() { ... } // true: >24 bool addHour(Date d) { if(addHour()) d.addDay(); } };

Vlastník informace Information expert

data by měla být uložena co nejblíže třídě (a pokud možno přímo v ní), která nad nimi provádí operace. Nedodržení tohoto principu nazývá Martin Fowler Feature Envy (závist vlastnosti).

Ve výšeuvedeném špatném příkladě třída Time závidí třídě Date její metodu addDay, kterou by chtěla použít. Závist vypovídá o špatném rozdělení zodpovědnosti mezi objekty.

Služba Service / Pure Fabrication

Je třída nesouvisející s problémovou doménou vytvořená k zajištění soudržnosti a nezávislosti tříd. Jinými slovy chování nesouvisející s doménovou problematikou, a přesto nezbytné pro běh aplikace by mělo být v samostatné třídě, aby nebyla zamlžena obchodní logika.

Tvorba Creator

Objekt A by měl vytvářet objekt B (ne naopak), pokud A agreguje či komponuje B, nebo má informace k jeho vytvoření. Viz vztahy mezi objekty.

Polymorfismus

viz Polymorfismus místo testování typu

Obsluha Controller

Pro obsluhu uživatelských akcí by měla být vyčleněna alespoň jedna samostatná třída. Viz MVC v Architektonických vzorech.