Proč jsou statické helpery zlo?
|
Minulý týden psal Arthur spotík o takovém tom víkendovém programování, ve kterém šlo hlavně o to, že přešel na Vistu, vytvořil si gadget pro přehrávání ČRo a taky editor hosts
souboru. Napsal ho v C# a zmínil se o tom, že použil techniku, se kterou nejsem s to…
Zkrátka podědil si generickou Dictionary<TKey, TValue>
a rozšířil ji o serializaci. Dictionary není serializovatelná. Je to dáno jak její generickou povahou (constraints pro atributy neexistují), tak tím, že sídlí v knihovně, která nesmí mít závislost na jiné, která serializaci zajišťuje.
public class SerializableDictionary<TKey, TValue>
: Dictionary<TKey, TValue>, ISerializable {
// ...
}
To byla moje chvíle, vyčmuchal jsem zde codesmell (zbytečné dědění, které by šlo vyřešit kompozicí). Neomaleně jsem vstoupil do diskuse chválící gadget a sprostě Arthura pohanil. Omlouvám se za to. Mým úmyslem bylo ho postrčit o kus dál, možná rozšířit obzory. Prostě sprostá rada, která by mi v začátcích se C# pomohla nebloudit labyrintem macatých tříd se spoustou dovedností a košatou hierarchií předků a potomků.
Alternativní řešení
V diskusi, kterou jsem tímto vyvolal, padlo i to, že jde o porušení Single Responsibility Principle, které už rozebral Borek. Arthur tedy nabídl, alternativní řešení, jak se s problémem vypořádat:
Samozřejmě jsem si mohl napsat i kašpárkovskou třídu, co má dvě statické metody (Serializuj a Deserializuj) a veřejný parametr „Dictionary“… šlo by to vyřešit mnoha způsoby, lišícími se co do pracnosti, elegance a efektivity, a já se rozhodl nedělat „composition blackbox“, ale podědit třídu – především proto, že bych musel zpřístupňovat spoustu veřejných metod od původní generiky, nebo deklarovat vložený objekt jako veřejný.
public static class DictionarySerializer<TKey, TValue> {
public static byte[] Serialize(Dictionary<TKey, TValue> dictionary) {
// ...
}
public static Dictionary<TKey, TValue> Deserialize(byte[] data) {
// ...
}
}
Přejdu teď composition blackbox – vrátím se snad k němu někdy jindy, ale zastavím se u statických metod. Myslím si totiž, že to je další cesta do pekel… :)
Staticky vs. Instančně
Na toto téma se vedlo jedno z vláken té jinak pěkné diskuse o šikovném gadgetu. (Platíte koncesionářské poplatky?) Osobně zastávám názor, že je lepší se statickým helperům vyhýbat. Jednak ze zkušenosti a druhak z některých signálů a pouček správného OO návrhu.
V objektových jazycích odvozených od C jsme si zvykli používat třídy. Třída je předpis, podle kterého se vyrábějí objekty. Objekty jsou přenašeči dat nebo službami. Mohou být tedy obdařeny rozhraním, přes které s ním komunikují jiné objekty. Jenže ve světě Céčkových jazyků, můžeme mít tzv. statické členy. Statický člen existuje jen jednou, po celou dobu života aplikace.
Jenže, co je to život aplikace? Nevím jak je to přesně v PHP, ale myslím, že tam je to cca jeden požadavek. Statické proměnné tu slouží jako šikovná náhrada globálních proměnných a statické metody dovolují volat akce bez nudného vytváření instance třídy. To je jistě super. Ve světě dotnetu je to však mnohem složitější. V ASP.NET žije aplikace dokud ji někdo nevypne, tj. neresetuje aplikační pool, nezmění konfiguraci nebo neaktualizuje binárky. Takže u statického členu nikdy netušíte, jak dlouho bude žít.
Dalším problémem je konkurence. V PHP se aplikace spouští při každém požadavku a po jeho skončení opět vyhnije a to vše se děje v rámci jednoho vlákna. V ASP.NET žije aplikace mnoho požadavků, které jsou zpracovány samostatnými vlákny. Vzhledem k tomu, že statické členy jsou v aplikaci jen jednou a to pro více vláken, musíte najednou začít řešit konkurenční přístup, k těmto členům.
Další z problémů statických členů je ten, že třída není objekt, tudíž by se s členy třídy nemělo počítat v objektovém návrhu. :) Statické metody ani vlastnosti nelze popsat rozhraním, při jejich užití vzniká úzká vazba. Takovéto členy se také krásně vyhýbají dědičnosti. Nelze je přepsat tradičními postupy.
Takhle bych ve zkratce shrnul hlavní nevýhody statického přístupu. Platí jen pro určité případy. Nemusíte se s nimi nutně setkat a pro váš konkrétní případ mohou být i výhodné. Pokud však navrhujete API, které má být znovupoužitelné (hlavní výhoda OOP), nikdy nevíte, v jakém prostředí poběží. A když už píšeme objektově, tak proč rovnou nepsat tak, aby naše třídy znovupoužitelné byly?
Jak tedy s těmi helpery?
Jak říká Michal Bláha, „statický helpery jsou výbornej syntax sugar.“ Jasně, proč ne? Ve skriptech může být statický helper jasným přínosem. Je ale třeba myslet na některá pravidla.
Základním pravidlem, pokud chci vytvořit statický helper, je vytvořit instanční implementaci. Dobrou ukázkou tohoto přístupu je například System.IO.File
, který je statickou fasádou nad System.IO.FileInfo
. Podobně funguje např. i metoda Rarous.TexyNet.Texy.Process
, která slouží k rychlému převodu textu do HTML, ale je tu i silné instanční API, které tato metoda volá.
Závěr
Statické helpery jsou zlo, když jsou primárním návrhem a taky jediným. Pokud dosahujete kompozice objektů pomocí dependency injection, jsou pro vás statické helpery nedosažitelné (bez napsání instančního wraperu). Když píšete unit testy, nemůžete snadno statický helper nahradit. Pokud máte statické vlastnosti, musíte myslet na konkurenční přístup a životnost zdrojů, které takto zapouzdřujete.
public interface IDictionarySerializer<TKey, TValue, TData> {
TData Serialize(IDictionary<TKey, TValue> dictionary);
IDictionary<TKey, TValue> Deserialize(TData data);
}
Přece jen existuje více cest k cíli a je možné, že se někde šeredně pletu. Jaký je váš pohled na věc?