\

Školení Návrhové vzory, OOP a UML


 Sunday, June 05, 2011
O špatně chápaném principu jedné odpovědnosti třídy (SRP) a o zneužívání myšlenek Domain driven designu (DDD)

 

Dnes na twitteru David Grudl odkázal na debatu, která se týká vlastností v PHP. O vlastnostech v PHP mluvit nechci, ale v tomto  příspěvku se chci dotknout některých “dogmat”, které se ozývají stále častěji a které byly použity jako univerzální kladivo na oponenty  i v odkazované diskuzi.

Jedno zvláštní dogma se týká principu jedné odpovědnosti třídy (Single responsibility principle). Tento princip říká, že třída by měla mít jednu přesně vymezenou odpovědnost, která je v souladu s jejím názvem. I když na první přečtění se tento princip zdá neproblematický, dá se zneužít jako univerzální kladivo. Dogmatici mi říkali, že jedna odpovědnost si vynucuje, aby třída vždy měla právě jednu metodu, která tuto odpovědnost realizuje. Není nad přehledný svět objektových dogmatiků, kde objekt je jen stupidní kontajner na jednu (de facto globální?) funkci.Smile

Dogmatiky tohoto zvláštního ražení  zanedbejme jako ztracené případy a SRP obohaťme o další vysvětlení, které říká, že třída by měla mít jen jeden důvod ke změně. Tento princip je užitečný v tom, se snaží z aplikací odstranit všemocné božské (God) objekty, které mnohdy už svým názvem signalizují, že řeší spoustu věcí. UniversalOrderAndInvoiceProcessor oznamuje, že se bude měnit nejen, když se změní zpracování objednávek, ale také když se změní zpracování faktur. Jednoduché, což? Proč o tomhle jednoduchém principu vůbec dále mluvit?

V diskuzi se o SRP mluví (viz i příspěvky níže), ale diskutující tam ve  své argumentaci používají něco, čemu na kurzech u SRP říkám falešné alternativy.

Mějme stejně jako v diskusi svou třídu Image, která nese informace o obrázku. Obrázek chceme uložit.

Varianta 1, kdy obrázek nese informace a současně nabízí metodu Save, ve které uloží data do souboru.

Co je v diskuzi vyčítáno této třídě? Porušuje princip jedné odpovědnosti, protože podle některých (Jiří Knesl, Ondřej Mirteš)  řeší dvě věci najednou – nese data o obrázku a současně data ukládá. Souhlasím, že jde o porušení SRP, ale hlavním důvodem je to, že metoda Save je napsána tak,  že třídu Image ukládáme vždy do souboru. Co když budeme chtít třídu Image uložit do nějakého “response” streamu na webovém serveru, nebo uložit přímo do databáze? Tuto třídu skutečně budeme měnit ze dvou důvodů – jednou, když přidáme nebo odebereme informace o obrázku a také, když budeme chtít ukládat obrázek do databáze, musíme rozšířit stávající metodu Save, což povede k tomu, že metoda bude mít v sobě nějaký podivný switch a  bude trpět smíšenou odpovědností, protože bude dělat několik věcí najednou,  nebo můžeme přidat novou samostatnou metodu SaveToDb.

Jedinou (!?) alternativou v diskuzi k tomuto postupu je vyvedení odpovědnosti za ukládání do různých úložišť do samostatných objektů, které mohou být  skryty za jednotným rozhraním.

Toto řešení důsledně separuje odpovědnosti, navíc je velmi snadné přidat další implementaci rozhraní IImagePersistor, např. DbPersistor, který data uloží do databáze. Už v diskuzi Jakub Vrána ale upozorňuje na to, že se mu nelíbí, jak se řešení komplikuje pro uživatele-vývojáře, který s třídami bude pracovat, protože tento vývojář musí vědět, že existuje nějaký IImagePersistor/FilePersistor odpovědný za uložení dat. Třída Image nestačí k tomu, abyste dokázali vygenerovat data obrázku a uložit je, což může být ve vaší knihovně častý scénář. Také bych rád poprvé v tomto článku připomněl princip OOP, ke kterému se za chvíli vrátím, že objekt představuje jednotu svého stavu a chování, které je pro tento stav definováno.

Psal jsem o falešných alternativách, můžeme najít i jiná řešení. Co ponechat metodu Save ve třídě Image, ale z třídy Image udělat tzv kompozitor - objekt, který skládá své chování tak, že využívá další pomocné objekty, na kterých závisí, a nabízí intuitivní rozhraní pro klienty.

Odpovědnosti jsou stále separovány a dokonce třída Image, náš kompozitor, dodržuje pravidlo, které říká, že kompozitor by měl být jednodušší než suma funkcí jeho pomocných objektů.  Klient třídy Image nemusí pracovat přímo s třídou FilePersistor, a přitom nemáme kód pro ukládání do souboru přímo ve třídě Image. Problém je, že metoda Save třídy Image vždy vytváří FilePersistor. Klient třídy Image si nemůže vyžádat to námi dříve zmiňované ukládání obrázku do databáze, a navíc třída Image závisí na jedné konkrétní třídě FilePersistor, u níž přímo volá konstruktor. V třídě Image mixujeme vytváření grafu spolupracujících objektů se samotným použitím pomocných objektů. Opět jde o dvě odpovědnosti, které bychom měli oddělit – SRP, nezapomeňme.

Nejprve ale zkusme vyřešit problém s tím, že klient nemůže ukládat data do databáze, protože třída Image ukládá data vždy do souboru.

Jednoduše přidáme další variantu metody, která přijímá odkaz na IImagePersistor, v našem případě třeba na DbPersistor. Původní metoda Save bez argumentů řeší ukládání do souboru. Ukládání do souboru je nejčastější scénář, který je zvolen jako výchozí. Stále ale tady máme problém s tím, že v metodě Save konstruujeme "natvrdo" FilePersistor. A navíc naše API klientům trochu lže. V podtextu klientovi sděluje, že výchozí metoda Save nemá žádné další závislosti, i když z implementace, !a jen z implementace!,  je zřejmé, že jsme závislí na přítomnosti třídy FilePersistor. Poznámka: V C# 4 můžeme použít volitelné argumenty u jedné metody, ale na principu této varianty řešení se moc nemění.

Zkusme naše prozatím ulhané API vylepšit a dodržet SRP.  Oddělme nyní konstrukci pomocných objektů, na kterých závisíme, od jejich použití v metodě Save.

Objekt Image si nyní v konstruktoru vynucuje předání IIMagePersitoru. Když klient IImagePersistor nepředá, objekt nezvznikne – sám konstruktor garantuje, že buď objekt Image má vyplněny všechny závislosti, nebo vůbec nevznikne. Vytvořili jsme konstruktor, který může použít a automaticky naplnit DI kontajner, nebo různé abstraktní továrny registrované v DI kontajneru  apod. DI kontajner je přesně tím objektem, který by měl být v aplikaci odpovědný za konstrukci grafu objektů, v metodě Save objektu Image injektovaný IImagePersistor jen používáme. SRP v praxi.

Možný že ale v tomto případě je injektování závislostí přes konstruktor moc striktní. Co když nám skutečně vyhovuje, že můžeme bez DI kontajneru vytvořit objekt Image, který bude data ukládat do souboru. Pak můžeme využít injektování přes vlastnosti, kdy příslušnou vlastnost po vzniku objektu vyplníme rozumnou výchozí hodnotou – v našem případě instancí FilePersistoru. Poté ale platí, že třídu Image stále částečně zatěžujete konstrukcí objektů…
U většiny DI kontajnerů je preferováno injektování závislostí přes konstruktory, všechny, které znám,  si ale poradí ale i s injektováním závislostí přes vlastností a u MEFu bych řekl, že injektování závislostí pomocí vlastností hrají prim.

Všechny tyto varianty mají své výhody a nevýhody a asi nemusím zdůrazňovat, že ani jedna není univerzálním kladivem. Varianty s injektováním závislostí (konstruktor, metoda, vlastnost) jsou samozřejmě mnohem lépe testovatelné.

Dokážu přidat i další příklady, ale chtěl jsem,  abyste viděli, že SRP není ani nesmysl, ale ani princip, který by, podobně jako to zaznělo v diskuzi, sděloval – existují jen dvě alternativy, jak rozdělovat odpovědnosti, a ZROVNA TA TVOJE JE ŠPATNĚ.

A poslední  poznámka:

Jiřé Knesl také v diskuzi uvedl: “ objekt buďto data reprezentuje (pak má settery/gettery), nebo vykonává činnost (pak dostane data parametry)”. Tohle je podle mě  postoj blízký hlavně některým Javistům, o čemž svědčí i podle mého soudu schematický a nevěrohodný článek, který se zabývá vlastnostmi v Javě a na který se J. Knesl odkazuje. Znovu připomínám, že objekt představuje jednotu svého stavu a chování, které je pro tento stav definováno.  Objekt, který má jen gettery a settery, je ”krabičkou na data”, pouhou strukturou známou i z neobjektových jazyků, a když má objekt jen metody, tak jde o (v mnoha případech skutečně globální) funkce/procedury, které prefixujeme názvem proměnné/třídy. V diskuzi to myslím nezaznělo, ale když někdo razí tuto drastickou separaci chování od samotných dat, často dodává, že takto je to přece definováno Evansem, tedy autoritou,  v kanonické knize o Domain Driven Designu. Když se ptám, kde o tom Evans mluví, dozvím se, že Evans má objekty, které mají svůj stav (vlastnosti)  a s objekty pracují speciální business-doménové služby (chování). I když mám vůči DDD spoustu výhrad, zde Evanse špatně interpretují – Evans by model, kde objekty mají jen stav a nemají žádné chování, nazval anemickým modelem – izolovaná data podepřená berličkami nesouvisejících globálních funkcí. Business služby jsou, zjednodušeně řečeno, určeny pro zapsání složitější business logiky, na které spolupracuje více objektů a žádný participující objekt není sám o sobě přirozeným kandidátem, do kterého by bylo vhodné logiku situovat.

Zde bych mohl pokračovat dále k rozdělení objektů v DDD, ke skutečnému významu vlastností u objektu, co říká princip “tell, don't ask”, ale už teď mi původně krátký komentář k SRP a DDD až moc nabobtnal. Když budete mít zájem o další naznačená témata, napište prosím komentář k článku.



Sunday, June 05, 2011 9:13:04 PM (Central Europe Standard Time, UTC+01:00)       
Comments [19]  Analytické drobky | C# | Návrhové vzory | UML


Sunday, June 05, 2011 11:25:57 PM (Central Europe Standard Time, UTC+01:00)
A co třeba metoda resize()? Může si ji třída Image dovolit implementovat sama nebo rozhodně musíme mít interface IImageResizer a třídy ho implementující?

Dál by mě zajímalo, co bude místo těch tří teček u File.Open. Ono to je totiž kupodivu docela podstatné. U obrázků totiž málokdy chci uložit RawData, ale spíš se hodí nějaký standardní formát, třeba JPG nebo PNG. A tento formát by měl korespondovat s názvem souboru. Takže když chci obrázek uložit jako "krajina.jpg", tak očekávám, že se uloží ve formátu JPG. Kde bude tahle logika?
Monday, June 06, 2011 12:10:28 AM (Central Europe Standard Time, UTC+01:00)
Ad 1) Metodu Resize mít objekt může a může ji i implementovat. V některých řešeních se vám ale transformace s obrázky mohou začít (Rotate, Skew) množit, a proto řešení refaktorizujete a zavedete raději ImageServices, aby nemusela vše řešit jedna třída. V průběhu návrhu a vývoje se objevují další doposud skryté doménové objekty a služby.

2) Když zachovám maximum ze svého řešení a použiju dekorátora. Řadu dekorátorů může zase sestavit DI kontajner.
https://gist.github.com/1009518
RawData v konvertovaném obrázku představují pole bajtů ve speciickém formátu (JPEG).

Je samozřejmě možné nekonvertovat celý obrázek v paměti a nevytvářet kopii Image, ale dekorátor může předat podkladovému objektu vrátit "Lazy" objekt Image, který bude data z pole RawData číst a konvertovat postupně - v dávkách.

Dá se udělat i řešení bez dekorátora, kdy bude do tříd IImagePersistor vždy injektován pomocný objekt ImageConverter se svým rozhraním pro převod do různých formátů.

Atd...
Monday, June 06, 2011 12:24:12 AM (Central Europe Standard Time, UTC+01:00)
Ještě chci upozornit na, že když objekt Image má metodu Save, kteřá přijímá název obrázku a rozhraní IIMagePersistor přijímá také tento argument, tak se předpokládá, že klient chce vždy uložit obrázek pod daným jménem a že jednotlivé IIMagePersistory rozhodují o tom, jak s názvem naloží. FilePersistor uloží data do souboru s vyžadovaným jménem, DbPersistor uloží jméno třeba do sloupce Name v databázi. Jinými slovy - pokud název obrázku má klient předávat vždy, předpokládá se, že pro každý perzistor má název význam a že nejde jen o způsob, jak si vynutit uložení obrázku v určitém formátu.
Pokud by tomu tak nebylo, bylo by lepší, kdyby klient mohl požadovaný formát obrázku určit i třeba přes nějakou enumeraci ImageType, která bude předána další variantě metody Save a každý perzistor musí být schopen vygenerovat i nějaké "výchozí" jméno obrázku a toto vygenerované jméno by měl i objekt Image umět klientovi vrátit...
Monday, June 06, 2011 7:22:07 AM (Central Europe Standard Time, UTC+01:00)
Dobry den,
ja bych mel zajem o Vase zkusenosti ohledne DDD. Mohl by ste prosim zminit nektere z vyhrad, ktere k DDD mate?

Dekuji mnohokrat
Monday, June 06, 2011 7:36:58 AM (Central Europe Standard Time, UTC+01:00)
Aleš: Něco podrobného určitě sepíšu. Před nedávnem jsem se trochu vyjadřoval k DD zde.
http://forum.builder.cz/read.php?160,3318125
Monday, June 06, 2011 8:07:57 AM (Central Europe Standard Time, UTC+01:00)
Po přečtení diskuze plné dogmat byl toto osvěžující článek, navíc s vysokou informační hodnotou. Díky!
Monday, June 06, 2011 8:48:50 AM (Central Europe Standard Time, UTC+01:00)
Super čtení. Bylo by fajn popsat celý SOLID. Myslím, že podobné informace na českém internetu chybí a je to určitě škoda
landy
Monday, June 06, 2011 10:26:46 AM (Central Europe Standard Time, UTC+01:00)
Proč je odkazovaný článek nevěrohodný? Jeho autor Allen Holub (http://www.holub.com/ http://en.wikipedia.org/wiki/Allen_Holub) je lektorem OOP, první knihu o OOP napsal před 20 lety, učí na několika univerzitách, psal pro JavaWorld, IBM DeveloperWorks. Nechci tady argumentovat "hrdina musí mít vždy pravdu" - jen mi přijde divné, že pokud někdo ostatní lidi poučuje o OOP už 20 let, tak se na to, že vlastně OOP nechápe, nepřišlo.

Dále: "Dogmatici mi říkali, že jedna odpovědnost si vynucuje, aby třída vždy měla právě jednu metodu, která tuto odpovědnost realizuje." - to ale v diskuzi od nikoho nezaznělo, přijde mi tedy taková věta (obzvlášť boldem) jako řečnická finta: "vlož někomu něco do úst a pak to vyvrať."

Co se anemického modelu týká, někdy je přirozené, že některé objekty mají jen settery/gettery - typicky config objekt. V případě e-mailu, nebo obrázku (jako jsou v Jakubově článku) existuje několik možností, jak se s potřebou "konat více operací na jedněmi daty" vyrovnat. Delegací, kategoriemi (jako jsou ve Smalltalku, Obj-C), dědičností (často chyba) nebo obrácením kontroly (nemyslím teď IoC), tedy vytvořením servisní vrstvy - což jste sám v komentářích přiznal, že může být žádoucí. A v tu chvíli se už dostaneme k tomu, že máme objekty, které konají a objekty, které reprezentují a anemický model to zjevně není.

Jinak děkuji za blogpost děkuju, za postupy při refaktorování třídy jsem schpný se sám postavit - sám bych třídu upravoval podobným postupem.
Monday, June 06, 2011 10:36:43 AM (Central Europe Standard Time, UTC+01:00)
landy: +1
Monday, June 06, 2011 10:45:30 AM (Central Europe Standard Time, UTC+01:00)
1) Jak to, že Image může implementovat metodu resize? Co když ji v budoucnu budu chtít vyměnit třeba za účelem použití jiného algoritmu (např. bikubický místo bilineárního)?

2) Aha, takže RawData bude pole bajtů ve specifickém formátu (JPEG)? A tohle RawData budou přepočítávat všechny metody, které s obrázkem pracují (třeba ta resize)? Budou ho zároveň také používat pro načtení jednotlivých bodů obrázku? Když pominu zbytečnou pomalost tohoto řešení, nebude vadit, že zrovna JPEG je ztrátový formát, jehož každé načtení a uložení zhorší kvalitu toho obrázku?
Monday, June 06, 2011 11:22:50 AM (Central Europe Standard Time, UTC+01:00)
Jiří Knesl:
Díky za reakci.
Omlouvám se, pokud to vyznělo tak, že vám vkládám do úst větu: "Dogmatici mi říkali, že jedna odpovědnost si vynucuje, aby třída vždy měla právě jednu metodu, která tuto odpovědnost realizuje"
Článek se netýkal jen diskuze u Jakuba Vrány a na diskuzi odkazuju až níže, kdy vyřídím úplné nesmysly. Pokud chcete, v článku to ještě více zdůrazním.
Žádný rétorický trik to nebyl, stejně jako Vám nepodsouvám, že ani u vás není odkaz na Allena Holuba není argumentace autoritou.
Každý z nás píšeme lepší a horší články a ani Allen není neomylný. Když se podívám do diskuze, vidím, že s ním také mnoho lidí nesouhlasilo. O vlastnostech napíšu asi jindy, tady jsem nechtěl do debaty zasahovat, protože si myslím, že PHP řeší některé věci jinak než C# a nechtěl jsem nikoho poučovat bez znalosti věci.

Ano - občas (a závisí na vrstvě aplikace) máme jen nosiče dat s gettery a settery a služby (i v DDD - aplikační, infrastrukturní) složené pouze z metod. Já jsem se jen bránil tomu, že by bylo vhodné vždy usilovat o separaci dat od chování, což k anemickému modelu vede. Alespoň tak jsem pochopil vaše věty.
Myslím, že i Evans několikrát narážel na to, že lidé jeho odstavce o službách pochopili jako ospravedlnění anemického modelu.

Ještě jednou se omlouvám, pokud se vás dotkla ta zmínka o dogmaticích. Už delší dobu jsem chtěl napsat podobný článek a včera byl čas a diskuze u Jakuba Vrány mi nahrála téma.
Monday, June 06, 2011 11:38:41 AM (Central Europe Standard Time, UTC+01:00)
Jakub Vrána:
Ad Resize) Rozhraní by mělo být stabilní, implementace se může měnit. Když chcete změnit algoritmus, změníte vnitřek metody, když budete chtít používat "na střídačku" obě metody nebo dkonce více metod a nejste fanda IFů, použijete tzv. adjustments (mini strategie, které zapouzdřují oba algoritmy) a na které metoda Resize deleguje - protože už by její kód byl moc komplikovaný. Klient ale stále používá stejné rozhraní - metodu Resize.

BTW: Otázky "co kdyby" jsou dopředným designem, takto se dá smést ze stolu postupně jakokoli řešení

Ad RawData) Chtěl jsem nějak reprezentovat obrázek, RawData jako bajtové pole mi přišlo jako všem srozumitelná vlastnost. Mohl bych také začít mluvit o LockBits apod.
RawData mohou nést cokoli - bitmapu, po konverzi v dekorátoru RawData v *novém dočasném objektu Image pro uložení* mají JPEG.

Image si může po načtení souboru nést svůj formát - vlastnost ImageType. Když chcete data znovu uložit a zjistíte, že Image.ImageType = JPEG, tak už nic opakovaně nekonvertujete. Za přepokladu, že se obrázek nezměnil.

Pokud chcete obrázek pro pozdější modifikace, můžete vždy současně uložit bitmapu i JPEG, aby nedocházelo ke ztrátám. To řešení se dá jednoduše upravit.

Asi by bylo lepší, abyste napsal, co po třídě Image požadujete, protože se mi zdá, že neuvažujeme o stejných službách třídy Image. Já Image bral jako vygenerovanou bitmapu, kterou chcete uložit (jednorázově) v nějakém preferovaném formátu. Vy podle mě mluvíte o tom, že se obrázek (nejlépe originální bitmapa) může i dále upravovat a že tedy byste načetl JPEG, který upravíte a pak znovu uložíte. To se dostáváme k tomu, jak chytře navrhnout ty dekorátory-filtry, kterými objekt Image proplouvá a já tam mám zatím jediný pro konverzi.
Monday, June 06, 2011 12:11:06 PM (Central Europe Standard Time, UTC+01:00)
Ještě k článku: Na předposledním řešení (https://gist.github.com/1009297) mi vadí to, že čím víc by toho Image měl umět, tím víc by konstruktor musel mít parametrů. Na posledním řešení (https://gist.github.com/1009315) zase to, že dost možná zbytečně volá new FilePersistor(), ale především to, že vytváří cyklickou závislost (Image musí znát FilePersistor a naopak). Podrobněji to rozebírám u sebe: http://php.vrana.cz/muze-mit-trida-image-metodu-resize.php

Ke komentáři: samozřejmě očekávám, že objekt Image bude reprezentovat obrázek a dokáže s ním dělat běžné operace - načíst z nějakého binárního formátu, zase ho uložit, zjistit jeho šířku a výšku, zmenšit ho, redukovat počet barev, zjistit barvu na daném pixelu a tak dále. Nemusí to umět hned, ale musí na to být připraven.

Příklady v článku jsem bral jako zjednodušení - pokud by ale třída skutečně měla mít jen vlastnosti Length a RawData (obsahující načtená data), tak vůbec nechápu, proč by se měla jmenovat Image a ne třeba BinaryData.

Co se metody Resize týče - chci mít možnost použít více resizovacích algoritmů, o kterých třída Image nemusí mít ani ponětí. Rozhodně kvůli tomu nechci měnit vnitřek metody. Je to případ zcela analogický k metodě Save.

Otázkou "Co kdybychom obrázek nechtěli uložit do souboru, ale třeba do databáze?" začala celá tahle diskuse.
Monday, June 06, 2011 12:13:38 PM (Central Europe Standard Time, UTC+01:00)
Díky za odpověď. Hlavně i já jsem při komentování zapomenul napsat, že objektů, které mají jen settery/gettery by mělo být v aplikaci tak 1 % a většinou je žádoucí stav, kdy si objekt udržuje svůj stav a víc než jen zabalenou funkcí je plnohodnotným objektem (navíc od začátku jsem do diskuze vstoupil s tím, že právě "přepravkové lomeno výkonné" objekty jsou právě většinou špatnou praktikou a že použití setterů nebo dokonce public atributů bývá často znakem špatného návrhu - spíše se stavím na stranu názoru: nebudu číst data, řeknu radši objektu, který data má, ať operaci rovnou provede). Já si právě myslím, že většinou oddělení dat od chování je správné spíše vzácně (ale nevyplatí se na tyto hraniční případy zapomínat).

Stejně tak i ten Holub spíše nabádá k objektům, které odpovídají Vaší definici - ale vymezuje situace, kdy je vhodné objekt bez zodpovědností vytvořit. A ač to neříká moc nahlas, myslím že i z jeho článku vyplývá, že by měl programátor s takovými konstrukcemi šetřit. Já si myslím, že každý článek, který srovnává vhodné use-cases pro plnohodnotné objekty a pro objekty bez zodpovědnosti nutně trpí tím, že ho lidi pochopí: autor po nás chce, ať používáme tyto třídy 50:50 a ne 99:1, když v článku autor vyjmenuje klady i zápory takových tříd.
Monday, June 06, 2011 6:57:55 PM (Central Europe Standard Time, UTC+01:00)
Jiří: Díky, tady si už asi rozumíme mnohem víc. Zkusím přes prázdníny napsat více, jak tyhle záležitosti řeším.

Jakub: Mám pocit, že většina nedorozumění plyne z toho, že, jak jsem již psal v předchozím komentáři, bych potřeboval vědět, jaké služby od třídy Image čekáte, jestli jde o nějakou univerzální knihovní třídu Image, nebo jestli jde o "business objekt" v nějaké konkrétní aplikaci.
Sám jsem uvažoval spíš druhý případ ("business objekt) a samozřejmě jsem rozhraní zjednodušil pro účely příkladu diskutovaného u vás. Pokud by šlo o třídu Image v knihovně, tak o DI ani nemluvím. Mimochodem třída Image v .Net Frameworku nabízí metodu Save.

Takže skutečně nepovažuju Image za wrapper nad bajtovým polem.


Ad množství argumentů v kosntruktoru) Pokud třída přijímá množství argumentů, svědčí to buď o tom, že toho řeší příliš mnoho, nebo že je potřeba najít "složené funkce" - abstrakce,,které dříve neexistovaly. Místo předávání IIMagePersistor a IImageTransformeru (Rotate, Resize,...) můžete předat jen IImageManipulators, který vám vydá jako IIMagePersistor , tak IImageTransformer.

Ad řešení s vlastnostmi) FilePersistor vytvořen být nemusí, je jen výchozí rozumná hodnota. Pokud vám vadí, že je vždy vytvořen objekt FilePersistor, i když se nepoužije, je možné použít "lazy" inicializaci.

Cirkulární závislost mezi třídami FilePersistor (IPersistor) a Image není nutná. Persistory mohou přejímat odkaz jen na pole bajtů, na stream, nebo jen na IIMage, ImageBase... Stále se vracíme k tomu, že asi nevím, o jaké třídě Image mluvíte - vy ji "máte v hlavě" a plynule přecházíte mezi tím, že se jedná o příklad, ale přitom když příklad doplníte, že nejde o "Vaši" reálnou a na výkon optimalizovanou třídu Image.

A k metodě Resize.
Je klient odpovědný za výběr algoritmu?
Resize() -- výchozí algoritmus, zakódodován zpočátku klidně ve třídě, refaktorizovat lze vždy.
Resize(IIMageResizer resizer) --klientem vybraný algoritmus
Resize(Action<Image> image) --metoda/anonymní metoda/lambda výraz předaný klientem
Není za to odpovědný klient? Kdo dodá algoritmus Resize? Potřebuje ho třída vždy? Existuje výchozí algoritmus? Může klient psát další algoritmy pro Resize.

Pokud chcete, můžeme tohle někde o prázdninách probrat a každý zkusit sestrojit prototyp kódu (více tříd), který splní určené omezující podmínky, a poté můžeme probrat výhody a nevýhody různých řešení.

BTW: Podle mého soudu tohle není debata v režimu "kdo bude mít ABSOLUTNÍ pravdu" za všech okolností a pro všechny typy projektů. Této premisy se týkal i můj článek, absolutní principy platné na všech projektech za všech okolností neexistují. Měl jsem dojem, že vás článek před "slovutnými objektovými teoretiky" částečně hájí, ale vy mě asi vidíte na druhé straně barikády.:) Jen bych prosil, abyste mě řadil alespoň do správné škatulky svých oponentů.:)
Monday, June 06, 2011 7:11:39 PM (Central Europe Standard Time, UTC+01:00)
A ještě všem děkuju za komentáře, ani jsem nečekal, že tato témata zaujmou tolik lidí. Takže se těmto tématům zkusím na blogu více věnovat.
Monday, June 06, 2011 9:29:09 PM (Central Europe Standard Time, UTC+01:00)
René, vašeho článku si opravdu vážím a skutečně jsem ho pochopil jako obhajobu svého postoje proti dogmatikům zastávajícím názor, že Image rozhodně nemůže mít metodu Save. Pokusil jsem se vás proto zařadit i do jiné škatulky, než ty slovutné objektové teoretiky. Že to s metodou Resize vidíte třeba jinak než s metodou Save, mě nenapadlo a popravdě řečeno to dosud ani nechápu - když má metoda Resize výchozí implementaci přímo ve třídě, proč by ji tam nemohla mít i metoda Save? Obzvlášť v PHP, kde existuje funkce imagejpeg($resource, $filename).

Nicméně ani váš postoj mi není vlastní. Podobně jako existuje předčasná optimalizace, tak na mě většina těchto postupů působí jako předčasná dekompozice. Pod vidinou nějakého zdánlivého vylepšení programu ho ve skutečnosti zkomplikuji, aniž bych si tím jakkoliv pomohl.

Osobně bych to probral velmi rád - pokud to nevyjde dřív, nechcete přijet třeba na PWP? A nemohli bychom přejít na tykání? Tam se tomu stejně asi těžko vyhneme.
Tuesday, June 07, 2011 7:47:17 AM (Central Europe Standard Time, UTC+01:00)
Jakub: S tykáním nemám žádný problém. Na PWP nebo na nějakém jiném setkání se určitě můžeme domluvit. Rád tě poznám osobně.
Bez toho zadání se některé věci špatně vysvětlují a v každém projektu skutečně volím různá řešení.
Pro mě je metoda Resize "lokální" - předpokládám, že pracuje hlavně s daty třídy Image a zpočátku jsem předpokládal, že budete mít jen jediný algoritmus. Když vzroste množství algoritmů, je potřeba řící, jestli algoritmy dodává klient (třída o nich nic neví), nebo jestli vy jako autor třídy předpřipravíte sadu algoritmů.
Pak je také možné třídu konfigurovat takto:
Algoritmus Resize je pro mě "adjustment" - adaptace metody Resize tak, že se použije úplně jiný algoritmus, nebo metoda Resize nabídne "sloty", které může klient upravit - buď dodáním strategie, nabídnutím virtuálních metod (template method), vyvoláním událostí (notifications) v určitém kroku algoritmu, ve kterých klient upraví chování

Image.FromFile.WithResizeBehavior(new MyAlg()).
Image.FromFile.WithResizeBehavior(image => {lambda}).

Výsledkem metody Resize je změněný objekt Image, u kterého si snadno ověřím, k jakým změnám došlo.
Nebo mohu v C# vvyužít extenzní metody apod.

Metoda Save překračuje hranice objektu, to zda se mi podařilo objekt uložit, zjistím jen tak, že se podívám na soubor-do databáze, nebo podhodím Mock, který zachytí celý povolený scénář ukládání.

imagejpeg($resource, $filename - S tímhle žádný větší problém nemám.

Problém bych začal mít, kdybys na objektu začal vytvářet jak na běžícím pásu metody.

SaveToDb
SaveToResponse

Nebo metody:
SaveBmp
SavePng

Když budeš mít metodu Save s voláním imagejpeg a pro další úložiště/formáty, které bude nutné podporovat, ji upravíš, tak jak jsem naznačoval výše, nemám s tím žádný větší problém - puristé tohle určitě nesnesou, ale stojím si za tím.
Jediný problém nastane tehdy, když by metoda nebyla virtuální a ty bys potřeboval rychle běžící testy a nechtěl jsi mít "integrační test", který ukládá rovnou do souboru v nějakém temp adresáři (to chvíli trvá, temp adresář je potřeba promazat), ale cceš mít jen unit test, který jen ověřuje, že metoda Save byla zavolána.
Zkusím o tom napsat více.
Wednesday, July 20, 2011 4:47:19 PM (Central Europe Standard Time, UTC+01:00)
Souhlas - hezký článek i diskuse, spousta materiálu na zamyšlení. Také se moc těším na další
Comments are closed.