Deméteřin zákon aneb minimalizujeme strukturální závislosti

Od Petr Zemek, 2014-11-16

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áší.

(De)motivační příklad

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.

V čem je problém?

Kód výše trpí následujícími nedostatky:

  • Neodpovídá reálnému světu. Už jste někdy viděli, aby si od vás pošťák vyžádal peněženku a vytáhl si z ní peníze sám? Zní to absurdně, že? Ovšem přesně toto kód dělá!
  • Špatná rozšiřitelnost. Co když zákazník peněženku vůbec nemá a peníze by si vybral z prasátka? Nebo by si je půjčil od rodičů/kamaráda? Kód výše počítá s tím, že každý zákazník má peněženku. Museli bychom jej rozšířit nějak takto:
    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.

  • Zbytečná závislost. Pošťák je v původním kódu závislý jak na zákazníkovi (třída 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.
  • Strukturální duplikace (angl. structural duplication). Většina programátorů se rychle naučí, že duplikovat kód je špatné (změny je pak potřeba provést na více místech). Typů duplikací je ale více a patří mezi ně i duplikace struktury. Vidíte ji? To, že zákazník má peněženku, přístupnou přes 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.
  • Znesnadněné testování. Jednotkový test původního kódu by si vyžadoval vytvoření instance zákazníka včetně peněženky. Pokud bychom např. k testování použili mocky či stuby, bylo by potřeba vytvořit nejen mock/stub pro 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).
  • Porušování principu Tell, don't ask. O tomto principu jsem zde nedávno psal. Měli bychom objektům říkat, co od nich chceme a nikoliv se jich vyptávat na interní detaily (peněženka) a dělat rozhodnutí na základě nich (výběr peněz).

Co s tím?

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.

Obecný princip: Deméteřin zákon

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:

  • metody své třídy,
  • metody na atributech/datových složkách své třídy,
  • metody na svých parametrech,
  • metody na globálních objektech,
  • metody na lokálně vytvořených objektech.

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.

Výhody

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ří:

  • Lepší udržovatelnost a rozšiřitelnost. Lze měnit interní strukturu bez ovlivnění jiných částí kódu.
  • Nižší počet závislostí. Změny v kódu mohou ovlivnit menší počet závislých tříd. U jazyků vyžadujících překlad (např. C++) má dále za následek rychlejší průběžný překlad kvůli nižšímu počtu #includovaných hlavičkových souborů.
  • Omezení strukturální duplikace. Informace o struktuře je obsažena pouze na jednom místě (v jedné třídě).
  • Jednodušší testování. Není potřeba vytvářet mnoho objektů jen kvůli tomu, že testovaná metoda volá getXXX().getYYY().getZZZ().

Potenciální nevýhody

Nic není dokonalé a i bezvýhradné dodržování Deméteřina zákona má některé nevýhody.

  • Může vést k velkému množství obalujících metod (angl. wrappers). Jak jsme mohli vidět, tak typickým řešením porušení Demétařina zákona je vytvoření obalující metody, které zapouzdří delegování. V některých případech tak může dojít k obrovskému nárůstu počtu takovýchto obalujících metod, což může být nežádoucí (rozsáhlé rozhraní, rychlost kódu).
  • Může vést k rozhraní, které působí nepřirozeně. Pokud máme kód typu 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.

Typické zápachy v kódu související s Deméteřiným zákonem

Všímejte si kódu v následujících konstrukcích (první dvě jsem převzal odtud).

  • Zřetězená volání metod. Jedná se o kód typu 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.

  • Velké množství dočasných proměnných. Podívejte se na následující kód:
    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().

  • Složité přípravné fáze testů. Pokud máte test podobný tomuto, zbystřete. Je možné, že dochází k porušování Deméteřina zákona.
    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.

Kdy se o porušení nejedná

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';

Další čtení

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.

Obsah tohoto pole je soukromý a nebude veřejně zobrazen.

Filtrované HTML (využíváno)

  • Povolené HTML značky: <a href hreflang> <em> <strong> <cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <table>
  • Zvýraznění syntaxe kódu lze povolit přes následující značky: <code>, <blockcode>, <bash>, <c>, <cpp>, <haskell>, <html>, <java>, <javascript>, <latex>, <perl>, <php>, <python>, <ruby>, <rust>, <sql>, <text>, <vim>, <xml>, <yaml>.
  • Řádky a odstavce se zalomí automaticky.
  • Webové a e-mailové adresy jsou automaticky převedeny na odkazy.
CAPTCHA
4 + 10 =
Vyřešte tento jednoduchý matematický příklad a vložte výsledek. Např. pro 1+3 vložte 4.
Nějak se mi tady rozmohl spam, takže poprosím o ověření.

Jan Flos (neověřeno)

7 years 6 months zpět

Naprosto nejlepší vysvětlení, co jsem kdy četl. Jsem nadšen.