Chyby v návrhu: porušování principu "Tell, don't ask"

Od Petr Zemek, 2013-08-11

V dnešním díle seriálu o chybách v návrhu se podíváme na chyby, které objektově orientovaný kód degradují na procedurální úroveň. Zaměříme se na porušování principu "Tell, don't ask", který říká, že byste měli objektům říkat, co po nich chcete, a nikoliv se jich vyptávat a pak činit rozhodnutí za ně.

Úvod

Jeden z prvních principů, který se programátoři o objektově orientovaném paradigma naučí, je zapouzdření (angl. encapsulation). Tento princip říká, že by stav objektu neměl být přístupný navenek a o jeho změnu by se měl starat pouze dotyčný objekt. Ten navenek poskytuje rozhraní, pomocí kterého s ním může okolní svět komunikovat. Porušení tohoto principu může vést k tomu, že objekt degradujeme na pouhou Céčkovou strukturu, kdy se o její konzistenci budou muset starat uživatelé a velmi snadno dojde k jejímu narušení. Pokud však budou objekty zapouzdřené, tak změny stavu může provést pouze sám objekt, který tak zaručí, že jeho data budou vždy v konzistentním stavu. Princip zapouzdření má i řadu dalších výhod, např. že je možné změnit implementaci objektu, aniž bychom museli měnit rozhraní a informovat o tom všechny uživatele.

S principem zapouzdření souvisí jiný, méně známý princip: Tell, don't ask. Tento princip říká, že bychom s objekty měli komunikovat tak, že jim řekneme, co chceme udělat, a nikoliv tak, že se jich budeme vyptávat na jejich stav a na základě toho provedeme rozhodnutí, co dělat. Porušení tohoto principu má za následek to, že objektově orientovaný kód degradujeme na procedurální kód, čímž přijdeme o výhody objektově orientovaného programování. Pěkně to vystihl Alec Sharp ve své knize Smalltak by Example (strana 67):

Procedural code gets information then makes decisions. Object-oriented code tells objects to do things.

Ve zbytku příspěvku si ukážeme tři příklady porušení tohoto principu, kde se u každého z nich dozvíme, co a proč jsme udělali špatně a jak to napravit.

První ukázka

Mějme třídu Function, která reprezentuje funkce, a třídu FunctionCall, která reprezentuje volání funkce. Dejme tomu, že máme volání funkce v objektu fc a chceme zjistit, zda je možné do tohoto volání přidat další argumentu. Co nás napadne, tak je napsat takovýto kód:

FunctionCall fc = ...;
Function f = fc.getFunction();
unsigned numOfArgs = fc.getNumOfArgs();
if (f.getNumOfParams() > numOfArgs || f.isVarArg()) {
    // Můžeme přidat další argument.
    // ...
}

Když se nad tím zamyslíme, tak další argument můžeme přidat, pokud má funkce nižší počet parametrů, než předáváme argumentů, nebo se jedná o funkci s proměnlivým počtem parametrů. Přesně toto vyjadřuje naše implementace. No jo, ale co když budeme toto zjištění chtít provést na mnoha místech v programu? Jako zkušení harcovníci se chceme vyhnout duplikaci kódu, a tak náš kód umístíme do funkce:

bool newArgCanBeAddedToFunctionCall(const FunctionCall &fc) {
    Function f = fc.getFunction();
    unsigned numOfArgs = fc.getNumOfArgs();
    return f.getNumOfParams() > numOfArgs || f.isVarArg();
}

Problém s duplikací vyřešen. Jupí! Co jsme ale efektivně udělali, tak je, že jsme do objektově orientovaného kódu zavlekli procedurální kód. Všimněte si, že funkce newArgCanBeAddedToFunctionCall() je typickým příkladem procedurálního kódu, který zjišťuje informace o jiném "objektu" na na základě toho činí rozhodnutí (vrací odpověď ano či ne). Výše uvedená konstrukce je typickou ukázkou funkce, která patří do třídy FunctionCall, protože operuje nad jejími daty a činí podle nich rozhodnutí. Pokud do třídy FunctionCall přidáme tuto metodu, tak bude její volání v kódu vypadat následovně:

if (fc.newArgCanBeAdded()) {
    // ...
}

Všimněte si, že nyní říkáme objektu fc, co od něj chceme a necháme jej se rozhodnout, co k tomu musí udělat, aby nám dal výsledek. Nemusíme se vůbec starat o to, jak to zjistí. Co je pro nás podstatné, tak je, že když se dostaneme do těla onoho podmíněného příkazu, tak lze do volání přidat další argument. Navíc, když jsme kód upravili, tak jsme se mohli zbavit onoho komentáře "// Můžeme přidat další argument.", protože to je jasné z názvu volané metody.

Druhá ukázka

Druhou ukázku jsem s jistými úpravami převzal odtud, protože se jedná o velmi výstižnou ukázku. Mějme třídy Widget a Panel, které nám reprezentují ovládací prvky v grafické aplikaci. Dejme tomu, že chceme odstranit Widget z prvku, který jej obsahuje. To ale můžeme udělat pouze tehdy, pokud existuje nějaký prvek, který jej obsahuje. Co dáme dohromady, tak je následující kód:

Widget &w = ...;
if (w.hasParent()) {
    Panel &parent = w.getParent();
    parent.remove(w);
}

Co v něm děláme, tak se ptáme na interní stav objektu třídy Widget (to, zda má otce, tedy prvek, který ho obsahuje) a na základě tohoto interního stavu děláme rozhodnutí. To je však porušení principu "Tell, don't ask". Nás by nemělo vůbec zajímat, jaký je interní stav objektu. Co chceme, tak je zrušit prvek z prvku, který jej obsahuje. Proto to uděláme tak, že ono zjišťování otce atd. přesuneme do třídy Widget, což nám umožní přepsat kód výše takto:

Widget &w = ...;
w.removeFromParent();

Přehledné a jasné. Navíc nyní nemusíme znát nic o fungování vnitřností objektů třídy Widget, což zlepšuje zapouzdření. Taktéž stejně jako v předchozím příkladu nyní nemusíme duplikovat kód na všech místech, kde chceme provést tuto operaci.

Třetí ukázka

Pokud ani po předchozích dvou ukázkách nemáte úplně jasno, tak věřím, že poslední ukázka vás osvítí. Mějme bázovou třídu Animal, z které dědí různá zvířátka. Dále mějme nějaké konkrétní zvířátko, o kterém ale nevíme, co je zač, takže můžeme použít pouze rozhraní třídy Animal. Chceme, aby se dané zvířátko pohlo o jeden krok dopředu. Co uděláme, tak je, že si zjistíme, kolik má zvířátko nohou a každé noze řekneme, aby se pohla:

Animal &animal = ...;
for (int i = 0; i < animal.getNumberOfLegs(); ++i) {
    animal.getLeg(i).makeStep();
}

Určitě cítíte, že toto je špatně. Když chcete svému psovi říct, aby se pohl, tak mu prostě řeknete "Pohni se!" a nedáváte příkazy každé jeho noze. Pes je sám schopen se o své nohy postarat. Řešení je jednoduché: přidáme virtuální metodu makeStep() do třídy Animal.

Animal &animal = ...;
animal.makeStep();

Kód dělá přesně to, co chceme: pohne zvířátkem. To, jak zvířátko pohyb provede, je čistě na něm.

Takže ještě jednou: principem "Tell, don't ask" je, že bychom měli objektům říkat, co po nich chceme, a nikoliv se jich ptát na interní stav a dělat podle něho rozhodnutí za tyto objekty.

Poznámka na závěr

Bystrý čtenář určitě zpozoroval, že mimo porušení principu "Tell, don't ask" došlo v původním kódu všech ukázek také k porušení Deméteřina zákona (angl. Law of Demeter). O tom si ale povíme jindy. Když jsme však kód upravili tak, aby neporušoval "Tell, don't ask", tak přestal porušovat i Deméteřin zákon, což je skvělé.

Další čtení

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
1 + 1 =
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í.