... aneb snižujte závislosti mezi třídami
Jedna z možných definic návrhového principu Tell, Don't Ask (TDA) zní takto:
"Každé rozhodování zcela závislé na vnitřním stavu objektu by mělo být prováděno uvnitř tohoto objektu."
Princip hovoří o tom, že bychom měli navrhovat třídy tak, aby se volající strana nemusela dotazovat na záležitosti týkající se vnitřního stavu volané třídy.
Ukázka porušení TDA
Mějme zjednodušený autobusový rezervační systém:
class Bus
{
private int numberOfSeats;
private IList<Passenger> passengers = new List<Passenger>();
public Bus(int numberOfSeats)
{
this.numberOfSeats = numberOfSeats;
}
public bool HasFreeSeat
{
get
{
return numberOfSeats > passengers.Count;
}
}
public void AddPassenger(Passenger passenger)
{
passengers.Add(passenger);
}
}
class BusReservation
{
public void AddPassengerToBus(Bus bus, Passenger passenger)
{
if (bus.HasFreeSeat)
{
bus.AddPassenger(passenger);
}
}
}
Proč je uvedené řešení problematické:
- Základním principem OOP je zapouzdřenost interních dat objektu a jeho chování. Třída
Bus
však zbytečně zveřejňuje informaci o volných sedadlech a nutí volající stranu, aby s touto informací pracovala a zohledňovala ji před voláním metodyAddPassenger
. - Třída
BusReservation
je závislá na tříděBus
na dvou místech. Dotazováním na vlastnostHasFreeSeat
a voláním metodyAddPassenger
. Každá nadbytečná závislost mezi třídami zvyšuje komplexitu návrhu a zhoršuje vlastnosti systému. - Třída
Bus
nutí volající stranu, aby měla znalost o tom, že před zavolánímAddPassenger
si musí nejprve sama ověřit splnění kontraktů přidání cestujícího. S tím souvisí i závislost na správném pořadí volání. Samostatné voláníHasFreeSeat
nedává smysl. - Co když se v budoucnu změní podmínky, za kterých je možné přidat cestujícího? Nyní je přidání závislé pouze na volném sedadle, ale nově může přibýt podmínka typu
"je autobus pojízdný". Pak bude nutné doplnit test na vlastnost
IsMobile
na všechna místa voláníAddPassenger
. - Každá nadbytečná závislost zvyšuje komplexitu jednotkových testů. Pokud chceme testovat třídu
BusReservation
v izolaci, musíme mockovat tříduBus
(nebo lépe rozhraní). V našem problematickém případě musíte přidat chování mocku pro vlastnostHasFreeSeats
a metoduAddPassenger
.
Vhodnější řešení
Odstraníme závislost volající třídy na dotazování se na volné sedadlo. Veškeré testy na proveditelnost akce přidání cestujícího jsou zapouzdřeny uvnitř metody
AddPassenger
. Pokud některá z podmínek není splněna, je vrácena výjimka, kterou zpracuje volající strana. Volající straně se situace zjednoduší.
class Bus
{
private int numberOfSeats;
private IList<Passenger> passengers = new List<Passenger>();
public Bus(int numberOfSeats)
{
this.numberOfSeats = numberOfSeats;
}
private bool HasFreeSeat
{
get
{
return numberOfSeats > passengers.Count;
}
}
public void AddPassenger(Passenger passenger)
{
bool isFull = !HasFreeSeat;
if (isFull)
{
throw new Exception("Bus is full.");
}
passengers.Add(passenger);
}
}
class BusReservation
{
public void AddPassengerToBus(Bus bus, Passenger passenger)
{
bus.AddPassenger(passenger);
}
}
Vezměte si na pomoc Adapter
Pokud využíváte rozhraní, které nemůžete měnit (komponenta třetí strany) a které porušuje TDA, můžete si pomoci návrhovým vzorem Adapter (Wrapper).
Např. v jazyce C# je za příkazem foreach
schován adaptér, který zjednodušuje práci se vším, co zveřejňuje metodu IEnumerator GetEnumerator()
.
Z IEnumerator
pak postupně volá metodu bool MoveNext()
a dotazuje se na vlastnost object Current { get; }
. Více na stackoverflow.com.
Závěrem
Tak jako většina principů a doporučení objektového návrhu, tak i TDA není možné bez rozmyslu aplikovat dogmaticky ve všech situacích. Záleží na zodpovědnosti navrhované třídy, zda-li poskytuje příkazy (commands) k vykonání nějaké akce nebo dotazy (queries) vracející vnitřní stavy objektu. V případě příkazů byste však měli o dodržování principu TDA usilovat.
Díky za článek.
OdpovědětVymazatVe správném kódu je použita výjimka což je anti-pattern. Výjimky by se neměli používat pro zcela běžné situace.
Add passanger by mělo vracet bool. Volající třída každopádně musí být připravena na obsazený bus.
TT
Tomáši, díky za připomínku.
VymazatVyvolání výjimky v AddPassenger by nemělo být porušením nějakého dobrého mravu. Nepřidání uživatele může nastat z více důvodů. Na tento stav třída Bus zareagovat neumí a proto vyvolá výjimku. Volající strana (třída BusReservation) dostane kontext neúspěchu operace. Je otázkou, zda-li bude na chybu nějak reagovat nebo nechá jednoduše výjimku propagovat do "vyšších" tříd. Přidání cestujícího může být pouze jedním z mnoha kroků větší transakce, která si jako celek výjimku zachytí.
Myslím, že používání výjimek kód zpřehledňuje a zlepšuje čitelnost.
Třída BusReservation nutně nemusí na nepřidání cestujícího reagovat. Záleží, jak jsou nadefinovány její role a zodpovědnosti.
Pokud máš odkaz na nějaký zdroj pro podporu Tvého tvrzení o anti-patternu, prosím přidej link. Děkuji.
Roberte i Vlaďko, díky za reakce. Odpovídám později, dovolenkoval jsem :)
VymazatObecně se držím principu nepoužívat výjimky na běžný chod programu a opak považuji za anti-pattern. Za běžný chod který považuji i pokus o rezervaci v již plném autobuse. Příklad: osoba se zobrazí jízdní řád, po pár vteřinách nebo minutách klikne na rezervovat místenku.
Zdroj: Joschua Blooch, Effective Java, chapter 9 Exceptions. (Prakticky cituji poslední větu na straně 243, ale chce to přečíst o kontext) http://books.google.cz/books?id=ka2VUBqHiWkC&printsec=frontcover&source=gbs_atb#v=snippet&q=%22exceptions%20are%20designed%22&f=false
Na druhou stranu uznávám tvůj i Vlaďčin argument - že použitím výjimky je návratová hodnota flexibilní bez změny kódu třídy Bus, která bude časem volat další kontroly a jen propouštět jejich výjimky dále. Samozřejmě záleží na další stavbě programu. Použitá výjimky mi ale přijde vhodné jen při užší dostupnosti metody než public. (V Javě do package protected, v .Net do internal). Ale třeba je Váš názor jen modernější. Nebudu jej teda nadále považovat za anti-pattern :).
Obecně se dá řící, že programátoři velmi rádi vracejí NULL, false atp, než aby vyhodili vyjímku, typicky metoda GetById(Id) má vyhodit vyjinku.
OdpovědětVymazatAle Robert C. Martin by určitě raději vyhodil vyjimku a mě osobně se také víc líbí vyhození výjimky.
Nicméně není nic proti ničemu tam podobnou metodu mít (lepší by byla asi metoda CanAddNěco, která by pořešila jak počet sedadel, tak mobilitu atp)