Pod celkem nevinným článkem u Martina Malého se rozproudila zajímavá diskuse na (vděčné) téma objektově orientovaného návrhu.
Pozadí je zhruba toto: Martin ve svém C# programu použil generickou třídu Dictionary<TKey, TValue> k ukládání čehosi a když mu vyvstala potřeba tuto hashmapu serializovat, zjistil, že třída Dictionary XML serializaci nepodporuje (protože Dictionary je z nejniternějšího jádra .NETu a ti mají zakázáno odkazovat se na věci z jmenného prostoru System.Xml). Udělal tedy jednoduchou věc – od Dictionary podědil a XML serializaci přidal.
Zatímco ve světě PHP by Martina obdivovali za to, jak krásně využil „novinku“ pětky (ano, OOP) a elegantně přidal funkčnost děděním, v nekompromisním a krutém světě .NETu okamžitě přispěchal Aleš Roubíček, aby mu vpálil do obličeje, že takto tedy ne a že tak se programovalo kdysi dávno, když ještě počítače fungovaly na ruční pohon. (Byla ta nadsázka v celém souvětí dostatečně zřejmá?)
Nechme teď stranou fakt, že pro Martinovy potřeby bylo dědění plně dostačující – procvičil si a dosáhl toho, co potřeboval, všechno bylo OK a svět byl krásný. Diskuse se pak ale stočila k obecným OOP principům a některé myšlenky byly zajímavé.
V podstatě se porovnávalo řešení implementované pomocí dědičnosti (tak, jak to udělal Martin), s řešením, kdy zodpovědnost za serializaci má cizí objekt, říkejme mu třeba XmlSerializer. Možná i proto, že v komentářích padlo zákeřné slovo „reflexe“, se nakonec Martin uchýlil k obecným pozorováním, jako třeba že „zase se ta houpačka zhoupla na druhou stranu“, „dosti bylo zapouzdření“, „teď můžeme objektu koukat pod pokličku“, „programátorští guruové 90. let by bez konceptu zapouzdření nedali ani ránu“ a podobně. (Viz např. komentáře 48 nebo 55.)
To všechno může vést k dojmu, že princip zapouzdření (tak dokonale implementovaný v Martinově původním řešení) jakýmsi způsobem stojí proti Single Responsibility principu, kde objekt Dictionary přestává být „black boxem“ a některé věci za něj dělají jiné objekty, nedej bože pomocí reflexe.
Ale pozor – princip zapouzdření není v rozporu se Single Responsibility principem. Ve světě OO návrhu neexistují žádné „módní vlny“, kdy se pár let preferuje zapouzdření, potom se od něj upouští, pak se k němu zase vrací atd. Zapouzdření je od začátku jedním z fundamentálních principů OOP a nikdy se to nezměnilo. Jen je potřeba pochopit, co je to ten bájný „black box“.
Jedna skupina lidí si myslí, že black box se tomu říká proto, že nevidíme dovnitř a je tedy pro nás jako pro uživatele velmi pohodlné konzumovat libovolné služby, které black box nabízí. Jak toho využít? Ano, správně – do black boxu toho nacpat co nejvíc. Uvnitř sice bude trochu nepořádek, ale to nám nevadí, my komunikujeme jen s uhlazeným rozhraním. Tito lidé chápou princip zapouzdření jako poučku „dělej si uvnitř objektu co chceš, klienty bude zajímat jen rozhraní“ – a zkušenost je naučí, že je to v podstatě pravda.
A pak existují dobří programátoři :) … Vtípky stranou, každý jednou zjistí, že udržovat objekt, který toho dělá moc, je prostě těžké. Proto už v 70. letech chytré hlavy daly dohromady tzv. Single Responsibility Principle, který říká, že každý objekt by měl mít jeden a právě jeden účel (zodpovědnost). Zodpovědností třídy Dictionary je implementovat hashmapu a přidání další zodpovědnosti (uložení na disk ve formátu XML) by bylo porušením SRP. Můj další oblíbený příklad, který jde skoro ad absurdum, je samotné vytváření objektů – návrhový vzor Abstract Factory a další vznikly kvůli tomu, že v určitých případech je dokonce užitečné oddělit objekt od aktu jeho vytvoření. V porovnání s tím vypadá přesunutí serializace do externího objektu skoro jako samozřejmost :)
SRP je jedno z nejužitečnějších pravidel pro udržovatelný kód a v praxi bych dokonce doporučil myslet na něj víc, než na nějaké zapouzdření (to je dobře zajištěno jazykem samotným, pokud umíte používat private, public a nejste vyloženě OO-prase). To, že objekt nějakým způsobem zkoumáte nebo ho modifikujete externím objektem (avšak stále přes jeho veřejné rozhraní!), není porušením principu zapouzdření.
Pokud se tedy vrátíme ke konkrétnímu příkladu se serializací, řešení využívající dědičnost naplňuje princip zapouzdření, zatímco řešení s využitím XmlSerializeru naplňuje princip zapouzdření a současně Single Responsibility Principle. Je tím pádem objektově čistší a není divu, že .NET framework používá právě tento přístup namísto toho, aby tam byla nějaká třída typu XmlSerializableDictionary.
Pokud si z tohoto článku odnesete jedinou věc, ať je to to, že zapouzdření neznamená nacpat do objektu co možná nejvíc, ale že byste se naopak měli řídit Single Responsibility principem.
„V určitých případech je dokonce užitečné oddělit objekt od aktu jeho vytvoření“ – nezbývá než souhlasit. Třeba u dětí je toto oddělení od aktu jejich vytvoření velmi žádoucí! ;)
Pardon, teď vážněji: Asi jsem četl jinou diskusi… ;) Říkal jsem něco trošku jiného než jaks to interpretoval zde. Takže to s dovolením trochu rozvedu:
Říkal jsem, že když jsem se v polovině 90. let OOP učil, tak tehdejší guruové hlásali myšlenku „zapouzdřit“ a „objekt se o sebe musí starat sám“. Byly určité náznaky, např. „friend“ třídy, ale základ bylo zapouzdření + dědičnost. Ono to ostatně tehdy ani moc jinak nešlo. Ačkoli „single responsibility“ bylo známo, tak při tehdejším stavu vývoje bylo spíš jako akademický koncept. Ostatně – jak dlouho že má .NET extension methods? :)
Od té doby jsem víc v podstatě nepotřeboval, v Javě jsem nedělal, v Object Pascalu (Delphi) to potřeba nebylo a v PHP pak už vůbec. TAKŽE když jste mi poslední dva dny vyprávěli o těch „samozřejmých kvalitních OOP věcech“, tak jsem si zjistil oč jde (protože se rád nechám poučit) – a co nevidím? Ústup od ortodoxních OOPistických pouček, co si z 90. let pamatuju… Ale můžu to naformulovat jinak, co takhle třeba: „Vývoj paradigmatu“? :)
Není to nic proti ničemu, nic špatného ani nějaké „uchýlení k obecným proklamacím“. Jazyky se vyvíjejí, stejně tak i přístup k programování a stejně tak i představa o tom, co je „moderní“. V tomto smyslu jsem to taky komentoval, asi jako Mirečkovo „To nás v Zahrádkách neučili!“ Rozhodně nepopírám Single Responsibility princip ani ho nestavím jako antipod zapouzdření, jen říkám, že je to posun proti praxi z 90. let – kurs otočený ne o 180, ale řekněme tak o 60 stupňů. A zrovna tak se odvažuju tvrdit, že za dalších 10 let přijdou do praxe další principy, které jsou dnes „akademické“ a dnešními prostředky nedosažitelné / neefektivní. Takže opravdu „houpačka“, kdy jsou „v kursu“ ty techniky, které současná technologie umožňuje.
Jak říká děda Simpson: „Bejval jsem v kursu, ale potom ho zas změnili a ten, ve kterém jsem teď, není ten správnej, a ten správnej mě děsí! To se stane i vám, hoši!“ :)
Takže když to shrnu: Učím se nový jazyk, sžívám se s jeho knihovnou a začínám tím, že si zkouším „staré dobré obecné a známé“ OO věci. Stejně tak si u nového mobilu nejdřív vyzkouším základní funkce tak jak je znám, a teprve pak přijde na řadu sžívání se „zkratkami“ a „novými postupy“, které mi novější technologie umožní. A samozřejmě jsem rád vždy, když mi někdo ukáže: „A víš že se to dá dělat i takhle?“
A závěrem do smířliva: Říkáš, že Single Responsibility je pravidlo, které má mít programátor na paměti. Já souhlasím a přidám k tomu dodatek: Pravidlo je dobrý sluha, ale špatné dogma. ;)
Takže přeji dobrý večer a – měj strpení se starším mužem, co si cestu k .NET prošlapával pětadvacet let od strojového kódu 8008 a TinyBASICu… :)
PS: Děkuji za tip na extension methods a děkuji za všechny další tipy, které mi v budoucnu rád a ochotně budeš poskytovat! ;) (To máš za to, že ze mne děláš v článku tak trochu osla! ;) )