Již několikrát jsem ve svých příspěvcích zmiňoval Deméteřin zákon (angl. Law of Demeter). Dnes se podíváme na to, o co jde, k čemu je to dobré a co nám to přináší.
Začněme ukázkou. Využiji klasický příklad, který se u Deméteřina zákona uvádí. Mějme pošťáka, reprezentovaného třídou Mailman
, který doručuje balíček za 200,- Kč zákazníkovi, reprezentovaného třídou Customer
. Při doručení je potřeba tento obnos zaplatit. Kód pošťáka by pro tuto akci mohl vypadat takto:
customer.getWallet().withdraw(200);
Pošták si tedy od zákazníka vyžádá peněženku a vybere z ní 200,- Kč. Pro zjednodušení budeme ve zbytku příkladu ignorovat kontrolu, zda získání peněz proběhlo v pořádku.
Kód výše trpí následujícími nedostatky:
if (customer.hasWallet()) { customer.getWallet().withdraw(200); } else if (customer.hasPiggyBank()) { customer.getPiggyBank().withdraw(200); } else if (...) { ... }
S každou novou metodou by tak přibyl další if
. Pošťák tak zákazníka doslova prohledá, což opět neodpovídá reálnému světu.
Customer
), tak na peněžence (třída Wallet
). Pokud se budeme pohybovat např. ve světě C++, tak si kód pošťáka vyžádá hlavičkový soubor od zákazníka a od peněženky. Každá změna ve třídě Wallet
si tak vyžádá znovu přeložit modul s pošťákem. To jednak prodlužuje dobu průběžného překladu a jednak to zanáší do kódu zbytečnou závislost navíc. Pokud se např. přejmenuje metoda Wallet::withdraw()
, tak je potřeba kód pošťáka upravit.getWallet()
, je obsaženo jak ve třídě Customer
, tak ve třídě Mailman
kvůli kódu pošťáka výše. Stejně jako se snažíme vyvarovat duplikaci kódu bychom se měli snažit vyvarovat strukturální duplikaci.Customer
, ale i pro Wallet
a vstupní kód vhodně nastavit. Takový test vyžaduje zbytečně dlouhou přípravnou fázi (angl. setup phase).Metodu getWallet()
ve třídě Customer
nahradíme za metodu getPayment()
:
class Customer { public: // ... double getPayment(double amount); // Vrací získané peníze. // ... };
Pro zjednodušení budeme sumu peněz reprezentovat pomocí double
, což, jak jistě víte, obecně není dobrý nápad.
Tím, že jsme přidali tuto novou metodu se kód pošťáka zjednoduší:
customer.getPayment(200);
Navíc nyní mnohem více odpovídá realitě, je rozšiřitelný (změny v Customer.getPayment()
se ho nedotknou), odpadne zbytečná závislost třídy Mailman
na Wallet
a strukturální duplikace, kód je lépe testovatelný a již neporušuje princip Tell, don't ask.
Důvodem, proč původní kód trpěl tolika nedostatky je, že porušoval tzv. Deméteřin zákon (angl. Law of Demeter), zavedený v tomto článku. Pokud by vás zajímalo, jak tento zákon vznikl, mrkněte na odkazy uvedené na konci tohoto příspěvku.
Deméteřin zákon pro metody nám říká, že metoda může volat pouze:
Už ale nemůže volat metody na návratových hodnotách těchto metod (pokud nejsou stejného typu, viz dále). Příklad:
class Car { public: void someMethod(Passenger passenger) { engine.getType(); // OK passenger.getName(); // OK engine.getType().setID(123); // Ne! passenger.getAddress().getStreetNumber(); // Ne! Address address = passenger.getAddress(); // Taktéž ne! Je to jen přepsaný příklad výše s využitím dočasné proměnné address.getStreetNumber(); // pro uložení návratové hodnoty a nijak se od něj neliší. } private: Engine engine; };
Řešením je přidat do třídy Passenger
metodu getStreetNumber()
a do Engine
metodu setTypeID()
.
Mezi jiné názvy tohoto principu patří Princip nejmenší znalosti (angl. Principle of least knowledge) či Nemluvte s cizinci (angl. Don't talk to strangers). Motivace je následující (převzato ze strany 126 knihy Programátor pragmatik). Představte si, že přestavujete dům. Obvykle existuje něco jako "generální dodavatel", se kterým máte uzavřenou smlouvu na provedení určité práce. Tento dodavatel může práci udělat sám, nebo může na část práce využít různé subdodavatele. Jako zákazník ale nemáte se subdodavateli nic společného; pokud subdodavatel něco zvorá, tak hlava z něj bolí generálního dodavatele, nikoliv vás! Při psaní softwaru bychom se měli držet stejného principu. Když po objektu něco chceme, tak by to za nás měl vykonat. Neměl by nám jen dát odkaz na někoho, s kým si to máme vyřešit.
Cílem Deméteřina zákona je minimalizace vazeb, strukturálních duplicit a předpokladů týkajících se struktury. Jak jsme viděli na úvodním příkladu, mezi hlavní výhody dodržování tohoto principu patří:
getXXX().getYYY().getZZZ()
.Nic není dokonalé a i bezvýhradné dodržování Deméteřina zákona má některé nevýhody.
customer.getAddress().getCountry().getPopulation()
, měli bychom do třídy Customer
přidat metodu getCountryPopulation()
? Měla by třída Customer
vůbec o něčem takovém vědět? Příklad jsem převzal odtud.Jako i v mnoha jiných případech vývoje SW, i zde je potřeba vážit možná pro a proti. Každopádně, výhody Deméteřina zákona jsou nezanedbatelné a kód, se kterým se běžně setkávám, by z dodržování Deméteřina mohl těžit. Pokud uvažujete pro a proti, vemte do úvahy také skutečnost, že jako programátoři jsme ze své podstaty líní tvorové a mnohdy maskujeme svou lenost za tvrzení, že dodržením tohoto zákona bychom si přihoršili :). Když např. tvrdíme, že dodržením tohoto zákona bychom program zpomalili, měli bychom si ověřit (např. profilováním), že tomu tak skutečně je a že to není jen naše domněnka.
Mně se nejvíce osvědčilo dívat se na porušování tohoto pravidla jako na zápach v kódu (angl. code smell). Zápach v kódu je indikátor, že v kódu může být hlubší problém, nikoliv že tam problém určitě je.
Všímejte si kódu v následujících konstrukcích (první dvě jsem převzal odtud).
getXXX().getYYY().getZZZ()
. Např. když řidiče v autě zastaví policista, tak můžete v kódu vidět něco takového:Licence licence = driver.getWallet().getDriversLicence();
Nyní už víme, že lepší by bylo přidání metody getDriversLicence()
do třídy řidiče, který si řidičský průkaz sám vyhledá a předloží jej policistovi.
Wallet wallet = driver.getWallet(); Licence licence = wallet.getDriversLicence();
Tento kód je na tom prakticky stejně, jako kód v předchozím bodě, jen je složitější ho detekovat. Řešením je opět přidat do třídy řidiče metodu getDriversLicence()
.
int population = 436; Country country = Country("Rockwell Falls", population); Address address = Address("Main Street", 102, country); Customer customer = Customer("Steve", "Kady", address); // Test metody, které se předává customer. // ... // Kód výše bylo nutno vytvořit jen kvůli tomu, že testovaná metoda volá // customer.getAddress().getCountry().getPopulation().
Všimněte si, jak je struktura Customer - Address - Country
zadrátovaná do testu. Pokud dojde ke změně v této struktuře, pak je potřeba test upravit.
Navzdory rozšířené představě není Deméteřin zákon o počítání teček. Vezměme si např. následující kód v Pythonu:
str.strip().lower().startswith('s')
Nejdříve se ze začátku a z konce řetězce odstraní bílé znaky, pak se všechna velká písmena zkonvertují na malá a následně se zkontroluje, zda řetězec začíná na "s"
. Tento kód Deméteřin zákon neporušuje. Důvodem je, že metody strip()
a lower()
vrací obě instance typu řetězec. Jak bylo poznamenáno výše při uvedení Deméteřina zákona, toto je naprosto legální a problém to nezpůsobuje. Kód je již tak závislý na třídě typu řetězec a je jedno, kolik takových metod zavoláme.
Podobně je na tom kód, který využívá tzv. plynulá rozhraní (angl. fluent interface). O nich plánuji v budoucnosti něco napsat. Takováto rozhraní nám umožňují např. vytvoření objektů velmi čitelným způsobem:
Function main = Function("main") .withReturnType("int") .withParam("int", "argc") .withParam("char **", "argv") .withBody(/* ... */);
Jako poslední případ bych zmínil situaci, kterou stejně jako Phil nepovažuji za porušení Deméteřina zákona. Jedná se o případy, kdy je struktura objektů dobře známá a neměnná. Např. tabulka má řádky a sloupce:
table.rows[0].colums[1] = 'x';
Pro další informace lze zavítat na následující odkazy.
Určitě se doporučuji taktéž mrknout na toto skvělé video od Bena, ve které řeší právě Deméteřin zákon.
Komentáře
Díky
Naprosto nejlepší vysvětlení, co jsem kdy četl. Jsem nadšen.
Přidat komentář