Tuesday, 19 August 2014
Veřejná přednáška pro WUG - TPL – konkurenční, paralelní a asynchronní kód pro náročné.
Rád bych vás pozval na svou přednášku, kterou pořádá WUG.
Název přednášky: TPL - konkurenční, paralelní a asynchronní kód pro náročné.
Datum konání: 2.10.2014 od 17:30 do 21:00
Místo konání: pobočka: BB centrum, budova Alfa (Aquarius), Vyskočilova 1461/2a, Praha 4
Registrace na přednášku: http://wug.cz/praha/akce/597-TPL-konkurencni-paralelni-a-asynchronni-kod-pro-narocne
Anotace přednášky:
Znáte alespoň trochu Task Parallel Library a přednášek slibujících další nenáročný „úvod do TPL“ jste už viděli dost? Myslíte si, že klíčová slova async/await v C# jsou magií kompilátoru, jejíž kouzlo pro vás už navěky pominulo po zhlédnutí triviálních a donekonečna opisovaných příkladů, jak zavolat asynchronně pár nudných webových služeb?
Na přednášce probereme, jak rozšířit knihovnu TPL o další užitečné konstrukce i jak odstranit některá omezení v současné verzi TPL. Podíváme se na různé způsoby psaní konkurenčního, paralelního a asynchronního kódu. U konkurenčního kódu se zaměříme (nejen) na aktory a porovnáme různé způsoby, jak můžeme aktory psát.
Nezapomeňte s sebou vzít i kolegy, kteří hlásají, že každou nebezpečnou hlavu konkurenčního kódu setne jeden pořádný „lock“, a to nejlépe rekurzivní, aby vás deadlock nebo livelock ve firmě zabavil i o dlouhých zimních večerech.
Tuesday, 19 August 2014 11:08:00 (Central Europe Standard Time, UTC+01:00)
.NET Framework | C# | Návrhové vzory
Wednesday, 25 June 2014
Task Parallel Library a RStein. Async 5 z n – Hrajeme si s ThreadPoolSchedulerem
Po napsání ThreadPoolScheduleru v předchozím díle následuje další slíbený oddychový díl, ve kterém si máme s ThreadPoolSchedulerem pohrát. Název je možná trochu zavádějící, protože nás žádné rozkošné hrátky nečekají.ThreadPoolScheduler zcela pragmaticky otestujeme, abychom si potvrdili, že jde o plně funkční threadpool a že se takový threadpool dá použít všude, ke je očekáván TPL scheduler.
Jako vždy připomenu, že knihovna RStein.Async, ve které naleznete i ThreadPoolScheduler, je dostupná na Bitbucketu.
git clone git@bitbucket.org:renestein/rstein.async.git
Seriál Task Parallel Library a RStein.Async (předběžná osnova)
Task Parallel Library a RStein. Async 1 z n – Popis základních tříd a obcházení omezení v TPL.
Task Parallel Library a RStein. Async 2 z n – (boost) ASIO v .Net a IoServiceScheduler.
Task Parallel Library a RStein. Async 3 z n – Ukázky použití IoServiceScheduleru. Coroutines.
Task Parallel Library a RStein. Async 4 z n – ThreadPoolScheduler založený na IoServiceScheduleru.
Task Parallel Library a RStein. Async 5 z n – Hrajeme si s ThreadPoolSchedulerem.
Task Parallel Library a RStein. Async 6 z n – Vytvoření StrandScheduleru.
Task Parallel Library a RStein. Async 7 z n – Náhrada za některé synchronizační promitivy – ConcurrentStrandSchedulerPair.
Task Parallel Library a RStein. Async 8 z n – Jednoduchý “threadless” actor model s využitím StrandScheduleru.
Task Parallel Library a RStein. Async 9 z n – Píšeme aktory I.
Task Parallel Library a RStein. Async 10 z n – Píšeme aktory II.
Task Parallel Library a RStein. Async 11 z n – Píšeme nový synchronizační kontext - IoServiceSynchronizationContext.
Task Parallel Library a RStein. Async 12 z n – Použití IoServiceSynchronizationContextu v konzolové aplikaci a Windows službě.
(bude upřesněno)
Poznámka: V celé sérii článků budu používat slovo Task pro třídu, task pro název proměnné / argumentu metody a ”anglicismy” tásk/tásky místo “úloha/úlohy“ nebo jiného českého patvaru při zmínce o /úlohách-táscích/ v dalším textu. Předpokládám, že pro většinu vývojářů je takový text srozumitelnější.
Vytvoříme si ThreadPoolScheduler pro testy.
V metodě InitializeTest třídy IoServiceThreadPoolSchedulerTests vytvoříme IoServiceScheduler, předáme ho do konstruktoru ThreadPoolScheduleru a ThreadPoolScheduler poslouží k inicializaci ProxyScheduleru.
ProxyScheduler našeho ThreadPoolScheduleru je TPL scheduler pro TaskFactory. Tásky vytvořené v testTaskFactory zpracuje ThreadPoolScheduler.
m_testTaskFactory = new TaskFactory(ProxyScheduler.AsTplScheduler());
Vše o vzájemných vztazích mezi “proxy” schedulery a “reálných” schedulery naleznete v prvním díle seriálu.
Přišla také chvíle, abych vysvětlil, proč existuje třída IAutonomousSchedulerTests a upřesnil terminologii v knihovně RStein.Async. Ve třídě IAutonomousSchedulerTests se nacházejí testy, kterými musí projít všechny autonomní schedulery. Jako autonomní scheduler označuju takový scheduler, který po svém vytvoření ihned zpracovává předané tásky a nevyžaduje ze strany aplikace již žádnou další konfiguraci ani podporu při zpracování tásků. Z schedulerů, které prozatím známe, můžeme za autonomní schedulery označit právě ThreadPoolScheduler nebo CurrentThreadScheduler, který jsme viděli v prvním díle seriálu. Naopak IoServiceScheduler není autonomní scheduler, protože po svém vytvoření čeká na to, až mu aplikace propůjčí pro vyřizování tásků thread tím, že zavolá jednu z jeho metod Run, RunOne, Poll nebo PollOne.
Nejprve otestujeme konstruktory ThreadPoolScheduleru.
Jestliže není předán IoServiceScheduler, musí být vyvolána výjimka.
Počet threadů v threadpoolu nesmí být nulový a ani neumíme vytvořit záporný počet threadů.
Jak jsem před chvílí vysvětlil, ThreadPoolScheduler je jedním z autonomích schedulerů, a proto musí projít všemi testy pro autonomní schedulery. Většinu testů jsme viděli při testování CurrentThreadScheduleru, a proto u následujících testů jen jen shrnu, že v jednom testu ověřujeme bezproblémové vyřízení jednoho tásku a v dalším testu zpracování většího množství tásků.
Následující test je zajímavější, protože ověřuje, že když zavoláme metodu Dispose, tak ThreadPoolScheduler vyřídí všechny zbývající tásky a teprvé poté metoda Dispose ukončí činnost scheduleru. O metodě Dispose u schedulerů chci ještě v nějakém dalším díle napsat více, protože deterministické ukončení činnosti různých schedulerů, kdy každý může mít zcela odlišné chování, je jeden z nejméně příjemných úkolů a bez ohledu na to, jaké řešení zvolíte, nezbavíte sebe ani ostatní, kteří schedulery ve svých aplikacích pouze používají, všech problémů. Jsem v čím dál silnějším pokušení některé hraniční scénáře, kdy klient nerespektuje životní cyklus schedulerů a tásků, přesunout do temné říše nedefinovaného chování. U ThreadPoolScheduleru ale nic takového nehrozí, i když byste měli mít nepříjemné mrazení z toho, že se snažíte likvidovat scheduler, aniž byste si byli jisti, že před voláním metody Dispose vyřídil všechny tásky.
Asynchronní test Dispose_When_Tasks_Are_Queued_Then_All_Tasks_Are_Executed spadá pod ty užitečné, ale ne zrovna bezpečné testy, o kterých jsem už také psal. Vygenerujeme tisíc tásků, zařadíme je ke zpracování, ale každý tásk je zablokován do té doby, dokud nestornujeme CancellationTokenSource s názvem waitForSignalCts. Za storno odpovídá metoda CancelAfter, která stornuje CancellationToken po uplynutí stanoveného času. My stornujeme CancellationToken po uplynutí jedné sekundy od zařazení tásků ke zpracování. Ihned po zavolání metody CancelAfter zavoláme metodu Dispose scheduleru a ověříme, že všechny vygenerované a scheduleru předané tásky byly zpracovány.
Kdyby to snad někomu ušlo, upozorním, že CancellationTokenSource používáme ke komunikaci mezi thready. Žádný tásk nestornujeme, jen použijeme CancellationToken k částečné synchronizaci mezi kódem v testu a kódem v táscích. Taková rychlá náhrada za synchronizační primitivu ManualResetEventSlim nebo její příbuzné.
Psal jsem, že test není bezpečný. V testu je totiž “race condition”, protože by teoreticky mohlo dojít k tomu, že metoda Dispose bude zavolána teprve poté, co jsou všechny tásky v scheduleru už vyřízeny. Takový test by opět prošel, ale ověřil by jen chování, které už ověřuje test WithTaskFactory_When_Tasks_Are_Queued_Then_All_Tasks_Are_Executed. V této fázi vývoje knihovny RStein.Async dokážu i s takovým špinavým testem sdílet jednu “solution” ve Visual Studiu.
Další testy už jsou pro všechny schedulery společné a můžete se na ně podívat sami.
Můžeme být spokojeni, všechny testy jsou zelené.
V dalším díle si pořídíme zajímavý přírůstek do rodiny schedulerů s názvem StrandScheduler. Plným jménem StrandSchedulerDecorator.
Wednesday, 25 June 2014 04:30:00 (Central Europe Standard Time, UTC+01:00)
C# | Návrhové vzory
Saturday, 23 March 2013
Monday, 03 December 2012
Prezentace z přednášky na MS festu 2012 - DI v .NET bez pověr, iluzí a frikulínského nadšení
Tomáše Herceg & comp. opět po roce uspořádali další ročník konference MS Fest. A musím hned dodat, že z mého pohledu velmi povedený MS Fest, jehož organizace nikde neskřípala a na kterém jsme se cítil příjemně. Tímto organizátorům ještě jednou děkuju za skvělou organizaci konference a za veškerý servis, který poskytovali účastníkům konference i přednášejícím.
Na MS Festu jsme měl přednášku nazvanou Dependency injection v .Net Frameworku bez pověr, iluzí a frikulínského nadšení.
Sice jsem se jako každý rok po domluvě s organizátory na tématu přednášky dodatečně zděsil, že na přednášku mám jen 60 minut, a těsně před konferencí se stovkami účastníků jsme musel nahodit svůj přídavný a životní energii rychle spalující extrovertní pohon, ale samotná přednáška probíhala oproti minulému roku poklidně. Nemyslete si, já teprve po minulém ročníku MS Festu dovedu ocenit, jaké je to blaho, když s vámi v půlce probíhající přednášky nezačne zuřivě diskutovat oponent z Nokie.
Nabízel jsem tyto přednášky.
Tomáš Herceg mi původně v programu navrhl dvě přednášky, ale já jsme měl čas jen na přípravu jedné přednášky a vybral jsem tu, která dostala nejvíce hlasů.
Nevím, jaká je poptávka po pokročilejších/hard core přednáškách. Jak jsem psal na Twitteru, sám bych raději přednášel o “Task parallel library pro pokročilé”, ještě raději o skrytých pokladech v RX Frameworku, ale RX si již dříve zamluvil Jarda Jírava. Bavilo by mě také přednášet o specialitkách typu dynamic, mohli bychom se pobavit o klíčových slovech async/await v netradičních kontextech, nebo bychom mohli napsat dalšího hostitele .Net Frameworku. To je alespoň malý výběr z témat, která mi jsou blízká, protože jsem podobné vývojářské specialitky řešil pro různé firmy u nás i v zahraničí. Nevím, jaká by ale byla po těchto tématech na MS Festu poptávka, protože povídat si v potemnělé posluchárně jen pro sebe nebo prezentovat pro maximálně deset dalších lidí má své kouzlo, ale - při vší úctě - intimní atmosféru mám raději s jinými než vývojářskými kulisami a aktéry.
Slíbená prezentace:
Doprovodný kód je na Bitbucketu:
Díky za to, že jste na mou přednášku přišli. A děkuju za hodnocení přednášky, moc jste mě potěšili.
P .S. Ještě málá terminologická poznámka, proč používám slovo “kontajner”, a ne kontejner, na což se mě ptal už Tomáš Herceg po zaslání anotace.
Oficiálně [myšleno - slovo kontajner žádný speciální význam] nemá – dokonce myslím ÚJČ slovo kontajner ani neuznává.
Viděl jsem, že se ale v ČR slovo kontajner docela vžilo a snažím se jeho použitím odlišit od konotací „kontejneru“ – u kterého mnoho lidí vidí spojitost s odpadky.:)
P. P. S. A ještě děkuju Alešovi Roubíčkovi za to, že si ochotně pročetl mou prezentaci a upozornil mě na místa, která by si zasloužila nějaké upřesnění.
P.P.P.S. Mrzí mě jediná věc. Na svých přednáškách většinou nezvětšuju písmo, na této přednášce jsem písmo ve Visual Studiu zvětšil přesně tak, jako to měl Tomáš Herceg na první přednášce, a při výkladu mě dost mátlo, že na obrazovce je vidět málo kódu, i když jsem měl z domova vyzkoušeno, že by kód měl být vidět bez problémů. Místo toho, abych VS přepnul na celou obrazovku, tak jsem skroloval a v duchu se divil, proč je toho vidět tak málo. Takové momentální okno přednášejícího, který zapomněl, co udělal s fontem o přestávce. A ještě – poté, co mi spadl mikrofon a já si jej znovu nasadil, tak prý bylo slyšet hlavně v zadních řadách praskání a jiné pazvuky. Já jsem bohužel nic neslyšel a nikdo z posluchačů neprotestoval.
Monday, 03 December 2012 13:25:14 (Central Europe Standard Time, UTC+01:00)
.NET Framework | C# | Entity Framework | Návrhové vzory
Wednesday, 26 September 2012
Tuesday, 20 March 2012
Pozvánka na kurz objektových principů a návrhových vzorů – jaro 2012 a informace k dalším kurzům
Opět bych vás chtěl pozvat na kurz Objektovými principy a návrhovými vzory řízený design a vývoj kvalitních aplikací 1.
Veřejný kurz Objektovými principy a návrhovými vzory řízený design a vývoj kvalitních aplikací 1
Datum konání kurzu: 4. 6. – 6. 6. 2012
Místo konání:
Školící středisko Tutor
U Půjčovny 2
110 00 Praha 1
Po celý den máme k dispozici wifi připojení a samozřejmě také teplé i studené nápoje. V ceně kurzu jsou obědy v restauraci.
Podrobné informace o kurzu a možnost přihlásit se na kurz
Program kurzu
Výběr z ohlasů na kurz
Zde jsou ještě některé ohlasy z twitteru na kurzy, které proběhly na podzim roku 2011:
https://twitter.com/#!/AugiCZ/status/129271721512538112
https://twitter.com/#!/topascz/status/129228333991989248
https://twitter.com/#!/petrkucera/statuses/129474672575250432
FAQ - často kladené dotazy ke kurzům
Tento rok se na jaře uskuteční pouze výše popsaný kurz. Dva další kurzy, Školení Základy objektově orientovaného návrhu a vývoje (UML 0) a Pokročilé návrhové vzory a objektové principy 2, proběhnou na podzim a můžete se na ně také předběžně hlásit. Důvodem, proč tyto kurzy neproběhnou na jaře, je moje vytížení dalšími projekty.
Pro jistotu připomenu, že všechny kurzy lze také objednávat v “inhouse” variantě, kdy kurz proběhne ve vaší firmě v termínech, na kterých se spolu domluvíme, a za podmínek, které s vámi ráda dohodne Petra Steinová. V inhouse variantě je také samozřejmě možné zcela upravit program kurzu a věnovat se jen “specialitkám” a “špekům”, které v aplikaci řešíte.
Budu se těšit na záludné dotazy na kurzu.
Tuesday, 20 March 2012 09:28:04 (Central Europe Standard Time, UTC+01:00)
Kurzy UML a OOP | Návrhové vzory | UML
Monday, 12 March 2012
Lazy loading (zpožděné nahrávání) objektů do kolekce i ve starší aplikaci s využitím dynamické proxy
Jestliže používáte i přes jeho nezralost Entity Framework, nebo jste si zvolili jiné ORM, které zvládá “lazy loading”, neboli zpožděné, či chcete-li dodatečné nahrání dat do typových kolekcí, možná jste přemýšleli, jak byste stejnou službu napsali ve starší aplikaci, která žádné ORM nepoužívá, nebo v hybridní aplikaci, pod kterou si představuju aplikaci, jejíž starší moduly ORM nepoužívají, ale novější moduly již s přístupem přes ORM počítají. I bez ORM byste ale často v aplikaci rádi využívali některé vychytávky, které s sebou přináší ORM. Dnes chci ukázat, že zpožděné nahrávání kolekcí není žádná magie, která by bez ORM byla v aplikaci zapovězena. Když budete mít zájem, můžeme v dalších článcích probrat například i automatickou detekci změn vlastností na objektech a s tím související ukládání objektů i odvolávání proběhlých změn, jestliže zákazník nechce změny uložit a třeba na formuláři stiskne po deseti minutách zuřivé editace objektu a po několika masivních business transakcích, které pozmění desítky objektů najednou, tlačítko Storno.
Co si představit pod zpožděným nahráním objektů v kolekci? Můžeme vyjít z již zlidovělé třídy Objednávka (Order), která má kolekci svých položek (kolekce Items). Místo nahrání všech položek objednávky z databáze ihned po vytvoření instance objednávky, odložíme nahrání položek až na dobu, kdy budou v aplikaci poprvé potřeba. Před klienty třídy Order ale tuto optimalizaci skrýváme tak, že kolekci Items naplníme položkami teprve při prvním přístupu ke kolekci přes veřejné rozhraní třídy Order.
Jde sice o triviální kód, kdy v get akcesoru kontrolujeme bool příznak m_itemsIsLoaded (byly položky nahrány?) a při prvním přístupu ke kolekci zavoláme metodu loadItems, ale představte si, že tento kód u starší aplikace zběsile doplňujete ke každé kolekci v každé třídě, kde teď stojíte o zpožděné nahrávání. Čitelnost kódu je citelně snížena a chrabří obhájci principu jedné odpovědnosti třídy (SRP) právě dopisují Kladivo na heretiky S.O.L.I.Dní víry pravé a připravují v zájmu lepší veřejné vývojářské morálky autodafé účtu dotyčného vývojáře na všech významných sociálních sítích.
Můžeme se rozhodnout, že nebudeme zatěžovat zpožděným nahráváním kolekcí přímo třídu Objednávka, ale že vytvoříme proxy třídu, do které odpovědnost za tuto “infrastrukturní službu” vložíme. Proxy objekt je, jak známo, objekt, který má z hlediska klienta stejné rozhraní jako původní objekt a klient si není vědom, že pracuje s instancí zástupce (surogátem) třídy Order, a ne s originální třídou Order.
Samotná třída Objednávka není nijak zatěžována znalostí, že její kolekce Items je nahrána až při prvním přístupu ke kolekci. Zpožděné nahrávání zvládne ale potomek třídy Order, třída OrderProxy, ve které je podobný kód, který se původně nacházel v objednávce. A protože platí, že potomek nějaké třídy může být v aplikaci používán na všech místech, kde je očekáván předek, můžeme například z repozitáře/identitní mapy objektů začít ihned vydávat klientům instance OrderProxy místo originální třídy Order.
Odpovědnosti už jsme rozdělili lépe, protože třída Order není zatěžována zpožděným nahráváním kolekce a jediným důvodem existence třídy OrderProxy je právě zpožděné nahrávání. Přesto stále platí, že budeme do úmoru psát další a další třídy Proxy, u nichž jediná kreativní vývojářská činnost spočívá v pojmenování bool proměnné, která nám sděluje, jestli kolekce byla, nebo nebyla nahrána. Je možné si práci zjednodušit tím, že například vytvoříme T4 šablonu, která proxy třídy vygeneruje, ale my se dnes zaměříme na to, jak vytvořit proxy třídu, aniž bychom museli psát proxy ručně nebo spoléhat na T4 šablony.
Použijeme takzvanou dynamickou proxy, kterou si lze představit jako nástroj, kterému řekneme, co má proxy dělat, a on pro každou třídu bez ohledu na unikátní rozhraní každé třídy sám vygeneruje proxy, která pro tuto třídu implementuje námi vyžadované chování. Slovo “dynamická” u proxy vyjadřuje hlavně to, že jde o proxy generovanou automaticky za běhu aplikace! V našem konkrétním scénáři se zpožděným nahráváním objektů vytvoříme i pro aplikaci, která má v business vrstvě stovky i tisíce tříd jen jednu další třídu, která představuje “deskriptor” pro každou proxy zajišťující zpožděné nahrávání kolekcí, přičemž platí, že tento “deskriptor“ bude schopen obsloužit všechny kolekce ve všech třídách. Sice se pro každou třídu vytvoří unikátní proxy (potomek originální třídy), ale tato proxy se bude odkazovat na obecný scénář zpožděného nahrávání v našem “deskriptoru”. Při čtení dalších částí článku mějte na paměti, že kód dělá stále to samé, jako námi vytvořená třída OrderProxy výše, jen jsme kód zobecnili tak, abychom si dokázali vynutit zpožděné nahrávání kolekcí ve všech třídách. Vedlejším a nepříjemným důsledkem tohoto zobecnění, jak je tomu asi u každé abstrakce, je samozřejmě snížená schopnost jen po letmém prolétnutí infrastrukturního kódu očima poznat, oč přesně usilujeme. Výhodou však bude to, že náš deskriptor oddálí zmiňované autodafé vývojáře už jen tím, že dodržuje princip DRY-Don't repeat yourself – místo psaní jen mírně obměněného kódu v každé konkrétní proxy jednorázově vyjádříme náš záměr zpožděně nahrávat kolekce v “deskriptoru” pro generování dynamické proxy.
Mějme třídy Customer, Order a OrderItem. Zajímá nás hlavně to, že třída Customer má kolekci objednávek a třída Order kolekci položek objednávky. Pro všechny kolekce chceme doplnit zpožděné nahrání kolekcí.
Všimněte si také toho, že vlastnosti s kolekcemi jsou virtuální. Stále platí, že dynamická proxy je potomkem naší třídy a musí být schopna přepsat implementaci a doplnit kód pro zpožděné nahrání kolekce stejně, jako jsme přepisovali get akcesor u “manuální” proxy výše.
I když je mnohem zábavnější napsat si podporu pro dynamicky generované proxy sám, ne vždy bychom měli vynalézat na projektu kolo, zvláště když jsme v časovém presu a navíc pod drábovou knutou nudných projektových manažerů surově lámajících naše vývojářská křídla , a alespoň v tomto článku použijeme výborný a ověřený nástroj pro generování dynamických proxy z projektu Castle. Nejjednodušší způsob přidání knihovny pro generování dynamických proxy spočívá v instalaci přes NuGet. V Powershell konzoli ve Visual studiu zadejte příkaz.
Vytvoření dynamické proxy v Castlu je kupodivu otázkou napsání jednoho řádku kódu. My služby Castlu zapouzdříme do naší vlastní třídy ProxyEngine.
Do konstruktoru třídy ProxyEngine dostáváme instanci třídy SimpleObjectFactory, kterou si můžete prozatím zjednodušeně představit jako velmi jednoduchou generickou továrnu na výrobu business objektů, která zároveň funguje jako identitní mapa. Její kompletní výpis naleznete níže v tomto článku.
Hlavní je pro nás metoda AddDefaultProxy, která dostává jako první argument typ, pro který má být vytvořena proxy. Tedy předáte-li typový deskriptor objednávky, metoda by měla vyrobit OrderProxy. Druhým argumentem jsou argumenty, které mají být předány konstruktoru naší třídy. Jestliže objednávka vyžaduje v konstruktoru odkaz na své id, vygenerovaná proxy třída garantuje, že jí id bude do konstruktoru předáno.
Teď přichází zajímavá část – vytvoření proxy:
Proměnná m_proxyGenerator je instancí třídy ProxyEngine z Castlu, která představuje výkonné jádro pro generování proxy. Prvním argumentem metody proxyGenerator.CreateClassProxy je typ, pro který chceme proxy vytvořit. Druhý argument typu ProxyGenerationOptions jsou různé volby, které dovolují jemně řídit, jak se bude vytvořená proxy chovat. My zatím potřebujeme jen sdělit, které metody originální (ne proxy) třídy chceme v proxy “přepsat”. Proto jsou ProxyGenerationOptions inicializovány hned v konstruktoru třídy ProxyEngine a je jim předána instance třídy ProxyGenerationHook, která, jak ihned ve výpise uvidíme, vybírá kolekce, u kterých má být podporováno zpožděné nahrání objektů.
Rozhraní IProxyGenerationHook je rozhraní Castlu. Metoda ShouldInterceptMethod z tohoto rozhraní je metoda, kterou Castle používá k rozhodnutí, jaké metody a vlastnosti mají být “přepsány” v dynamické proxy. Třída ProxyGenerationHook v metodě ShouldInterceptMethod říká - tedy vrací true - , že chceme zachytit všechny get akcesory ( methodInfo.IsSpecialName&& methodInfo.Name.StartsWith(GET_METHOD_NAME_PREFIX), jejichž návratovou hodnotou je kolekce. Přesněji řečeno každá kolekce podporující generické rozhraní ICollection<T> (methodInfo.ReturnType.GetInterface(COLLECTION_NAME) != null). Jiných metod ani vlastností si v tomto článku u tříd nevšímáme, a proto pro ně z metody ShouldInterceptMethod vrátíme false. Zajímavou metodou v rozhraní IProxyGenerationHook je i metoda NonVirtualMemberNotification, pomocí níž nás Castle informuje, že v originální třídě je nevirtuální metoda – my metodu NonVirtualMemberNotification nevyužíváme, ale mohli bychom do ní snadno doplnit kód, který vyhodí výjimku, jestliže jste Castlem notifikováni, že existuje nevirtuální vlastnost vracející ICollection<T>, protože vaše firemní konvence vyžadují, aby všechny kolekce podporovaly zpožděné nahrávání kolekce.
Nyní jsme již Castlu sdělili, že máme zájem “přepsat” get akcesory kolekcí, ale stále Castle neví, jakou logiku má do těchto get akcesorů doplnit. Vraťme se k metodě CreateClassProxy. Třetí argument je zřejmý, předáváme argumenty, se kterými má být zavolán konstruktor naší originální třídy. Posledním argumentem metody CreateClassProxy je objekt, který nás zajímá nejvíce – jedná se o tzv. interceptora, který bude použit vždy, když je na proxy použita metoda/vlastnost, kterou chceme “přepsat”. Stále píšeme obecné řešení zpožděného nahrávání kolekcí, a proto metodě CreateClassProxy předáme interceptora s výmluvným názvem LazyLoadInterceptor. LazyLoadInterceptor je ten “zázračný” typ, který jsem výše popisoval jako obecný deskriptor funkcí, které musí podporovat dynamická proxy pro instanci z každé třídy v business vrstvě.
Náš LazyLoadInterceptor, stejně jako každý jiný interceptor, musí podporovat rozhraní IInterceptor z Castlu. Rozhraní IInterceptor má jedinou metodu Intercept, kterou Castle zavolá vždy, když je volána metoda/vlastnost, kterou chceme “přepsat”.
Metodě Intercept je předán objekt Invocation, který nese základní informace o volané metodě.Kromě dalších vlastností je vhodné si zapamatovat, že v invocation.Method naleznete objekt MethodInfo (deskriptor metody) a v InvocationTarget zase konkrétní instanci, na které je metoda volána. Zdůrazním, že touto konkrétní instancí je v našem případě (dynamický) proxy objekt, ne instance originální třídy.
Zkusme si scénář v metodě Intercept projít. Mějme na paměti, že i když ten kód může vypadat na první pohled děsivě, neřeší nic jiného než ručně napsaná proxy výše. Zkusme se v našem popisu pro názornost zaměřit na konkrétní proxy objektu reprezentujícího zákazníka Josefa Nováka v momentě, kdy je poprvé přistoupeno k jeho kolekci Orders (seznam objednávek), i když kód v LazyLoadInterceptoru funguje analogicky ve všech dalších proxy business tříd v systému.
- Metoda Intercept nejprve na předaném objektu invocation volá metodu Proceed. Metoda Proceed vyvolá get akcesor originálního objektu a návratovou hodnotu (při prvním volání prázdnou typovou kolekci objednávek) nalezneme ve vlastnosti invocation.ReturnValue. Proč voláme nejprve invocation.Proceed? Protože potřebujeme v interceptoru kolekci, do které u zákazníka můžeme nahrát objednávky, a tuto kolekci stále spravuje instance originální třídy, jak si můžete ověřit ve výpisu třídy Customer.
- Jestliže se nejedná o první volání metody, nic neděláme, protože kolekce už musí být naplněna. V proměnné m_inspectedMethods máme názvy vlastností, které jsme již u daného objektu zpracovali.
- Jestliže invocation.ReturnValue je null, opět nic dalšího neděláme. Nemáme žádnou kolekci, do které bychom mohli nahrát objednávky.
- Do kolekce m_inspectedMethods přidáme název aktuální vlastnosti (Orders), protože jsme ji již začali zpracovávat.
m_inspectedMethods.Add(invocation.Method.Name);
- Nejprve potřebujeme zjistit, z jaké třídy pocházejí objekty, které budeme do kolekce, jejíž data nahráváme, přidávat. U objektu zákazník a kolekce Orders půjde samozřejmě o objekty z třídy Objednávka.
Type collectionItemType = invocation.Method.ReturnType.GetInterface(COLLECTION_INTERFACE_NAME).GetGenericArguments()[0];
- Dohledáme třídu z databázové vrstvy, která nám bude schopna vrátit seznam objednávek pro daného zákazníka.
Object dbComponent = findDbComponent(collectionItemType);
Pro účely článku je zvolena jednoduchá jmenná konvence - rozhraní pro přístup k databázi se jmenují vždy I<Název třídy>DbComponent. Pro objednávku tedy hledáme typ IOrderDbComponent. Pokud db komponentu nenalezneme, nic dalšího nemůžeme dělat.
- V nalezené db komponentě musíme najít metodu, která nám vrátí záznamy pro všechny objednávky zákazníka Josefa Nováka.
MethodInfo methodInfo = getDbCollectionMethodInfo(dbComponent, invocation.TargetType, collectionItemType);
Opět je zvolená jmenná konvence, kdy metoda má tvar Get{TypeInCollection}RecordsBy{ParentType}Id" a přijímá jeden argument typu int . V našem scénáři hledáme tedy na db komponentě metodu GetOrderRecordsByCustomerId, která přijímá id “rodičovského” zákazníka. V dalších článcích bych rád ukázal, jak se bez této i dalších dále zmíněných jmenných konvence obejdeme a budeme moci nakonfigurovat zpožděné nahrávání kolekcí přes jakkoli nazvané třídy a metody.
Stejně jako v předchozím bodě platí, že nenalezneme-li vyhovující metodu, nic dalšího nemůžeme dělat.
- Dále u objektu zákazník získáme hodnotu vlastnosti Id, kterou potřebujeme pro vyvolání metody na db komponentě nalezené v předchozím odstavci
int? objectId = getTargetObjectId(invocation.InvocationTarget);
- Zavoláme metodu GetOrderRecordsByCustomerId. Návratovou hodnotou je objekt DataTable, který v našem scénáři obsahuje záznamy všech objednávek patřících Josefu Novákovi.
DataTable retValues = methodInfo.Invoke(dbComponent, new object[] {objectId}) as DataTable;
- Přes pomocnou třídu SimpleObjectFactory, kterou LazyLoadInterceptor vyžaduje v konstruktoru, vytvoříme proxy objekty Objednávek a přidáme je do kolekce Orders zákazníka.
var targetCollection = invocation.ReturnValue;
addItemsToCollection(targetCollection, collectionItemType, retValues, invocation);
Navíc se u každé vytvořené objednávky pokusíme nastavit odkaz na “rodičovského zákazníka”, přesněji řečeno na proxy zákazníka. Opět je zvolena jmenná konvence, kdy objekt Order musí obsahovat vlastnost nazvanou Customer, jinak k nastavení “rodiče” nejde. Jak jsem již psal výše, v dalších článcích bychom si měli ukázat, jak tyto výchozí jmenné konvence rozšíříme a dovolíme i jejich úplné nahrazení.
- Hotovo, kolekce Orders u zákazníka Josefa Nováka je naplněna proxy objekty třídy Order a stejný scénář proběhne i při přístupu ke kolekci Items (položky objednávky) u každé objednávky.
Nyní můžeme vyzkoušet, jestli jsou proxy třídy generovány a hlavně jestli naše úsilí nebylo marné a proxy třídy podporují zpožděné nahrávání kolekce.
Výsledkem by měl být tento výpis, ze kterého je patrné :
- Místo originální třídy jsou používány proxy třídy.
- Kolekce jsou naplněny, i když ve třídách Customer ani Order žádný kód pro nahrání kolekce nemáme.
- Je naplněna kolekce Orders u zákazníka i kolekce Items u každé objednávky.
Následuje slibovaný výpis generické třídy SimpleObjectFactory.
Třída SimpleObjectFactory podporuje rozhraní SimpleObjectFactory a při vydání objektu:
- Funguje jako identitní mapa, takže každý objekt je nahrán jen jednou (nyní per process, což se dá snadno změnit).
- Používá záměrně služby “zastaralé” db vrstvy pro nahrání dat každého objektu – vytváření objektů ale deleguje na ProxyEngine.
- Pokusí se nastavit hodnoty jednoduchých vlastností u vytvořených proxy objektů – jestliže se název vlastnosti shoduje s názvem sloupce v datovém zdroji, je vlastnost objektu nastavena na hodnotu sloupečku, která je uložena v řádku vytaženém z databáze.
Co můžeme udělat dále v dalších článcích, jestliže budete mít zájem:
- Nebudeme spoléhat na jmenné konvence při plnění kolekci a “rodičovských” vlastností, ale dovolíme nakonfigurovat interceptora tak, abychom mohli spravovat asociace mezi třídami podle konvencí unikátních pro každý projekt, a přitom abychom nemuseli do těchto nízkoúrovňových proxy služeb moc zasahovat. Konfiguraci provedeme nejlépe pomocí fluentního API.
- LazyLoadInterceptor nebude používat stále dokola reflexi pro dohledání typů a metod, ale bude nalezené hodnoty cachovat.
- Budeme schopni podpořit i zpožděné nahrávání “rodičovských” vlastností. Prozatím je “rodičovská” vlastnost nastavena jen při nahrání kolekce – když vytáhnete z databáze jako první objednávku a sáhnete na její vlastnost Customer, vlastnost vám nyní vrátí null!
- Mohli bychom rozšířit proxy třídy o sledování změn vlastností a umožnit u každého objektu uložení změn nebo vrácení změn (undo).
- Zavedeme repozitáře (Repository), které i v hybridní aplikaci sjednotí ve vyšších vrstvách aplikace přístup k objektům, které jsou nahrány přes ORM i k objektům vytaženým z našich stávajících “old school” db/business služeb.
- Místo toho, abychom generovali dynamické proxy vždy po startu aplikace, umoříme jednorázově (pro většinu aplikací stejně zanedbatelnou) režii spojenou s tímto postupem vygenerováním a uložením assembly s dynamickými proxy při prvním spuštění nové verze aplikace. Při dalším spuštění aplikace se již použijí proxy ve vygenerované assembly.
Pro dnešek toho bylo ale myslím dost. Snad jen dodám, že jsem se v tomto článku chtěl vyhnout různým buzzwordům, ale fajnšmekrům potvrdím, co asi sami tuší, že jsme v tomto článku zavítali do hájemství AOP - aspektově orientovaného programování.
Celý projekt si můžete stáhnout, nejlépe přes Mercurial (hg). Součástí zdrojových kódů je i jednoduchá třída napodobující rozhraní tradičních “db komponent” pro přístup do databáze a zpřístupňující data v Datasetu.
Monday, 12 March 2012 14:06:49 (Central Europe Standard Time, UTC+01:00)
.NET Framework | Entity Framework | Návrhové vzory
Tuesday, 06 December 2011
Tuesday, 30 August 2011
Sunday, 05 June 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.
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, 05 June 2011 21:13:04 (Central Europe Standard Time, UTC+01:00)
Analytické drobky | C# | Návrhové vzory | UML
Monday, 30 May 2011
Pár triviálních poznámek k vývoji aplikací
Dostal jsme dotaz, jak si poradit s odstraňováním chyb u starší a rozsáhlé aplikace. Když jsem odepisoval, uvědomil jsem si, že sepisuju jakési “triviální desatero vývoje”", které se snažím už dlouhou dobu svému okolí vtloukat dohlavy. I když jde o triviální zásady, budu příště odkazovat raději na tento příspěvek, než abych vše opakoval pokaždé znovu. Jméno firmy je v textu nahrazenou souslovím “anonymní firma”.
“Zdravím,
to je na hodně dlouhý příspěvek.
Alespoň tedy:
1) Je nutné zrušit umělou hranici mezi vývojáři a testery. Žádná výměna informací přes šéfy oddělení nebo pověřené osoby.
2) Na každou objevenou chybu musí být napsán automatický test, který zajistí, že chyba neprobublá do dalších releasů. Bez toho žádné organizační opatření nefunguje. Bez napsaného testu se chyba nepovažuje za odstraněnou, ale jen za náhodou se nyní neprojevující.
3) Vytvořit malé sebeorganizující týmy odpovědné za určitou část projektu (nastálo, nebo do dalšího releasu). V čele team leader, který garantuje kvalitu. Team leader je k dispozici i testerům a řeší nesrovnalosti v analýze a systémovém designu. S dalšími team leadery řeší problémy integrace různých částí projektu. Team leader ale stále většinu času kóduje, není to embryo vychovávané pro střední management.
4) Je potřeba postupně napsat velké množství automatických testů (unit, integrační, akceptační) tak, aby se testeři věnovali hlavně novým záležitostem v releasu a aby vývojáři ani testeři nebyli obětí "ručně prováděných" regresních testů, které mají formu nikdy nekončícího debugování. Tím se i zkrátí doba, kterou "anonymní firma" nutně musí trávit opakovaným debugováním a "ručním" nalézáním příčin chyb. Automatizované testy představují práci, která se na projektech vyplatí, a navíc jde i o mnohem levnější řešení problému, než nabírání dalších a dalších testerů.
5) Nedávat žádné fixní odhady na odstranění chyb ani nikoho exkluzivně nealokovat jen na odstraňování chyb. Vývojář není a přes různé manažerské poučky ani nebude anonymní zdroj, který sebereme z jiného projektu, posadíme k aplikaci, kterou nezná, ale u které dostane befelem, že za jednu normohodinu musí odstranit 20 bugů. Tato kouzla fungují jenom v Excelu. V reálném světě vývojář těch 20 bugů neodstraní, ani když ho posadíte do open space, který je oblepen motivačním majstrštykem vytisknutým z PowerPointu nejlepším absolventem MBA.
Pokud se objeví chyba, chopí se jí člověk, který za danou oblast odpovídá (konflikty přinejhorším vyřeší team leadeři). Vývojář neustále čte a refaktorizuje kód, pokud možno ihned také odstraňuje chyby . A jsme opět u testů - dokud ty automatizované testy mít nebudete, vývojáři do kódu raději nezasahují, protože nevědí, kde všude se změna projeví a raději neriskují další možnou příčinu pádu aplikace po nasazení u zákazníka. A psát použitelné, ne jen formální-švejkovské testy, kdy se hodnotí "jen code coverage", se musí všichni vývojáři naučit a nějakou dobu to zabere.
Mám zkušenost s 12 let starou aplikací psanou původně pro VB, poté čátečně přepsanou na .Net Framework, která byla po 9 letech postupně obalena testy, a ani dnes sice nejde o žádnou vývojářskou lahůdku, ale pracuje se na ní beze strachu, co při každé změně v aplikaci zničíme. Nic jiného než to, co píšu výše, se mi neosvědčilo.
A pak další záležitosti jako (nutné) bonbonky:
- Neztrácet čas “mergováním” změn v něčem tak zastaralém a nepohodlném jako je Subversion. V Mercurialu (GITu) je propagace změn z vývojové větve do hlavní (a zpět) otázka chvíle. V Subversion ani TFS jsem po pár zkušenostech raději moc "branchů" nedělal.
- Automatické buildy spojené s již napsanými testy.
- Automatické nasazení nové verze aplikace, aby testeři nemuseli pátrat, kde seženou novou verzi a také, aby nasazení do produkčního prostředí neznamenalo 3denní práci party lidí, kteří se metodou pokus-omyl snaží dostat aplikaci do použitelného stavu u zákazníka.
- Alespoň u nováčků code review, abyste rychle odstranili jejich špatné návyky.
- Statická analýza kódu.
A dovolím si jednu soukromou poznámku k "anonymní firmě" - pokud možno zredukovat/vyhodit všechny těžkotonážní, neskutečně drahé a pro vývojáře zabijácké nápady se zavedením nejhorší možné formy vodopádu - analýza->samostatný systémový design->vývoj->testy, v jehož bludném pádu se bude produkovat množství dokumentů, navíc rychlokvašenými analytiky/designery, v mizerné kvalitě a bez vazby na skutečné potřeby projektu. Analýza a systémový design jsou fáze projektu, které pomáhají jen do doby, než se jich chytí nějaký exot, který nikdy žádnou aplikaci nevyvíjel a který si myslí, že analytická práce spočívá ve štosování nahodilých myšlenek zákazníka do příslušné šablony pro use case. Což je dle mých zkušeností většina “čistých”, míněno vývojem nedotčených, analytiků na pracovním trhu.
Monday, 30 May 2011 09:31:24 (Central Europe Standard Time, UTC+01:00)
Analytické drobky | Návrhové vzory | Ostatní | UML
Monday, 16 May 2011
Pozvánka na kurzy objektových principů a návrhových vzorů – podzim 2011
Opět vás všechny zvu na pravidelný podzimní běh kurzů Objektovými principy a návrhovými vzory řízený design a vývoj kvalitních aplikací 1 a Pokročilé návrhové vzory a objektové principy 2.
Novinkou v tomto roce je kurz pro začátečníky nazvaný Základy objektově orientovaného návrhu a vývoje. Někteří z vás ho mohli během posledních dvou let absolvovat ve formě inhouse kurzu pod interním názvem UML 0. Kurz UML 0 vznikl velmi spontánně jako reakce na požadavky některých firem, které nechtěly rovnou absolvovat stávající kurzy. Nyní je vycizelované UML 0 dostupné i jako veřejný kurz, protože v emailech se opakovaly žádosti o jeho veřejnou formu od lidí, kteří kurz absolvovali a nyní na něj chtěli poslat své kolegy.
O tomto novém kurzu naleznete podrobné informace níže v této pozvánce. Znovu opakuji, že se jedná o kurz pro začátečníky, který dříve v nabídce nebyl, protože kurzy Objektovými principy a návrhovými vzory řízený design a vývoj kvalitních aplikací 1 a Pokročilé návrhové vzory a objektové principy 2 byly a stále jsou určeny pro lidi, kteří základy znají. Nenechte se ale zmást “kódovým” názvem nového kurzu (UML 0), účastníci bývají překvapeni, že s UML na kurzu zacházíme zcela pragmaticky a bez posvátné úcty, ale i bez módních předsudků a rychlých odsudků. UML bereme jako nástroj, a ne jako samoúčelný cíl našeho snažení na kurzu.
Veřejný kurz Základy objektově orientovaného návrhu a vývoje (UML 0)
Datum konání kurzu: 19. 9. – 21. 9. 2011
Místo konání:
Školící středisko Tutor
U Půjčovny 2
110 00 Praha 1
Po celý den máme k dispozici wifi připojení a samozřejmě také teplé a studené nápoje. V ceně kurzu jsou obědy v hotelu.
Podrobné informace o kurzu a možnost přihlásit se na kurz
Program kurzu
FAQ - často kladené dotazy ke kurzům
Veřejný kurz Objektovými principy a návrhovými vzory řízený design a vývoj kvalitních aplikací 1
Datum konání kurzu: 10. 10. – 12. 10. 2011
Místo konání:
Školící středisko Tutor
U Půjčovny 2
110 00 Praha 1
Po celý den máme k dispozici wifi připojení a samozřejmě také teplé a studené nápoje. V ceně kurzu jsou obědy v hotelu.
Podrobné informace o kurzu a možnost přihlásit se na kurz
Program kurzu
Výběr z ohlasů na kurz
FAQ - často kladené dotazy ke kurzům
Veřejný kurz Objektovými principy a návrhovými vzory řízený design a vývoj kvalitních aplikací 2
Datum konání kurzu: 24. 10. – 26. 10. 2011
Místo konání:
Školící středisko Tutor
U Půjčovny 2
110 00 Praha 1
Po celý den máme k dispozici wifi připojení a samozřejmě také teplé a studené nápoje. V ceně kurzu jsou obědy v hotelu.
Podrobné informace o kurzu a možnost přihlásit se na kurz
Program kurzu
Výběr z ohlasů na kurzy
FAQ - často kladené dotazy ke kurzům
Program nového kurzu Základy objektově orientovaného návrhu a vývoje (UML 0)
Předpokládané znalosti účastníků
- Základní přehled o etapách vývoje projektu.
- Vhodné je mít nějaké vlastní zkušenosti z analýzy a vývoje projektů, abychom mohli porovnat různé postupy a doporučení a jejich použitelnost v praxi.
- Chuť se učit.;) Schopnost pohlédnout na některá domněle známá témata bez předsudků.
- Částečná znalost UML je vhodná, ale u tohoto kurzu není vyžadována. Vhodná je hlavně pro konfrontaci vlastních zkušeností s tím, co zazní o použitelnosti různých částí UML na kurzu.
- Nenávist ke kariéře zručného "bušiče", nikým nerespektovaného projektového vedoucího (tzv. sekretářky 2.0) a neschopného analytika / vývojáře.
Obecné informace ke kurzu
Pro vývojáře se u konstrukcí a prvků jazyka UML, které jsou považovány za analytické, dělají časté odbočky do kódu, aby vývojáři pochopili, že UML ani principy OOP nejsou nějaké nesmyslné abstrakce, ale užitečné konstrukce, které sami v programovacích jazycích používají denně. U role analytika stále zdůrazňuji, jaké znalosti z oblasti vývoje aplikací musí analytik mít, aby byl pro projekt užitečný a nevytvářel jen dokumentaci pro dokumentaci, kterou vývojáři nevyužijí a (až příliš často oprávněně) ji považují za nesmyslnou, drahou a hlavně vyvíjenému projektu nic nepřinášející. Kurz je určen pro vývojáře, systémové designery, analytiky a projektové manažery, kteří se chtějí seznámit se základními principy objektového programování a s modelováním v jazyce UML.
Program školení
- Proč má dnes UML v některých kruzích špatnou pověst? Musíte být těžkotonážní rytíři cválající na drahých CASE nástrojích a neživotných formálních projektových metodikách, nebo si vystačíte s nástroji a postupy, které zachytí vaše myšlenky, ale nenutí vás přizpůsobovat se "Jen Tomu Jedinému Pravému Stylu Návrhu A Vývoje"?
- Dostali jste poptávku, máte za pár dní dodat nabídku a v seznamu svých dovedností nenalézáte jasnovidectví ani nemáte po ruce čarovnou skleněnou kouli, abyste zákazníkovi do nabídky vyvěštili konečnou cenu i datum dokončení projektu? Co dělat v počáteční fázi projektu, kdy ještě ani nevíte, jestli budete projekt vyvíjet?
- Požadavky na systém. Jak je to s případy užití? Má vlastní zrychlená funkční specifikace bez zbytečných formalit.
- Rozsáhlé ukázky fukčních specifikací z projektů.
- Diagram tříd v UML.
- Rozdíl mezi analytickým diagramem tříd a diagramem tříd vytvářeným ve fázi systémového designu - existuje takový rozdíl, nebo jde jen o další hloupý mýtus?
- Třída - základní principy OOP, operace, atributy, viditelnost členů třídy.
- Vztahy mezi elementy diagramu (asociace, agregace, generalizace, závislost, realizace) – vše vykládáno na konkrétních příkladech z praxe + ukázky nejčastějších chyb, se kterými jsem se setkal.
- Dobře zapamatovatelné principy návrhu známé pod zkratkou SOLID v příkladech. Unit testy, integrační testy, akceptační testy - skutečně si stále myslíte, že se bez nich na projektech obejdete?
- Nejen SOLIDní principy stojí v pozadí návrhu aplikací...
- Nenásilný přechod k jednoduchým návrhovým vzorům.
- Příklady složitých diagramů tříd. Jak je udržovat v souladu s napsaným kódem? A musíme je udržovat? Co se na projektech vyplatí dělat a co projekt spolehlivě zabije?
- Objektový diagram + příklady.
- Diagramy a diagramy interakce. Příklady. Typy projektů, pro které se tyto diagramy hodí.
- Vysvětlení stavových diagramů + výhody aplikací řízených přesně definovanými stavovými automaty.
- Diagram aktivit - modelování složitých business procesů v organizaci. Hrajete si s diagramem aktivit rádi, ale ocení to Váš zákazník?
- Vrstvy a moduly v aplikaci – architektura aplikace.
- Relační a objektový svět? Stále jediné možné partnerství z rozumu?
- Nasazení aplikace – výklad Component & Deployment diagramů.
- OOP a jeho vztah UML. Vztah UML a OOP k projektové praxi a realitě.
- Výhody a nevýhody UML – vyzdvižení nejvíce používaných postupů, odhození nepotřebné veteše z jazyka UML.
- Odpovědi na dotazy frekventantů kurzu.
Těším se s Vámi na setkání na kurzu!
René Stein
Monday, 16 May 2011 11:15:38 (Central Europe Standard Time, UTC+01:00)
Analytické drobky | Kurzy UML a OOP | Návrhové vzory | UML
Monday, 24 January 2011
Tipy pro Windows Phone 7 aplikace V – vytváříme prvni aplikaci (a stavíme ji na vytvořených základech)
V předchozích článcích jsme si vytvořili miniframework pro view modely a ukázali si hostitele našich view modelů. Přišel čas naše znalosti, idiomy a návrhové vzory zakódované ve formě aplikační infrastruktury v našem miniframeworku využít při tvorbě konkrétní aplikace. Pro účely tohoto článku i následujících článků jsem se rozhodl, že vytvoříme aplikaci, která nám dovolí spravovat vlastní blog na doméně Posterous s využitím Posterous API.
Hlavní případy užití, které budeme v aplikaci podporovat.
- Přihlášení uživatele ke svému účtu - téma dnešního článku.
- Zobrazení seznamu blogů, které patří přihlášenému uživateli.
- Zobrazení seznamu příspěvků na vybraném blogu.
- Zobrazení detailu příspěvku na blogu.
- Úprava stávajícího příspěvku na blogu.
- Zadání nového příspěvku na blog.
Jako vždy nás tato prozatím letmo načrtnutá témata dovedou k dalším podivným zákoutím vývoje WP7 aplikací a my se z nich s úsměvem záludnostmi WP7 poučeného idiota zoceleného harcovníka pokusíme dostat..
Skutečně vás nechci urážet popisem klikání ve Visual Studiu ani popisem základů XAMLu, “data bindingu”, “behaviors”, takže jen napíšu, že byste před vytvářením aplikace měli:
- Stáhnout si a nainstalovat Windows Phone Developer Tools.
- Ve Visual Studiu založte nový projekt Windows Phone Application – nejlepší bude, když ho pojmenujete jako já RStein.PosterousReader.WP, abyste nebyli zmateni názvy jmenných prostorů dále v článku.
- Vývojářský život je na tomto projektu jednodušší o to, že k práci s Posterous API použijeme můj C# Posterous API Wrapper pro WP7. Po stažení přidejte referenci na knihovnu RStein.Posterous.API.SLM do svého projektu.
A i když to dnes ještě není tak nutné, zřiďte si na Posterous vlastní účet, abyste mohli aplikaci později testovat na reálných datech. C# Posterous API pro nás v triádě Model-View-ViewModel bude představovat model, což má pro nás výhodu, že se můžeme stále soustředit na view a view modely, o které šlo i v předchozích článcích, a model můžeme považovat za černou skříňku.
- V novém projektu vytvořte hlavně dvě nové složky View a ViewModels. Na obrázku jsou červeně podtrženy další složky, které si již dnes doporučuju přidat do projektu - Behaviors, Extensions, HostServices, Icons, SpecialTypes, UI, UIServices a hlavně ViewModels a Views.
- Předpokládám, že jste schopni do svého projektu vložit kód tříd, které jsem popisoval v předchozích článcích. Seznam předchozích článků s výpisy kódu naleznete na konci tohoto článku.
Dnes vytvoříme přihlašovací obrazovku do naší aplikace. Pro lepší představu je zde obrazovka, kterou byste měli mít hotovou na konci dnešního článku.
Nejprve určíme, které funkce musí obrazovka a její podkladový kód zvládat:
- Přihlašovací obrazovka se zobrazí ihned po startu aplikace.
- Uživateli dovolíme zadat přihlašovací jméno a heslo.
- Jestliže není uživatelské jméno vyplněno (prázdný řetězec) a/nebo není vyplněno heslo, tlačítko Přihlásit se je neaktivní.
- Jestliže dojde k “tombstoningu” stránky, bude jméno i heslo po návratu z “tombstonovaného” stavu zachováno – ověříme si tak poprvé in vivo, že naše třída ViewModelBase podporuje “tombstoning” přesně dle našich požadavků.
- Po návratu na přihlašovací obrazovku z jiné části aplikace bude zachován jen obsah textového pole “uživatelské jméno”, textové pole “heslo” bude prázdné.
- Po kliknutí na tlačítko Přihlásit nebudeme zadané jméno a heslo ihned ověřovat, ale uložíme oba údaje pro použití na dalších stránkách aplikace. Poté přesměrujeme uživatele na další stránku se seznamem blogů, kde bude jméno a heslo využito k získání seznamu blogů uživatele. Důvodem je to, že v Posterous API se jméno a heslo zasílá při každém webovém požadavku, žádné jednorázové přihlášení neexistuje a nemá smysl generovat nějaký “dummy” požadavek jen pro ověření hesla. Jestliže se na stránce se seznamem blogů, na kterou z přihlašovací obrazovky přesměrováváme, data kvůli neplatným přihlašovacím údajům získat nepodaří, aplikace nás vrátí na přihlašovací obrazovku. Vytvoření stránky se seznamem blogů bude téma dalšího článku.
A začínáme:
Do složky Views vložte nové View pro přihlášení uživatele. V dialogu Add new item vyberte Windows Phone Portrait Page a pojmenujte ji MainLoginView.xaml.
Jedná se o úvodní stránku aplikace, a proto v deskriptoru WP7 aplikace nazvaném WAMppManifest.xml, který naleznete v projektové složce Properties, změníme startovací stránku na View/mainLoginView.xaml.
I když to není u přihlašovacího dialogu nutné, v dnešním článku si ukážeme, jak můžeme jednoduše složit jedno view z dalších nezávislých view a také to, že view nemusí být jen stránka (Page), ale i “User Control”. Naše MainLoginView bude zobrazovat titulek aplikace (Posterous klient), titulek stránky (Přihlášení) , ale textová pole “heslo”, “uživatelské jméno” a tlačítko “Přihlásit" se” bude obsahovat “user control” LoginView, který můžeme použít jako součást i zcela jiného view (stránky) v aplikaci. Do složky Views přidejte Windows Phone User Control a nazvěte jej LoginView.xaml.
Zkompilujte (Build) “solution”.
Do view MainLoginView vložte tento kód:
Jak jsem zmiňoval v jednom z předešlých článků, všechny stránky v aplikaci by měly být potomkem naší třídy PageBase, která je hostitelem pro view modely. Proto je naše stránka uzavřena v elementu <controlex:PageBase>. XML jmenný prostor controleex je u mě namapován na xmlns:controlex="clr-namespace:RStein.PosterousReader.WP.UI". V projektovém adresáři UI musíte mít tedy třídu PageBase.
Také v “code behind souboru” musíte třídu MainLoginView učinit potomkem PageBase.
Hlavní xaml pro přihlášení uživatele obsahuje “User Control” LoginView, na který se v MainLoginView odkazujeme. (<views:LoginView Grid.Row="1"></views:LoginView>). XML jmenný prostor views je mapován na jmenný prostor RStein.PosterousReader.WP.Views v C# (xmlns:views="clr-namespace:RStein.PosterousReader.WP.Views) – v našem případě tedy na projektový adresář Views.
Do view s názvem LoginView vložte následující XAML:
Jak bylo vidět na snímku obrazovky na počátku článku, LoginView obsahuje hlavně textové pole pro zadání jména uživatele (txtName) a prvek pro zadání hesla uživatele (txtPassword) a tlačítko “Přihlásit se”. U prvků pro zadání uživatelského jména a hesla vás mohou zarazit jen tagy <i:Interaction.Behaviors> a atributy jako behaviors:TextboxPasswordAttachedProperties.TextBoxChangedAction, jejichž význam vysvětlím za chvíli. Také vás hned upozorním, že tlačítko “Přihlásit se” je speciální tlačítko ((<controlex:ButtonEx) dovolující reagovat na stisknutí tlačítka uživatelem tak, že spustí metodu Execute objektu ICommand, což standardní WP7 tlačítko nezvládá. Znovu připomínám, že Silverlight ve WP7 je bohužel založen na poměrně staré verzi 3 desktopového Silverlightu. Kód třídy ButtonEx uvidíte také za chvíli.
Máme view, ve view používáme “data binding”, ale nemáme ještě view modely, které fungují pro view jako zdroj dat.
V každém view modelu naší aplikace budeme používat objekt IPosterousApplication ke komunikaci s Posterous, budeme chtít z view modelů navigovat na další view v aplikaci a také bychom měli být schopni v každém view modelu získat uživatelské jméno a heslo zadané na na námi vytvářené přihlašovací obrazovce. To znamená, že společné vlastnosti a služby view modelů pro naši aplikaci můžeme soustředit v bázové třídě PosterousViewModelBase. Do projektové složky Views vložte novou třídu PosterousViewModelBase a do ní zkopírujte následující kód.
PosterousViewModelBase je potomkem naší staré známé třídy ViewModelBase
V konstruktoru třída PosterousViewModelBase přijímá odkaz na objekt IPosterousApplication, který je základním rozcestnikem pro přístup k Posterous API, navigační službu INavigationService, která view modelu dovolí navigovat na další view v aplikaci (např. ze seznamu článků na detail vybraného článku), a titulek zobrazené stránky. Titulek převezme a nabízí poté ve vlastnosti PageTitle bázová třída ViewModelBase.
Navigační služba je představována rozhraním INavigationService.
Základní implementace rozhraní INavigationService pro WP7 je dostupná ve třídě DefaultWPNavigationService a pouze obaluje navigační služby dostupné v samotných WP7 aplikacích.
Rozhraní INavigationService i třídu DefaultWPNavigationService vložte do projektové složky HostServices.Více si o navigaci mezi různými view povíme v dalších článcích.
Vraťmě se k PosterousViewModelBase. V PosterousViewModelBase máme vždy uloženo ve statických vlastnostech LastUsedUserName a LastUsedPassword poslední zadané přihlašovací údaje, které může každý view model využít při získání nebo úpravě dat z Posterous.
Poznámka: Kdyby někoho z vás pohoršovali statické vlastnosti, klidně si jako domácí úkol napište “CredentialsManager”, který bude injektován stejně jako již zmíněné dvě další služby do view modelů. Prozatím nechci komplikovat kód víc, než je nutné.
My máme dvě view, MainLoginView a LoginView, a proto vytvoříme i dva view modely. Opět zdůrazňuju, že tato volba je na vás a díky “dědění view modelů” bychom mohli ponechat třeba dvě view a vytvořit pro ně jen jeden společný view model.
Do projektové složky ViewModels přidejte třídu nazvanou MainLoginViewModel – view model pro MainLoginView. Název view modelu nyní musí odpovídat konvenci “NázevView +ViewModel”. Zdůrazňuju i zde, že jde jen o konvenci a že si můžete jednoduše napsat jiný IViewModelResolver, který dohledá dle vašich zcela jiných projektových konvencí k view vhodný view model.
MainLoginViewModel je sympaticky jednoduchý objekt. Má jen delegující konstruktor, ve kterém předá své bázové třídě PosterousViewModelBase vyžadované povinné argumenty posterousApplication, navigationService a titulek stránky.
Texty v aplikaci nejsou prozatím lokalizovány a titulky stránek jsou uloženy ve třídě GlobalConstants. Do “rootu” projektu přidejte třídu GlobalConstants.cs
Název aplikace (APP_MAIN_TITLE) , podobně jako titulek stránky, vydává ve vlastnosti AppTitle opět ViewModelBase
Jen o trochu složitější bude view model pro LoginView. Do projektové složky ViewModels přidejte třídu LoginViewModel.
V LoginViewModelu máme opět delegující konstruktor, jedinou změnou oproti MainLoginViewModelu je to, že jako titulek stránky předáváme prázdný řetězec, protože předpokládáme, že titulek na stránku dodá “nadřízené” view.
Vlastnosti a jejich význam
Název vlastnosti | Popis |
TextChangedAction | Akce, která má být vyvolána, když se ve view změní text přihlašovacího jména nebo hesla. Tato akce je v konstruktoru inicializována tak, že si vynutíme opětovné vyhodnocení toho, zda může být proveden LoginCommand. V článku popíšu, proč je to (prozatím?) řešeno takto. |
LoginCommand | Objekt podporující rozhraní ICommand, který v metodě Execute zavolá metodu handleLogin. Metoda handleLogin uloží zadané jméno a heslo do statických vlastností LastUsedPassword a a LastUsedUserName a přesměruje uživatele na stránku se seznamem blogů. Instanční vlastnost UserPassword je při každém pokusu o navigaci na stránku se seznamem blogů “vyčištěna” tím, že je do ní uložen prázdný řetězec, a při návratu na přihlašovací obrazovku tedy není automaticky předvyplněno heslo, což byl jeden z našich požadavků na přihlašovací dialog. LoginCommand může být proveden jen tehdy, když bylo ve view zadáno a do view modelu přes oboustranný (two way) binding zpropagováno uživatelské jméno a heslo. Pokud vlastnosti UserName a UserPassword mají hodnotu null nebo obsahují prázdný řetězec, LoginCommand nemůže být proveden. |
UserName | Zadané přihlašovací jméno na službu Posterous. V metodě DoInternalInit, která je volána na začátku životního cyklu view modelu předvyplníme uživatelské jméno posledním zadaným uživatelským jménem, které jsme dříve uložili do statické vlastnosti LastUsedUserName. |
UserPassword | Heslo na službu Posterous. |
Co potřebujeme, abychom mohli náš LoginViewModel zkompilovat?
Do třídy GlobalConstants přidejte URL dalšího view se seznamem blogů a článků.
Stránku PostsListView.xaml vytvoříme v dalším článku, nyní ji celou vytvářet nemusíte. Postačí do složky Views dát novou stránku (Page) s názvem PostsListView.xaml.
Jak jsem již psal, ve WP7 nemáme bohužel podporu pro objekty ICommand. Třída DelegateCommand, jejíž instancí je LoginCommand, je minimalistickou implementací rozhraní ICommand.
Pokud podobnou třídu nemáte, vložte si do projektu třídu DelegateCommand. U mě je v jmenném prostoru RStein.PosterousReader.Common.
Třídě DelegateCommand můžete předat dva delegáty. Delegát executeAction (“co má být vykonáno”) je spuštěn v metodě Execute z rozhraní ICommand. Delegát canExecuteAction představuje implementaci metody CanExecute (“může být nyní command vykonán?”). Minimalistická implementace je to proto, že nijak nepoužívám událost CanExecuteChanged, a namísto toho jsem si pro “binding” vystavil speciální vlastnost CanExecuteCommand, která v get akcesoru deleguje na metodu CanExecute.
Vlastnost TextChangedAction v LoginViewModelu je delegát typu Action, který nám pomáhá vyřešit jeden z požadavků na přihlášení.
“Jestliže není uživatelské jméno vyplněno (prázdný řetězec) a/nebo není vyplněno heslo, tlačítko Přihlásit se je neaktivní.”
To znamená, že potřebujeme po každém přidání nebo smazání znaku v textboxu pro zadání jména uživatele i v textboxu pro zadání hesla zjišťovat, jestli můžeme ve view zpřístupnit tlačítko pro přihlášení. Když je alespoň jeden textbox prázdný, tlačítko pro přihlášení není dostupné, když je vyplněno alespoň jedním znakem jméno i heslo, tlačítko pro přihlášení je dostupné.
Jak je tento požadavek ve view a view modelu splněn?
Ve view LoginView je vlastnost IsEnabled tlačítka “Přihlásit se“ z třídy ButtonEx “nabindována” na vlastnost CanExecuteCommand objektu LoginCommand ve view modelu.
Do projektové složky UI si přidejte třídu ButtonEx, která doplňuje standardní WP7 tlačítko o jednoduchou podporu objektů ICommand.
Vlastnost CanExecuteCommand musí vracet true, pokud je v každém textboxu alespoň jeden znak, jinak false. Jak ale ve view modelu zjistíme, že uživatel zadal nebo smazal v některém textovém poli další znak? Do view modelu se při oboustranném bindingu zpropaguje hodnota z textového pole až po opuštění textového pole uživatelem, ale my přitom musíme reagovat ve view modelu na zadání každého znaku.
Takové chování textových polí ve WP7 skutečně nemáme a musíme si ho dopsat, a to nejlépe za pomoci ”attached” vlastností a objektů Behavior<T>. Nejprve se podívejme, jak vypadají objekty TextBox a PasswordBox v XAMLu, když jsou rozšířeny o “attached” vlastnost TextboxPasswordAttachedProperties.TextBoxChangedAction, která je “nabindována” na nám již známou vlastnost TextChangedAction typu Action ve view modelu. “Attached” vlastnost TextBoxChangedAction tedy říká: “Hej, kdykoli se změní u prvku zadaný text, musí NĚKDO ochotný zavolat delegáta TextChangedAction, abychom ve view modelu nebyli odříznuti od novinek ve světě view.”
A ten někdo bude náš objekt Behavior , který zase světu sděluje: “Mám dobré vychování, a když mi dovolíte vstoupit do bran tagu PasswordBox nebo TextBox, delegáta TextChangedAction zavolám, i když to sám WP7 TextBox a PasswordBox nezvládne.”
Další nepříjemností ve WP7 je, že i když objekt Behavior má mít pro TextBox a PasswordBox stejné chování, musíme napsat dva objekty Behavior pro každý prvek, protože TextBox ani PasswordBox spolu kupodivu nemají moc společného. My se alespoň pokusíme kód v obou objektech Behavior neduplikovat.
Do projektové složky Behaviors přidejte třídu TextboxPasswordAttachedProperties a v ní vytvořte attached vlastnost TextBoxChangedAction.
Do složky Behaviors přidejte abstraktní třídu TextChangedBehaviorBase, která bude fungovat jako základ pro dva podobné objekty Behavior určené pro TextBox (TextBoxTextChangedUpdateSourceBehavior) a PasswordBox (PasswordTextChangedBehavior). Do projektu musíte přidat referenci na knihovnu System.Windows.Interactivity, kterou naleznete většinou ve složce c:\Program Files\Microsoft SDKs\Expression\Blend\Windows Phone\v7.0\Libraries\System.Windows.Interactivity.dll. Bez této knihovny není dostupná bázová třída Behavior<T>.
Metoda RunTextChangedAction se pro objekt Textbox a PasswordBox asociovaný s tímto objektem Behavior pokusí dohledat a spustit delegáta v “attached” vlastnost TextBoxChangedAction. Metoda UpdateSource požádá odvozené třídy o vydání "objektu BindingExpression” voláním metody GetBindingExpression. Vrácený objekt “BindingExpression“ by měl zapouzdřovat propojení vlastnosti view modelu (např UserName) s textem v textovém poli. Po kontrole, že se jedná o oboustranný (two way) binding, metoda UpdateSource zavolá na objektu BindingExpression UpdateSource, což způsobí přenesení hodnoty zadané uživatelem v textovém poli ve view do zdrojové vlastnosti (např. UserName) ve view modelu.
Potomek TextBoxTextChangedUpdateSourceBehavior v metodě GetBindingExpression vrátí BindingExpression pro svou vlastnost Text. V přepsané metodě OnAttached, která je volána vždy, když je objekt Behavior asociován s textboxem, si přihlásíme odběr události TextChanged TextBoxu a při každé změně textu voláme zděděné a výše popsané metody UpdateSource a RunTextChangedAction. V metodě OnDetaching si nezapomeneme odběr události TextChanged odhlásit.
Pro PasswordBox máme dalšího potomka PasswordTextChangedBehavior, který se od třídy TextBoxTextChangedUpdateSourceBehavior liší jen tím, že v metodě GetBindingExpression vrací “BindingExpression” pro vlastnost Password a zadávaný text sleduje přes událost PasswordChanged.
V LoginView si nezapomeňte zkontrolovat, že máte namapovány správně xml prefixy behaviors a i na správné jmenné prostory v C#, abyste mohli využívat novou attached vlastnost TextBoxChangedAction a objekty TextBoxTextChangedUpdateSourceBehavior a PasswordTextChangedBehavior.
prostoryxmlns:behaviors="clr-namespace:RStein.PosterousReader.WP.Behaviors"
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
Dodejme, že náš delegát TextChangedAction ve view modelu simuluje změnu objektu LoginCommand, který je přidružen k tlačítku Přihlásit se a tlačítko Přihlásit tedy po každém zadání znaku zjistí, jestli má být dostupné. Zopakujme, že vlastnost IsEnabled tlačítka je “nabindována” na vlastnost LoginCommand.CanExecuteCommand.
Aplikaci můžete spustit.
Poznámka: Ještě si ale nezapomeňte do složky UIServices dát kód rozhraní IViewModelResolver a třídy ViewModelResolver, které jsem popisoval v článku propojení view modelu s view (stránkou), protože bez třídy ViewModelResolver by aplikace nebyla schopna pro naše MainLoginView a LoginView dohledat právě vytvořené view modely. A v aplikaci musíte mít samozřejmě všechny další třídy z předchozích článků, které jsou odpovědné za “tombstoning” apod.
U aplikace si můžete zkontrolovat, že:
- Přihlašovací dialog plní všechny požadavky, které jsme vypsal na začátku článku.
- Nemusíme se nijak starat o “tombstoning” aplikace. Zmáčkněte tlačítko Win, poté se tlačítkem Back vraťte do aplikace a všechny hodnoty v textových polích zůstanou zachovány.
- Tlačítko pro přihlášení je dostupné jen tehdy, když textová pole pro zadání jména a hesla jsou vyplněna. Pokud alespoň jedno pole vyplněno není, tlačítko je neaktivní.
- I naše minimalistická podpora rozhraní ICommand vede k tomu, že ve View nemáme žádný “code behind”. Logika stojící za view je jen ve view modelech a XAML tahá data z view modelů přes “data binding”. Pokud přesně tohle to považujete za důležité, budete určitě nadšeni. Jak uvidíte v dalších článcích, já jsem vůči strategii “vše do XAMLu” hodně skeptický, ale XAML puristé
si mohou jít po dnešku jistě ožrat držku slavit.
- I když šlo jen o přihlašovací formulář, vytvořili jsme si další skládací kostky (attached vlastnosti, Behavior, PosterousViewModelBase), které se nám budou hodit při psani dalších view a view modelů (nejen) v této aplikaci.
Zauvažujte nad tím, jestli by se nám také nehodila nějaká podpora ve view modelech pro ukládání nejen tranzientního stavu, ale i pro ukládání “trvalejšího” stavu, který bude dostupný i v další nezávislé instanci aplikace. Možná by stálo za to, aby si aplikace pamatovala poslední zadané přihlašovací jméno nejen při “tombstoningu”, ale aby přihlašovací jméno bylo nabídnuto i při novém spuštění aplikace. A z hlediska vývojáře bezbolestná podpora pro ukládání perzistentního stavu (data s “delší záruční lhůtou”) bude námětem dalšího WP7 intermezza. Doufám, že se těšíte.
Předchozí články:
Tipy pro Windows Phone 7 aplikace I
Tipy pro Windows Phone 7 aplikace II – podpora životního cyklu aplikace (včetně tombstoningu) ve "view modelech”
Tipy pro Windows Phone 7 aplikace III–propojení view modelu s view (stránkou)
Tipy pro Windows Phone 7 aplikace IV - intermezzo I - zjednodušená registrace serializovatelných tříd nesoucích tranzientní stav v KnownTypesDictionary
Monday, 24 January 2011 17:48:26 (Central Europe Standard Time, UTC+01:00)
C# Posterous API | Compact .Net Framework | Návrhové vzory | Silverlight | WP7
Monday, 10 January 2011
Tipy pro Windows Phone 7 aplikace IV - intermezzo I - zjednodušená registrace serializovatelných tříd nesoucích tranzientní stav v KnownTypesDictionary
Dnešní článek je jen “intermezzem”, protože doplňuje předchozí článek o slíbenou informaci, jak můžeme automaticky registrovat serializovatelné třídy, jejichž instance nesou tranzientní stav, který je uložen v KnownTypesDictionary. Přechozí článek končil ukázkou, jak můžeme vrátit staticky definovaný seznam typů, dnes se poohlédneme po trochu dynamičtějším řešení. I když postup není omezen jen na WP7 aplikace, ale můžete ho použít v kterékoli aplikaci, která využívá (WCF) DataContractSerializer, my se zaměříme v článku opět hlavně na řešení WP7 specialitek.
Přečetli jste si první odstavec a nevíte, co je tranzientní stav? Hledáte marně v MSDN typ KnownTypesDictionary? Nedivte se, milostný akt sice můžete rozjet bez předehry, ale abyste rozuměli tomuto intermezzu, pečlivě a pomalu si přečtěte jako předehru k intermezzu tipy pro Windows Phone 7 aplikace II – podpora životního cyklu aplikace (včetně tombstoningu) ve "view modelech”, i díl třetí, kde se bavíme o KnownTypesDctionary.
Předehru máte za sebou, takže víte, že KnownTypesDictionary je specializovaný objekt Dictionary, ve kterém je klíčem řetězec a hodnotou libovolný objekt a že v objektech KnownTypesDictionary ukládáme tranzientní stav view modelů při každém “tombstoningu”. I když KnownTypesDictionary může jako hodnotu nést libovolný objekt, my musíme instruovat DataContractSerializer, které objekty a hlavně z kterých odvozených tříd má v objektu Dictionary očekávat.
Když do KnownTypesDictionary ukládáme v jedné aplikaci instance z tříd OrderVO a InvoiceVO a v jiné aplikaci instance TwitterPost a ObservableCollection<TwitterPost >, musíme vždy znovu zmíněné třídy registrovat pomocí atributu KnownType.
Pro připomenutí následuje výpis kódu, kterým jsme končili a který registruje serializovatelné třídy v aplikaci “natvrdo” pomocí statické metody GetKnownTypes, jejíž název je předán do konstruktoru atributu KnownType.
Napevno zadrátované typy v KnownTypesDctionary nám nemusí vadit v jedné aplikaci, ale když chceme používat KnownTypesDctionary v mnoha různých aplikacích, musím řešení upravit.
Místo abychom třídy v metodě GetKnownTypes registrovali přímo, delegujeme odpovědnost za vrácení serializovatelných typů na specializovaného poskytovatele.
BTW: To víte, že první a okouzlující zákon vztahu mezi třídami zní: “Když je třída vyčerpána množstvím odpovědností, vždy si může přičarovat otroka, který špinavou práci udělá za ni”?. Občas se tomuto zákonu také říká SRP – princip jedné odpovědnosti třídy”.
Poskytovatel je objekt podporující rozhraní IKnownTypeProvider s jednou samopopisnou metodou.
Refaktorizujeme třídu KnownTypesDictionary, aby při vracení serializovatelných typů využívala objekt IKnownTypeProvider.
Do nové statické vlastnosti KnownTypeProvider je ve statickém konstruktoru přiřazen DefaultKnownTypesProvider, o kterém budeme mluvit za chvíli, ale do vlastnosti KnownTypeProvider můžete klidně vložit pro účely vaší aplikace lépe přizpůsobený IKnownTypeProvider.
Metoda GetKnownTypes vrátí všechny staticky registrované typy v poli KNOWN_TYPES společně s typy, které dodá IKnownTypeProvider. Typy jsou získány jen při prvním volání metody GetKnownTypes a jsou cachovány v proměnné _cachedKnownTypes, abychom opakovaným získáním typů z IKnownTypeProvider zbytečně neplýtvali výkonem WP7 telefonů ani trpělivostí uživatele. Předpokladem tohoto přístupu samozřejmě je, že v aplikaci je fixní množina serializovatelných typů, která se za běhu aplikace již nemění. Jestli vám to nevyhovuje, odstraníte cache z KnownTypesDictionary za 10 sekund.
Jaké odpovědnosti bude mít DefaultKnownTypesProvider?
- DefaultKnownTypesProvider nám vrátí všechny deskriptory třídy (Type) označené atributem [DataContract] v hlavní assembly aplikace.
- DefaultKnownTypesProvider nám vrátí všechny deskriptory třídy (Type) označené atributem [DataContract] v dalších assembly, ze kterých je složena aplikace.
- DefaultKnownTypesProvider nalezne další a pro aktuální aplikaci specifické objekty podporující rozhraní IKnownTypeProvider a vrátí seznam všech typů z těchto providerů.
- Pro všechny nalezené typy T zaregistruje i kolekci Observable<T>. To znamená, že pro třídu OrderVO označenou atributem DataContract je automaticky vrácen z poskytovatele nejen její Type (typof(OrderVO), ale i deskriptor kolekce typeof(ObservableCollection<OrderVO>).
Kód třídy DefaultKnownTypesProvider:
V metodě GetKnownTypes vyzvedneme všechny typy z “hlavní” (vstupní) assembly. Hlavní assembly získáme v metodě getExecutingAssembly přes vlastnosti třídy Deployment.
Typy z hlavní assembly sloučíme s typy v dalších assembly voláním metody getRefencedAssembliesTypes(). Ve WP7 jsem bohužel nenašel způsob, jak seznam dalších assembly získat, a proto další assembly poskytují objekty IAssemblyTypesProvider nalezené v hlavní assembly.
Chcete-li tedy automaticky vyhledat typy označené atributem DataContract v další assembly, vložte do hlavní assembly třídu podporující rozhraní IAssemblyTypesProvider.
Do proměnné collectionTypes v metodě GetKnownTypes vygenerujeme pro všechny nalezené typy jejich kolekce. Možná trochu složitě vypadající kód jen zabraňuje tomu, abyste nalezené deskriptory tříd typu ObservableCollection<T> v proměnné types balili znovu do kolekce ObservableCollection. Jinými slovy, když v proměnné types bude kolekce ObservableCollection<OrderVO>, do proměnné collectionTypes nebude generována kolekce ObservableCollection<ObservableCollection<OrderVO>>.
Na konci vracíme nalezené typy sloučené s vygenerovanými typy kolekcí ObservableCollection<T> a dalšími a pro aplikaci specifickými typy z ostatnich poskytovatelů . Za poskytovatele považujeme další objekty IKnownTypeProvider nalezené v hlavní assembly.
A tím máme hotovo.
Připomínám, že DefaultKnownTypesProvider můžete využít nejen ve WP7 aplikacích, ale ve všech aplikacích v .Net Frameworku, kde je používán DataContractSerializer a vy chcete automaticky registrovat odvozené serializovatelné typy.
Příště již začneme stavět aplikaci založenou na našem "mini frameworku”, abyste viděli, k čemu tyto počáteční díly seriálu včetně intermezza, které právě čtete, vůbec byly.
Předcházející články:
Tipy pro Windows Phone 7 aplikace I
Tipy pro Windows Phone 7 aplikace II – podpora životního cyklu aplikace (včetně tombstoningu) ve "view modelech”
Tipy pro Windows Phone 7 aplikace III–propojení view modelu s view (stránkou)
Monday, 10 January 2011 13:37:12 (Central Europe Standard Time, UTC+01:00)
.NET Framework | C# | Návrhové vzory | Silverlight | WP7
Monday, 03 January 2011
Tipy pro Windows Phone 7 aplikace III–propojení view modelu s view (stránkou)
Update 4. 1. 2011 - upraven kód objektu UIHelper a spolupracující metody tak, aby byl klíč generovaný pro tranzientní stav každého view modelu skutečně unikátní. Když rozkliknete GISTy, můžete se podívat i na původní verzi kódu.
V předchozím článku jsme se podrobně věnovali vytvoření bázové třídy pro view modely včetně podpory ukládání tranzientního stavu během “tombstoningu” a na konci jsem slíbil, že další díl bude věnován hlavně propojení našich view modelů s view.
View pro nás bude hostitel view modelu a v souladu s WP7 modelem navigace mezi jednotlivými stránkami budeme za hlavního hostitele považovat potomka třídy PhoneApplicationPage. Uvidíme ale, že při vytváření View nejsme omezeni na potomky PhoneApplicationPage, protože budeme schopni vytvořit i view jako potomka třídy UserControl.
Zrekapitujme hlavní funkce, které by měl náš hostitel view modelů zvládat:
- Při vytvoření stránky je třeba najít ke stránce přidružený view model. Dodejme již nyní, že povinností hostitele bude dohledat i vnořené (dceřiné) view modely, jestliže je jedna stránka složena z více nezávislých view a každé view může být asociováno s jiným view modelem.
- Při vytvoření stránky je nutné u view modelu volat metodu Init pro základní inicializaci view modelu, jestliže view model podporuje naše rozhraní IInittialize. Znovu připomínám, co zaznělo v minulém článku, že si implementaci žádných rozhraní u view modelů nevynucujeme.
- Jestliže se stránka stane poprvé nebo po návratu uživatele “aktivní” (zobrazenou), je nutné volat na view modelu metodu Activate, když view model podporuje rozhraní IActivated.
. - Jestliže uživatel ze stránky odchází, hostitel na view modelu volá metodu Deactivate z rozhraní IDeactivated.
- Hostitel musí být schopen při “tombstoningu” uložit tranzientní stav view modelu.
- Po návratu z “tombstoningu” hostitel dovolí view modelu obnovit tranzientní stav.
- Hostitel se nás bude snažit ve view modelech odstínit od všech WP7 nedomyšleností, záludností, nesmyslných omezení a programátorských vrtochů, které jsou nedílnou a z pohledu mobilní MS divize jistě i zábavnou součástí celkově kapriciozního životního cyklu třídy PhoneApplicationPage.
Nyní již můžeme přístoupit k vytvoření hostitele. Našim hostitelem bude třída PageBase, která je potomkem PhoneApplicationPage z WP7 frameworku. Nakonec jsem pro účely článku zvolil toto řešení, i když není problém v dalších dílech ukázat adaptér, který nás nutnosti vlastní stránky v aplikaci odvozovat z bázové třídy PageBase zbaví. Myslím si ale, že navigační model WP7 aplikací neposkytuje sám o sobě mnoho prostoru pro kreativitu, a proto nutnost dědit z PageBase místo třídy PhoneApplicationPage z WP7 za nějak zvlášť restriktivní nepovažuju.
Třída PageBase:
Kdykoli dojde k aktivaci stránky, čímž míním první navigaci na stránku, nebo návrat na stránku pomocí tlačítka Back, je volána metoda OnNavigatedTo. My jsme přepsali metodu OnNavigatedTo a v ní vždy nastavíme svou vlastnost WasLayoutUpdateCalled na false. Vlastnost WasLayoutUpdateCalled použijeme za chvíli, zde jen řekněme, že tuto vlastnost potřebujeme k tomu, abychom poznali, že již je vytvořen celý vizuální strom ovládacích prvků a že můžeme s ovládacími prvky bez obav pracovat. Bohužel v metodě OnNavigatedTo vizuální strom prvků vytvořen být nemusí. O některých problémech, které je nutné řešit, když není zcela vytvořen vizuální strom prvků, jsem již mluvil u ovládacího prvku WebBrowser v první části tohoto seriálu.
Metoda OnNavigatedTo také dá příkaz k vyzvednutí dříve uloženého tranzientního stavu pomocí metody loadSavedTransientState z vlastnosti PhoneApplicationPage.State. Tranzientní stav, pokud existuje, je pouze uložen do vlastnosti LastSavedTransientState a zatím se s ním nijak nepracuje. Klíčem, pod kterým je uložen stav celé stránky včetně tranzientního stavu view modelů, je plně kvalifikované jméno aktuální stránky (string stateKey = GetType().FullName). Měli bychom si být vědomi, že při této implementaci nesmíme mít jednu stránku v aplikaci nahranou vícekrát, protože by různé instance stejné stránky mezi sebou sdílely stav. Když budete trvat na tom, že jedna stránka může mít v systému několik instancí, není problém změnit generování klíče, pod kterým bude stav pro každou unikátně identifikovanou instanci uložen.
Pokračujme v našem scénáři. V konstruktoru si přihlašujeme odběr události LayoutUpdated (LayoutUpdated += PageBase_LayoutUpdated;), abychom byli notifikováni, že již můžeme bezpečně pracovat s ovládacími prvky.
Obslužná metoda handleLayoutUpdated je po každé navigaci na stránku spouštěna jen jednou, a proto se podíváme na hodnotu vlastnosti WasLayoutUpdateCalled – jestliže má hodnotu true, nic dalšího neděláme. Jinak metoda zkontroluje, zda instance PageBase je nově vytvořena, k čemuž dojde při první navigaci na stránku a také po obnovení z “tombstonovaného" stavu. Jinak řečeno – když je volán konstruktor třídy PageBase, znamená to, že máme stránku v “panenském” stavu, protože v ní nejsou žádná data a dokonce ani nebyly připojeny view modely. A proto bezparametrický konstruktor PageBase nastavuje vlastnost IsNewInstance na true, abychom v metodě handleLayoutUpdated věděli, že máme co do činění s novou instancí stránky. U nové stránky je třeba získat view modely, a pokud je stránka obnovena po “tombstoningu”, je třeba do view modelů nahrát tranzientní stav. Při variantě “nová instance stránky” metoda handleLayoutUpdated volá metodu LoadState.
Jestliže nebyl volán konstruktor a stránka obsahuje veškerý stav, k čemuž většinou dojde, když se vrátíme bez “tombstoningu” pomocí tlačítka Back na stránku, metoda handleLayoutUpdated volá pouze pomocnou metodu handleAllActivated, která proiteruje všechny dříve nahrané view modely, a když podporují rozhraní IActivated, tak na nich zavolá metodu Activate.
Metoda LoadState ihned deleguje na metodu restoreTransientState, jíž předá v objektu ElementIndexPair odkaz na aktuální stránku (this), která představuje “root” všech prvků, a relativní index nastavený na 0 (první prvek na této úrovni). V dalším argumentu jako prvek, pro který má být obnoven stav, předá opět odkaz na aktuální stránku (this) a poslední argument, ve kterém metoda restoreTransientState očekává naposledy použitý view model, je null, protože se žádnými view modely se ještě nepracuje.
Metoda restoreTransientState nejprve zkontroluje, jestli předaný ovládací prvek představuje View. K tomu použije objekt podporující rozhraní IViewModelResolver, který je uložen ve statické vlastnosti ViewModelManager. Přesněji řečeno, ve vlastnosti ViewModelManager můžete uložit delegáta (Func), který vrací objekt realizující rozhraní IViewModelResolver. Ve statickém konstruktoru třídy PageBase ukládám do vlastnosti ViewModelManager funkci, která vytváří v mé aplikaci výchozí IViewModelResolver s názvem ViewModelResolver (ViewModelManager = () => new ViewModelResolver();)
Rozhraní IViewModelResolver
Metodě IsView předáte vybraný objekt a ona vám vrátí true, jestliže objekt považuje za view, pro které by měl existovat view model. Metoda ResolveViewModel přijme objekt představující view a dohledá k němu view model.
Pro lepší představu vám mohu bez podrobnějšího komentáře ukázat třídu, která v aplikaci používající Posterous API dohledá k view view model na základě jmenné konvence. Za View se považuje každá instance z třídy, jejíž název končí znaky “View”, a k tomuto View je vrácen view model, který se jmenuje stejně jako view, ale končí znaky “ViewModel”. K view s názvem LoginView je tedy vrácen view model s názvem LoginViewModel.
Vraťme se k metodě restoreTransientState. Když metoda zjistí, že objekt představuje view, pokusí se dohledat dříve uložený tranzientní stav. Stav je uložen pro každé view v objektu IDictionary, konkrétně v třídě KnownTypesDictionary, pod klíčem, kterým je úplné jméno ovládacího prvku – úplným jménem se rozumí jméno třídy ovládacího prvku + jména tříd všech jeho vizuálních předků (ancestors). Ke jménu třídy každého prvku je přidán pořadový indexu prvku mezi prvky na stejné úrovni vizuálního stromu. Klíč generuje třída UIHelper v metodě GetTransientStateKey.
Metoda restoreTransientState se poté s s využitím metody prepareViewModel pokusí získat přes ViewModelManager view model (ViewModelManager().ResolveViewModel(obj), dále zjistí, jestli view model podporuje rozhraní ITransientStateManager, IInitialize a a IActivated a pokračuje takto:
- Jestliže máme uložen tranzientní stav (došlo k “tombstoningu”) a view model podporuje rozhraní ITransientStateManager, zavoláme metodu LoadState z rozhraní ITransientStateManager (stateManager.LoadState(LastSavedTransientState[stateKey]); a view model tak dostane šanci načíst dříve uložená tranzientní data.
- Když nemáme uložen tranzientní stav a view model podporuje rozhraní IInitialize, zavoláme metodu IInitialize.Init. View model tak dostane šanci nahrát data, ať už se souboru, z webové služby nebo jiného datového zdroje, která mají být zobrazena v aktuálním view.
Poté, co view model obnoví nebo inicializuje svůj stav, je volána metoda activateAndSetDataContext, která na view modelech podporujících rozhraní IActivated zavolá metodu Activate a nastaví view model jako datový zdroj (“DataContext”) view.
Nakonec pro všechny dceřiné prvky, které mohou představovat vnořená view (např. user control), rekurzivně zavoláme opět metodu restoreTransientState. K získání dceřiných prvků a jejich relativního indexu mezi prvky na stejné úrovni zanoření ve vizuálním stromu je použita metoda UIHelper.GetChildren.
Metoda LoadState, restoreTransientState a další pomocné metody, o kterých jsme mluvili v předchozích odstavcích:
Za povšimnutí stojí ve scénáři metody restoreTransientState ještě několik dalších věcí:
- Když zjišťujeme, jestli objekt podporuje rozhraní IInitialize (metoda getInitObjectWithSyncContext), ihned do view modelu zpropagujeme synchronizační kontext (initObject.SynchContext = SynchronizationContext.Current;), aby například událost PropertyChanged mohla být vyvolávána vždy v UI vláknu.
- Pomocná metoda selectViewModel(frameworkElement, lastViewModel, currentViewModel); odpovědná za výběr view modelu zajistí, že když IViewModelResolver nenalezne pro view žádný view model, automaticky view přiřadí view model použitý u předchozího view. Kdy to potřebujete? Třeba když stránku rozdělíte na několik nezávislých “user controls”, tak se můžete rozhodnout, jestli bude mít každý user control (view) svůj view model, nebo view model vytvoříte jen pro celou stránku a “user controls” view modely automaticky “zdědí”. To byl jen jeden příklad - úroveň zanoření view je zcela ve vaší režii. Slovo “zdědí” je v uvozovkách, protože nejde o klasické dědení DataContextu, ale k předání posledně použitého view modelu dojde i tehdy, když dvě view nemají společné “nadview”.
Poznámka: Nejprve jsem chtěl volání metod Init a LoadState na view modelech a následné nastavení DataContextu u view dát do jiného vlákna. Problém je v tomto případě s dědením view modelů. Když přiřadíte view model, na kterém jste zavolali asynchronně metodu Init, dalšímu vnořenému View, tak se může stát, že metoda Init ještě nedoběhla, view model u prvního view nastaven není, ale u vnořeného (druhého) view již nastaven je, což může vést k podivným a kvůli použití více vláken obtížně reprodukovatelným chybám. Za asynchronní zpracování časově náročných scénářů zodpovídají samotné view modely, které by měly přepisovat virtuální metodu DoInternalAsyncInit z ViewModelBase. Metodu DoInternalAsyncInit jsem popisoval v předchozím článku.
Viděli jsme, kdy hostitel volá metody Init, Activate a kdy voláním metody LoadState obnoví tranzientní stav ve view modelech. Ještě nám zbývá projít, kdy ukládáme tranzientní stav a kdy notifikujeme view modely voláním jejich metody Deactivate o tom, že k nim přidružené view (stránka) není již aktivní.
Nebudeme při ukládání tranzientního stavu nijak pátrat, jestli došlo k “tombstoningu”, ale stav view modelů do PhoneApplicationPage.State uložíme vždy, když dojde k opuštění stránky. Hlavním důsledkem tohoto rozhodnutí je, že s obsluhou metod Application_Launching, Application_Activated, Application_Deactivated a Application_Closing, kterou jsem popisoval minule, se nemusíte kvůli “tombstoningu” trápit a zmíněné metody můžete většinou zcela ignorovat.
O opuštění stránky jsm informováni v metodě OnNavigatedFrom, kterou přepíšeme. Metoda OnNavigatedFrom voláním metody SaveState uloží stav všech view modelů a poté metoda handleAllDeactivated na všech modelech podporujících rozhraní IDeactivated zavolá metodu Deactivate.
Popisovat kompletně logiku v metodě SaveState nemá smysl, protože tato metoda je reverzní k metodě LoadState. Pro všechny view rekurzivně vyhledá jejich view modely a dovolí jim uložit pod unikátním klíčem view, který je vygenerován třídou UIHelper, tranzientní stav. Ten samý tranzientní stav view modelů, jehož obnovení v metodě LoadState jsem popisoval výše.
Za komentář stojí, že podmínka if (stateManager != null && stateManager != lastDataContext) ošetřuje, abychom neukládali trazientní stav view modelu, který slouží jako DataContext u více View, opakovaně, ale vždy pouze jednou.
Tranzientní stav všech view modelů je ukládán do speciálního objektu Dictionary s názvem KnownTypesDictionary.
var statebag = new KnownTypesDictionary();
Připomínám, že při tombstoningu je možné uložit jen serializovatelné objekty. Představme si nyní, že KnownTypesDictionary je potomek třídy Dictionary<string, object>. Do takového objektu Dictionary můžeme vložit kteroukoli instanci z třídy, která je přímo či nepřimo potomkem třídy Object, ale při pokusu o serializaci dostaneme chybu, protože naše potomky DataContractSerializer nezná a očekává, že v kolekci budou jen instance třídy Object, a ne instance odvozených tříd. Pomocí atributu KnownType, který je aplikován na KnownTypesDictionary, můžeme DataContractSerializer informovat, které všechny třídy má v objektu Dictionary očekávat. Jednou z možností, jak to udělat, je předat atributu KnownType název statické metody, která seznam “známých” tříd vrací.
Dnes si ukážeme jednoduchou verzi třídy KnownTypesDictionary, která v metodě GetKnownTypes vrátí fixní seznam "známých” tříd. Příště si ukážeme, jak budou “známé” třídy dohledány a registrovány automaticky bez nutnosti vytvářet seznam známých tříd vždy znovu a “natvrdo” v každé aplikaci. V dalších dílech také zjistíme, jak právě vytvořená infrastruktura nám dovolí vytvářet WP7 aplikace příslovečným lusknutím prstu, tedy spíš nečetným lusknutím prstů o klávesnici.
.
Předcházející články:
Tipy pro Windows Phone 7 aplikace I
Tipy pro Windows Phone 7 aplikace II – podpora životního cyklu aplikace (včetně tombstoningu) ve "view modelech”
Monday, 03 January 2011 15:46:50 (Central Europe Standard Time, UTC+01:00)
Compact .Net Framework | Návrhové vzory | Silverlight | WP7
Friday, 17 December 2010
Tipy pro Windows Phone 7 aplikace II – podpora životního cyklu aplikace (včetně tombstoningu) ve "view modelech”
Již v prvním dílu seriálu o vývoji WP7 aplikací jsem zmiňoval nejen to, že mobilní verze Silverlightu ve WP7 je založena na Silverlightu 3, ale také, že mobilní Silverlight má své unikátní rysy, které v žádné desktopové verzi Silverlightu nenalezneme. Jednou ze změn je životní cyklus aplikace, včetně tzv. tombstoningu. Termín ”tombstoning”, kterého se i v tomto článku budu držet, abych nemusel zavádět nějaké směšně znějící české ekvivalenty, má asi naznačovat, že WP7 podporují nejen tradiční spuštění a ukončení aplikace, ale i jakýsi hybridní stav, v němž instance naší aplikace může být dočasně ukončena (“umrtvena”) například tím, že uživatel spustí jinou aplikaci, a poté může být z hlediska uživatele WP7 telefonu naše původní aplikace navrácena k životu, a to dokonce ve stavu, v jakém ji předtím uživatel zanechal. O smysluplnosti “tombstoningu” mám své pochybnosti a raději bych ve WP7 viděl tradiční multitasking, ale vývojářův boj s chováním “zombie” aplikací ve stavu “tombstoningu” má také něco do sebe. V tomto článku bych rád posunul boj se “zombie-tombstonovanými” aplikacemi do dalšího levelu a nabídnul pár “cheatů” .
Přechody mezi stavy WP7 aplikace nám nejlépe objasní metody pro obsluhu životního cyklu aplikace, které jsou po založení projektu automaticky vygenerovány v souboru App.xaml.cs.
Metoda Application_Launching je provedena jednou po spuštění aplikace. Jde o metodu, ve které můžete načíst dříve uložená data z “isolated storage”. Tato metoda NENÍ volána při obnovení “tombstonované” aplikace.
Metoda Application_Closing je provedena jednou při ukončení aplikace. Jde o metodu, ve které typicky uložíte data do “isolated storage”. Jde o stejná data, která načtete při dalším spuštění aplikace v již popsané metodě Application_Launching a můžeme tedy říci, že jde o data s delší záruční lhůtou, která mají význam pro různé instance aplikace. Když stáhnete z webové služby nějaké číselníky (typy zákazníků, kategorie objednávek) a nechcete je po startu aplikace stahovat vždy znovu, uložíte si je a při příštím spuštění aplikace rychleji naběhne, protože nemusí stahovat všechna data z webových služeb ihned po startu.
Další metody se týkají již tombstoningu. Metoda Application_Deactivated je volána vždy, když je vaše aplikace “tombstonována” a v této metodě byste měli uložit všechna data, která potřebujete k tomu, abyste mohli po návratu z “tombstonovaného” stavu aplikaci zobrazit uživateli tak, jako kdyby běžela celou dobu a k žádnému “tombstoningu” nedošlo. Pro vývojáře ale “tombstoning” ve skutečnosti znamená, že aktuálně běžící instance aplikace je zlikvidována! Můžeme tedy říci, že v této metodě hlavně ukládáme data s kratší záruční lhůtou, která mají význam jen pro další instanci aplikace po návratu z “tombstonovaného” stavu. Data, která chcete mít k dispozici i po obnovení z “tombstoningu” můžete ukládat pod vámi zvolenými identifikátory v objektu typu IDictionary ve vlastnosti PhoneApplicationService.Current.State. Data, která zde uložíte, musí být serializovatelná, protože se nedrží jen v paměti telefonu, ale jsou ukládána i do souboru.
Je ale nutné si uvědomit, že když je aplikace “tombstonována”, nemáte garantováno, že se uživatel do vaší aplikace vrátí a že budete obnoveni z “tombstonovaného” stavu. I v této metodě je proto vhodné ukládat data, která ukládáte v metodě Application_Closing popsané výše.
Uživatel může také spustit vaší aplikaci znovu, tedy spustit novou instanci aplikace, aniž by se vrátil k dříve “"tombstonované” aplikaci a vy svůj stav - data, o nichž jsem říkal, že mají kratší záruční lhůtu - dříve uložený v metodě Application_Deactivated nikdy nepoužijete. Když se uživatel vrátí do vaší aplikace, což většinou nastane po stisknutí tlačítko “Back”, kdy se v zásobníku dříve spuštěných aplikací, který je spravován přímé operačním systémem, stane aktivním vaše aplikace, musíte obnovit dříve uložený stav v metodě Application_Activated. Napsal jsem, že jde o zásobník aplikací, ale je potřeba si uvědomit, že jde spíš o metaforu, protože aplikace a jejich stránky nejsou nikdy fyzicky uloženy, ale při “tombstoningu” většinou bez milosti zlikvidovány, a metoda Application_Activated je volána v nové instanci naší aplikace. WP7 si jen v zásobníku pamatují, jaké aplikace a v jakém pořadí byly spuštěny a která stránka v konkrétní aplikaci byla aktivní.
Dále je potřeba si osvojit tato pravidla:
- Metoda Application_Activated NENÍ volána po spuštění nové (“netombstonované”) instance aplikace. Po spuštění nové (“netombstonované”) instance aplikace je volána pouze metoda Application_Launching.
- Metoda Application_Deactivated NENÍ volána při úplném ukončení aplikace. Při úplném ukončení aplikace je volána pouze metoda Application_Closing.
Zjednodušeně bychom mohli odpovědnosti metod v souboru App.xaml.cs popsat tímto fragmentem kódu:
Měli bychom ale vědět, že k tombstoningu může dojít kdykoli a podle mých testů na emulátoru i reálném zařízení může být aplikace “tombstonována”, i když se zrovna obnovuje z předchozího “tombstonovaného” stavu a je v metodě “Application_Activated“. Autoři WP7 se s “tombstoningem” moc nepatlají a bez skrupulí po určité době zlikvidují vlákna aplikace, což poznáte podle výjimky ThreadAbortException.
Také se mi nelíbí, že bych měl v tomto jediném souboru ukládat a obnovovat data pro všechny stránky (“formuláře”) aplikace. Rozhodl jsem se, že na obsluhu těchto metod rezignuju a místo životní cyklu aplikace se budu zajímat jen o životní cyklus view modelu (presentation modelu, chcete-li) u každé stránky ve WP7 aplikaci.
Dnes si ukážeme bázovou třídu pro view modely a další podpůrné třídy se službami, která nás svou spoluprací zbaví nutnosti při psaní každé stránky myslet na to, že autoři WP7 aplikací se vyžívají v recidivě nekrofilního chování u WP7 aplikací. Drahý bratr Sigmund Freud by se po prohlídce stavového automatu “tombstonované“ aplikace od architektů WP7 na své pohovce jistě tetelil radostí nad předaným šokujícím materiálem.
Nejdříve si ale navrhneme minimální množinu vlastností, kterou by měl každý view model splňovat, abychom se již nemuseli životním cyklem WP7 aplikace příliš zabývat při návrhu každé stránky.
- Musíme být schopni nahrát data při vytvoření nového view modulu. Také bychom měli zajistit, že když dojde k “tombstoningu” aplikace ještě před získáním všech dat, nezůstane view model v nějakém nekonzistentním stavu s třetinou nahraných dat, ale po obnovení z “tombstoningu” dostane šanci nahrát data znovu.
- Budeme schopni při “tombstoningu” automaticky perzistovat dočasný stav každého view modelu. Od této chvíle začneme říkat dočasnému stavu stav tranzientní. V tomto článku se zabývám jen tranzientním stavem aplikace, o perzistování “trvanlivějších” dat do “isolated storage” se pobavíme v některém z dalších článků.
- Po obnovení aplikace z “tombstonovaného” stavu každý view model automaticky nahraje svůj tranzientní stav.
Pro uložení a nahrání tranzientního stavu nadefinujeme rozhrani ITransientStateManager.
- Měli bychom dát šanci view modelům zareagovat na to, že stránka, ke které jsou přidruženy, se stala aktivní stránkou, i na to, že k nim přidružená stránka aktivní už není, ať už proto, že došlo k “tombstoningu” nebo uživatel přešel na jinou stránku v aplikaci.
Pro tyto účely máme rozhraní IActivated a IDeactivated.
- Budeme mít sice bázovou třídu pro view modely, ale její použití si nebudeme vynucovat. View model může být v aplikaci kterákoli třída. I když tato třída nebude potomkem bázové třídy pro view modely, bude moci volitelně využít většinu služeb, které jsou popsány v přechozích bodech.
- Jedná se o view modely, měli bychom tedy na úrovní bázové třídy podporovat rozhraní INotifyPropertyChanged, které nám dovoluje notifikovat o změně hodnoty ve vlastnostech view modelu. Rozhraní INotifyPropertyChanged je ve WPF i v Silverlight aplikacích všudypřítomné, proto si navrhneme další bázovou třídu PropertyChangedBase, která nám kromě implementace rozhraní INotifyPropertyChanged přinese další užitečné služby.
Jaké další užitečné služby má třída PropertyChangedBase?
Nemusíme vyvolávat událost PropertyChanged zadáním jen zadáním názvu vlastnosti (RaisePropertyChanged(“UserName”)), což může vést k chybě za běhu aplikace, když uděláme překlep v názvu vlastnosti ((RaisePropertyChanged(“UseName”)), ale můžeme předat název vlastnosti ve formě lambda výrazu, jehož syntaxe je zkontrolována již kompilátorem. K tomu nám slouží metoda RaisePropertyChangedEvent(Expression> propertyDefinition). Potomci třídy PropertyChangedBase mohou o změně hodnoty vlastnosti informovat takto:
U potomků třídy PropertyChangedBase, kterými budou i naše view modely, si můžeme také zavoláním metody RaiseAllPropertiesChanged vynutit vyvolání události o změně hodnoty pro každou vlastnost. Další metodě s názvem RaisePropertiesChanged(params string[] properties) můžeme předat názvy vlastností, pro které má být vyvolána metoda PropertyChanged.
A teď si již můžeme vytvořit slibovanou třídu ViewModelBase, která je potomkem třídy PropertyChangedBase a podporuje všechna rozhraní zmíněná dříve.
V konstruktoru třída ViewModelBase přijímá titulek zobrazované stránky, který je uložen do vlastnosti PageTitle.
Metoda Init z rozhraní Initialize by měla být volána vždy, když je view model vytvořen, nebo když je po obnovení aplikace z “tombstonovaného” stavu zřejmé, že view model nenahrál všechna data, což se zjistí voláním virtuální metody IsAllInitDataLoaded, která v této abstraktní třídě vždy vrací true a čeká na to, až odvozené konkrétní view modely dosadí svou logiku, kdy je považován view model za nahraný. Metoda Init by měla být taky volána, když odvozený view model obnovená tranzientní data považuje za špatná / zastaralá a nemůže je používat, což nám dá najevo nastavením vlastnosti IsInvalidModel na true. K rychlému zjištění, jestli je možné view model dále používat slouží derivovaná vlastnost CanUseModel, která spojuje logiku obsaženou ve vlastnosti IsInvalidModel a metodě IsAllInitDataLoaded.
Metoda Initialize.Init
V metodě Init nejdříve nastavíme vlastnost IsInvalidModel na true, protože naše metoda Init probíhá a kdyby došlo k “tombstoningu”, nemáme všechna data a je potřeba volat metodu Init znovu. Poté jen načteme titulek aplikace do vlastnosti AppTitle, inicializujeme vlastnost SuppressValidating, která slouží k dočasnému potlačení ověřování platnosti data zadaných uživatelem ve view modelu, na hodnotu false. O vlastnosti SuppressValidating budeme více mluvit v dalším článku včetně vysvětlení významu metody ValidateData a vlastnosti HasValidData.
Dále ViewModelBase nastavením vlastnosti IsInvalidModel na false sděluje, že jeho inicializace proběhla, všechna data v modelu považuje za platná a dá šanci odvozeným třídám, aby inicializovaly svá data voláním chráněné virtuální metody DoInternalInit, která má ve ViewModelBase prázdnou implementaci a do které odvozené třídy dají svou specifickou logiku pro načtení dat. Vlastnost IsInvalidModel mohou samozřejmě odvozené třídy nastavit v metodě DoInternalInit opět na true, ale my ve vlastnosti IsInvalidModel hodnotu true nenecháváme, protože si přepsání metody DoInternalInit v odvozených třídách nevynucujeme a v bázové třídě ViewModelBase nemůžeme vědět, kdy odvozené třídy považují svá data za neplatná.
V metodě Init nakonec také voláním metody další chráněné virtuální metody s názvem DoInternalAsyncInit v samostatném vlákně šanci odvozeným třídám asynchronně inicializovat svá data. Metodu DoInternalAsyncInit by měly odvozené třídy používat k časově náročné inicializaci, kterou není vhodné vhodné dávat do synchronně volané metody DoInternalInit a blokovat tak hlavní thread aplikace.
Mohlo by vás také zaujmout, že v sekci catch má speciální zacházení výjimka ThreadAbortException – tato výjimka není propagována výše, protože ji vyvolá samotné běhové prostředí Silverlightu při násilném “tombstoningu”, jak jsem poznamenal na začátku tohoto článku.
Rozhraní IActivated s metodou Activate a rozhraní IDeactivated s metodou Deactivate ve ViewModelBase mají jen prázdnou implementaci a čekají na to, jakou logiku do nich vloží odvozené třídy. Mimochodem – na tomto místě bychom měli poprvé vytušit, že budeme potřebovat “hostitele” našich view modelů, který bude vědět, kdy volat metody Activate, Deactivate, Init a další.
Třída ViewModelBase explicitně implementuje rozhraní ITransientStateManager, které jsme si zavedli pro podporu automatického ukládání a nahrávání tranzientního stavu. Metody LoadState a SaveState ale po svém vyvolání jen předají řízení chráněným virtuálním metodám DoInternalLoadTransientState a DoInternalSaveTransientState, aby dali šanci i odvozeným třídám změnit způsob uložení a nahrání tranzientního stavu, i když odvozené třídy jsou většinou spokojeni s tím, že to za ně zvládne předek ViewModelBase.
Třída ViewModelBase ve svém statickém konstruktoru dosazuje výchozí objekt podporující rozhraní ITransientStateHelper, které je klíčové pro uložení a obnovení tranzientního stavu view modelů a na které delegují i metody DoInternalLoadTransientState a DoInternalSaveTransientState v předchozím výpisu.
Všimněte si, že metoda DoInternalLoadTransientState po obnovení tranzientního stavu zkontroluje, jestli se dá view model používat, a pokud ne, zavolá i v této fázi metodu Init.
if (!CanUseModel)
{
Init();
}
A nyní se podíváme na mocné rozhraní ITransientStateHelper
Je asi zřejmé, že metoda GetTransientState vrátí v objektu Dictionary tranzientní stav objektu, který jí byl předán v argumentu obj. Metoda RestoreTransientState naopak obnoví tranzientní stav objektu v argumentu obj hodnotami v argumentu savedState.
Metoda IsTransientStateEnabledForObject zjistí, jestli je možné z předaného objektu získat tranzientní stav. Jak uvidíme, odvozené view modely mohou odmítnout uložení tranzientního stavu a kdekoli v aplikaci můžeme jejich rozhodnutí jednoduše zjistit předáním instance view modelu této metodě.
Zde je jedna z možných realizací rozhraní IStateTransientStateHelper, kterou používá i naše třída ViewModelBase.
Odpovědnosti metod GetTransientState a RestoreTransientState jsem již popsal, nyní jen zmíním pár specialitek v kódu třídy TransientStateHelper.
Metoda GetTransientState zjistí, jestli předaný objekt má tranzientní stav tak, že zavolá metodu IsTransientStateEnabledForObject(obj).
Metoda IsTransientStateEnabledForObject kontroluje, jestli třída, ze které objekt pochází, nezakázala vydání tranzientního stavu tím, že je na ní aplikován atribut [NonTransientState].
Atribut NonTransientState
Atribut NonTransientState může být aplikován nejen na celou třídu, ale i na jednotlivé vlastnosti objektu, které nemají být součástí tranzientního stavu. Metoda GetTransientState neukládá celé view modely, ale s využitím reflexe jen hodnoty jejich vlastností. Atribut NonTransientState dovoluje vyřadit vlastnosti, které v tranzientním stavu view modelu nemají co dělat. To ale není vše – přímo metoda GetTransientState dle jmenné konvence vyřadí všechny vlastnosti, jejichž suffix je v poli IGNORE_METHOD_SUFFIX_LIST
public static readonly IEnumerable IGNORE_METHOD_SUFFIX_LIST = new[] { "Command", "Action", "Helper", "Service", "SynchContext"};
Do tranzientního stavu se tak nedostanou ve view modelech se často nacházející, ale s tranzientním stavem view modelu nic nemající a navíc většinou neserializovatelné objekty jako jsou objekty ICommand (vlastnost SelectCommand, SaveCommand), delegáti TextboxTextChangedAction, služby (ILoggerService) a další.
Metoda RestoreTransientState obnoví tranzientní stav objektu. Za zmínku stojí, že když je předaný objekt potomkem PropertyNotificationBase, tak nastavením vlastnosti notificationBase.SuppressPropertyChangedNotification na true potlačíme dočasně vyvolání události OnPropertyChanged, protože je vhodné, abychom událost nevyvolávali, když view model neobsahuje všechna data a nevíme, kdo všechno na událost reaguje a jaká další, nyní ve view modelu se nenacházející data, by chtěl načíst. Jak jsem ale psal v požadavcích na začátku článku, view model nemusí podporovat žádná rozhraní, nemusí být potomkem ViewModelBase ani PropertyNotificationBase, a proto si ani dědění z PropertyNotificationBase v této metodě nevynucujeme. Poté, co metoda RestoreTransientState obnoví hodnoty všech vlastností, má TransientStateHelper povinnost notifikovat okolí o změně hodnot všech vlastností view modelu. Jestliže je objekt s obnoveným tranzientním stavem potomkem PropertyNotificationBase, zavoláme metodu RaisePropertiesChanged(propertyNames.ToArray()), jinak se metoda RestoreTransientState pokusí dohledat opět za pomoci reflexe na objektu metodu nazvanou RaisePropertyChangedEvent, která přijímá název vlastnosti a kterou použije pro hromadnou distribuci událostí OnPropertyChanged.
Ovládací prvky obsažené (nejen) v control toolkitu jsou přecitlivělé na pořadí vyvolávání události, a proto jsou vlastnosti seřazeny nyní tak, aby vlastnosti začínající slovem Selected vyvolávaly událost OnPropertyChanged jako poslední. Máte-li ve view modelu kolekci nazvanou Orders (všechny objednávky) a SelectedOrder (vybraná objednávka z této kolekce), je zaručeno, že událost OnPropertyChanged pro vlastnost SelectedOrder bude vyvolána až po události OnPropertyChanged pro vlastnost Orders.
Prefixy vlastností, které mají vyvolávat události jako poslední, jsou v proměnné LAST_SET_VALUE_METHOD_PREFIX. Tyto vlastnosti jsou označeny příznakem LastInit z enumerace PropertyType.
public static readonly IEnumerable LAST_SET_VALUE_METHOD_PREFIX = new[] { "Selected" };
To by pro dnešek k třídě ViewModelBase a jejím pomocníkům stačilo:
Příště se podíváme hlavně na “hostitele” view modelů, který by měl být schopen:
- Volat na view modelech ve “správnou dobu” metody Init, LoadState, SaveState, Activate, Deactivate.
- Připojit view modely k view (stránce).
- Náš hostitel bude podporovat i více view modelů na jedné stránce, včetně “dědění” a sdílení použitých view modelů mezi různými view na stránce.
A také si příště vysvětlíme, proč při získání tranzientního stavu ve ViewModelbase místo objektu Dictionary používáme vlastní třídu KnownTypesDictionary. Jak možná tušíte, i název “KnownTypes” odkazuje k tomu, že mají-li být hodnoty uložené v tranzientním stavu serializovatelné, tak DataContractSerializer, používaný infrastrukturou WP7 k serializaci tranzientního stavu, musíme přesvědčit, že v objektu Dictionary jsou jen objekty z jemu “známých” tříd.
Předcházející články:
Tipy pro Windows Phone 7 aplikace I
Friday, 17 December 2010 19:25:14 (Central Europe Standard Time, UTC+01:00)
C# | Compact .Net Framework | Návrhové vzory | Silverlight | WP7
Saturday, 18 September 2010
Pozvánka na mé kurzy OOP, UML a návrhových vzorů a odpovědi na některé dotazy zaslané emailem - podzim 2010
Opět bych Vás všechny rád pozval na pravidelný podzimní běh kurzů Objektovými principy a návrhovými vzory řízený design a vývoj kvalitních aplikací 1 a Pokročilé návrhové vzory a objektové principy 2. Níže také naleznete odpovědi na pravidelně se opakující dotazy v emailech.
Veřejný kurz Objektovými principy a návrhovými vzory řízený design a vývoj kvalitních aplikací 1
Datum konání kurzu: 22. 11. – 24. 11. 2010
Místo konání:
Školící středisko Tutor
U Půjčovny 2
110 00 Praha 1
Po celý den máme k dispozici wifi připojení a samozřejmě také teplé a studené nápoje. V ceně kurzu jsou obědy v hotelu.
Podrobné informace o kurzu a možnost přihlásit se na kurz
Program kurzu
Výběr z ohlasů na kurz
Veřejný kurz Objektovými principy a návrhovými vzory řízený design a vývoj kvalitních aplikací 2
Datum konání kurzu: 29. 11. – 1. 12. 2010
Místo konání:
Školící středisko Tutor
U Půjčovny 2
110 00 Praha 1
Po celý den máme k dispozici wifi připojení a samozřejmě také teplé a studené nápoje. V ceně kurzu jsou obědy v hotelu.
Podrobné informace o kurzu a možnost přihlásit se na kurz
Program kurzu
Výběr z ohlasů na kurzy
Zde jsou některé nové ohlasy na kurzy. Raději podotýkám, že tato hodnocení jsou mi zaslána účastníky z vlastní vůle a já určitě účastníky nenutím, aby mi psali nějaká hodnocení, natož lichotivá, hned na kurzu ani po něm, jak se prý děje u konkurence.
pan Martin Pospíšil, společnost Team Trackers s. r. o.
Děkuji za materiály a za výborné školení. Líbilo se mi ještě víc než jednička, díky těm praktickým příkladům, které jsme řešili společně.
Ing. Rudolf Plíva, společnost NOWIRE
Děkuji za velmi zajímavé a přínosné školení!
pan Jiří Filous, společnost Česká pojišťovna a. s.
Ano, mám zájem o zasílání informací o nových kurzech a děkuji za skvělé (i když náročné) školení.
Odpovědi na některé dotazy, které jsme obdrželi od dříve přihlášených zákazníků emailem.
Otázka: Na kurz půjdu, ale raději počkám s přihláškou na týden před školením, protože stejně jako konkurence mi dáte “last minute” slevu ne?
Odpověď: Hry s nějakou uměle nadsazenou cenou, která je potom těsně před kurzem snižována a nabízena v rámci “Last minute” nabídky, nehrajeme. Přijde mi nespravedlivé a nesmyslné, aby každý účastník platil jinou částku v závislosti na dni přihlášení. Všichni dosavadní účastníci mohou potvrdit, že zaplatili cenu, která je uvedena u školení a je jedno, zda se přihlásili 3 měsíce nebo 3 dny před kurzem.
Otázka: Na kurzu bychom rádi diskutovali záležitosti, které se týkají našeho projektu, je to možné? Je možné s vámi probrat podrobně i vývojářské specialitky, které se týkají C#, C++,Windows Forms, Silverlightu, Windows Mobile….
Odpověď: Na veřejném kurzu můžeme diskutovat o věcech, které zajímají všechny účastníky. Specifické dotazy, které se týkají vašich projektů, je nejlepší probrat se mnou po 16. hodině, kdy skončí oficiální část kurzu, ale já jsem vám samozřejmě k dispozici od 16:00 do umdlení mého či vašeho. Jestliže chcete 3 dny věnovat jen svému projektu, zvažte inhouse variantu kurzu, kde si můžete témata definovat sami. Pro podrobné informace o podmínkách inhouse kurzů napište prosím Petře Steinové (petra@renestein.net), která Vám velmi ráda obratem pošle nabídku.
Těším se s Vámi na setkání na kurzu!
René Stein
Saturday, 18 September 2010 10:17:31 (Central Europe Standard Time, UTC+01:00)
Analytické drobky | Kurzy UML a OOP | Návrhové vzory | UML
Wednesday, 07 April 2010
Pozvánka na kurz Objektovými principy a návrhovými vzory řízený design a vývoj kvalitních aplikací 1 – jaro 2010
Update: Kurz je zcela obsazen včetně náhradníků.
Rád bych Vás pozval na další běh kurzu Objektovými principy a návrhovými vzory řízený design a vývoj kvalitních aplikací 1. Pokud se někdo z Vás (oprávněně) diví, proč tak pozdě a proč Vás nezvu i na kurz OOP 2, níže v tomto spotu nalezne odpovědi.
Veřejný kurz Objektovými principy a návrhovými vzory řízený design a vývoj kvalitních aplikací 1
Datum konání kurzu: 14. 6. – 16. 6. 2010
Místo konání:
Školící středisko Tutor
U Půjčovny 2
110 00 Praha 1
Po celý den máme k dispozici wifi připojení a samozřejmě také teplé a studené nápoje. V ceně kurzu jsou obědy v hotelu.
Podrobné informace o kurzu a možnost přihlásit se na kurz
Program kurzu
Výběr z ohlasů na kurz
Odpovědi na některé dotazy, které jsme obdrželi od dříve přihlášených zákazníků emailem.
Otázka: Proč se kurz nekoná na předchozím místě (v hotelu Villa), kde byli naši kolegové?
Odpověď: Po vyhodnocení ohlasů na kurz jsme zjistili, že účastníci nebyli (stejně jako já ) příliš spokojeni s jídlem. Také jsme při domlouvání hotelu byli ujišťováni, kolik účastníků se může na kurz přihlásit a jak lze pro ně uspořádat pohodlně prostory, a tyto informace se v průběhu kurzu jevily jako marketingové fráze až blafy velmi optimistickými nebo nepočítaly s průměrným Evropanem, ale standardním liliputem. Stručně řečeno – poměr cena/výkon nebyl u čtyřhvězdičkového hotelu příliš dobrý. Jedinou výhodou hotelu Villa bylo parkování, nyní bude lepší na ”poslední míli” použít MHD.
Otázka: Proč se koná kurz tak pozdě (oproti přechozím rokům) a proč se nekoná kurz OOP2.
Odpověď: Byl a jsem vytížen tento rok dalšími projekty a nalézt vhodné termíny bylo problematické. Také jsme sháněli nové prostory, kde se bude kurz konat (viz předchozí odpověď). Termín kurzu OOP2 vycházel již na červenec a v době dovolených nemá smysl kurz pořádat. Při větším zájmu vyhlásím (co nejdříve) více termínů kurzu OOP2 na podzim.
Těším se na setkání na kurzu!
Wednesday, 07 April 2010 12:48:36 (Central Europe Standard Time, UTC+01:00)
Analytické drobky | Kurzy UML a OOP | Návrhové vzory | UML
Sunday, 21 March 2010
Výhody a nevýhody softwarových továren
Emailem jsem dostal zajímavou otázku, jaký je můj názor na softwarové továrny a kde vidím výhody a nevýhody softwarových továren. Odpověď nakonec publikuji i zde – už jen proto, že jsem si při jejím psaní uvědomil, že na továrnu kladu stejné nároky jako na kteroukoli další knihovnu v systému a že výběr softwarové továrny se u mě moc neliší od výběru třeba ORM Frameworku. Nejde o taxativní výčet výhod a nevýhod, ale spíš o volně nahozená témata, která mě za 20 minut psaní příspěvku napadla.
SW továrny jsou jen pokračováním trendu, že nemá smysl vynalézat znovu kolo ani v případě, že by bylo o dvě setiny procenta krásnější než to staré, které je již navržené a hotové. SW továrna říká – namísto opakování chyb svých předchůdců použijte ověřené zkušenosti, osvědčené praktiky, doporučené postupy a již otestovaný kód. Namísto obecného návrhového vzoru nebo vágně popsaného doporučovaného postupu máte k dispozici osvědčenou knihovnu, základní průvodce a odborný polštář, který by měl zabránit, že napácháte v dané oblasti velké množství hrubých bezpečnostních nebo jen s duchem technologie neslučitelných chyb.
Takže výhody:
1) Osvědčená řešení ihned po ruce. Tím se cíl SW faktory moc neliší od obecných frameworků a návrhových vzorů.
2) Není nutné psát infrastrukturní kód přímo v SW firmě (ISV). Je možné se soustředit na problémovou doménu zákazníka a ne na to, jak centrálně publikovat události nebo jak propojit různé pohledy v aplikaci tak, aby spolu dokázaly komunikovat .
3) Pokud stejnou továrnu používáte napříč všemi projekty, je zaučení nového vývojáře na dalším projektu jednodušší a rychlejší.
4) SW továrna používá praktiky osvědčené v dané technologii. Takže odlišně jsou zakódovány původně platformně nezávislá doporučení a všeobecně zaměřené vzory v SW továrně pro ASP.Net a jinak pro Silverlight. Místo popisu obecných návrhových vzorů je již vzor adaptován na konkrétní cílové prostředí.
Nevýhody:
1) Kód nemáte pod kontrolou a musíte se spolehnout, že továrna sama neobsahuje příliš kritických chyb ani otravných chyb s nižší prioritou. To platí o jakékoli abstrakci (frameworku, knihovně), kterou na projektu používáte, ale myslím, že ani v Microsoftu stále nemají SW továrny stejnou podporu jako samotný .Net Framework. A také by měla být známa alespoň orientační roadmapa SW továrny, abyste věděli, kam autoři směřují a jestli jsou si vědomi nevýhod a omezení stávající verze SW továrny. Od továrny, která se ihned po svém uvedení honosí přídomky „experimentální, testovací, zkušební“, bych dal ruce pryč. Jen ti nejodvážnější z nás si dovolí svým zákazníkům za půl roku po zatracení SW továrny jako slepé uličky tvrdit, že musí trochu poštelovat kardiostimulátor výkonného srdce aplikace a že místo plánovaných dvou hodin za přidání dvou vlastností do objednávky zákazník zaplatí dva měsíce migrace na jinou SW továrnu. Nenechal bych se zmýlit tím, že některé firmy pro odvážlivce používající jejich experimentální SW továrny a frameworky razí lichotivý titul „early adopter“ – méně korektní a pravdě bližší překlad z marketingového slovníku totiž zní „natěšený všehoschopný blbec“.
2) Univerzální řešení jako je SW továrna může být pro jednoduché aplikace zabijákem. Více času strávíte integrací aplikace do rámce vynucovaného SW továrnou než psaním obchodní logiky specifické pro aplikaci a důležité pro zákazníka.
3) U některých složitějších aplikací zjistíte, že rámec SW továrny je příliš těsný a vy potřebujete SW továrnu pro svůj projekt upravit nebo rozšířit. Náklady na rozšíření služeb , „hackování“ a ohýbaní SW továrny pro danou problematiku mohou převážit nad výhodami SW továrny. Z velké radosti nad úsporou času v raných fázích projektu, kde byla továrna použita, můžete ještě před dodáním první verze projektu a po pořádném časovém skluzu, vynuceném třeba přepisem částí továrny, které pro výkonnostních testech nestačí stávajícím požadavkům na projekt, přejít k jadrným kletbám nad šílenou SW továrnou, v níž si úprava jednoho modulu kaskádově vynutí úpravy všech dalších modulů. Místo používání původně slibované elegantní černé skříňky s jednoduchými službami se prohrabujete nevábnými vyhřezlými vnitřnostmi mizerně navržené SW továrny.
Z výhod a nevýhod snad vyplývá můj postoj k SW továrnám. SW továrny jsou dalším evolučním stádiem na cestě, jejímž cílem je využít pro danou problematiku osvědčená řešení a postupy, nepsat stále se opakující kód nebo v každém řešení už jen změnou typu klienta (Web, Windows Forms, Silverlight, mobilní aplikace) neprocházet znovu a znovu všechny slepé vývojářské uličky specifické pro použitou technologii. Objevování objeveného ponechme těm, kterým stačí zařvat vítězoslavně heuréka, i když jsou v pořadí stí nebo tisící, kteří stejnou infrastrukturní nebo funkční trivialitu konečně zakódovali rozumným a v životním cyklu projektu dále udržovatelným způsobem.
A samozřejmě si nemyslím, že tohle evoluční stádium je konečné. :)
Sunday, 21 March 2010 11:57:10 (Central Europe Standard Time, UTC+01:00)
.NET Framework | Analytické drobky | Návrhové vzory | UML
Tuesday, 02 February 2010
Hrátky s Reaktivním frameworkem (RX extenze)
V předchozím článku jsem ukazoval, jak volat asynchronně metody z C# Posterous API v Silverlightu. C# Posterous API nabízí asynchronní zpracování pomocí jednoho z doporučovaného přístupu k asynchronním operacím v .Net Frameworku – metoda s konvenčním sufixem Async (LoadPostsAsync) spustí vykonání operace v jiném vlákně a výsledky operace jsou nabídnuty v argumentech události, která je (opět) jen dle jmenné konvence spojena s asynchronní operací (událost LoadPostsCompleted). C# Posterous API nenabízí ve svém rozhraní metody pro podporu dalšího a již od verze 1.0 .Net Frameworku přítomného asynchronního vzoru, který je spojen s dvojicí metod začínajících prefixem Begin a End. (BeginGetRequest, EndGetRequest, BeginRead, EndRead apod.)
Dále předpokládám, že oba přístupy k vytváření asynchronních opreací znáte a že jste si vědomi i toho, jak se způsob práce s asynchronními API odlišuje od práce s běžnými synchronnními metodami.
V již odkazovaném článku bylo dobře patrné, jak je řízení toku asynchronních operaci odlišné od sady volání běžných synchronních operací.
Pro připomenutí:
posterousAccount.SitesLoaded += (o, e) =>
{
throwIfAsyncEx(e.Exception);
posterousAccount.PrimarySite.PostsLoaded += (_, e2) =>
{
throwIfAsyncEx(e2.Exception);
Posts = (from p in e2.Value
select new ViewPost
{
Title = p.Title,
Body = p.Body,
Url = p.Url
}).ToList();
};
posterousAccount.PrimarySite.LoadAllPostsAsync();
};
posterousAccount.LoadSitesAsync();
Jediné, co tento kód dělá, je, že nejprve (!) nahraje všechny blogy (příkaz k asynchronnímu nahrání posterousAccount.LoadSitesAsync(); je na posledním (!) řádku. Na prvním (!) řádku máme zpracování výsledku volání metody LoadSitesAsync, ve kterém opět nejdříve (!) lambdou přihlášenou k odběru události PostsLoaded (posterousAccount.PrimarySite.PostsLoaded += (_, e2)) řekneme, jak zpracujeme výsledek následného (!) volání další asynchronní metody (posterousAccount.LoadSitesAsync());. Tato “inverzní“ práce s asynchronními metodami a zpracováním jejich výsledku je na hony a možná ještě dále vzdálena intuitivní práci se synchronními metodami.
Zkusme se nyní podívat, jak by nám s “převrácením starších asynchronních metod z hlavy zpět na synchronní nohy” mohl pomoci RX Framework. Úplné základy v tomto článku nezazní a začátečníky odkazuji na sérii přednášek na Channel 9, kde dozvíte i zajímavé podrobnosti o genezi celého RX Frameworku a matematické dualitě rozhraní IEnumerable a IObservable (jinými slovy o společných rysech dobře známých GoF návrhových vzorů Iterátor a Observer).
Současné příklady jsou vytvořeny v aplikaci Windows Forms pro .Net 3.5. Silverlight má své zvláštnosti a a rozchození příkladů v SL si zaslouží další článek, protože teď by řešení problémů specifických pro SL zamlžovalo cíl příkladu. Aplikace je pro .Net 3.5, protože stejná aplikace pro .Net 4.0 hlásí konflikt (ambiguous reference) mezi NF typy a RX typy.
Upozornění: Nic z toho, co napíšu neberte ani jako dogmata ani, nedej bože, jako best practices. RX Framework je v Betě, zdokumentován je mizerně a z jednoho řádku u každé metody se dá jen těžko bez dalších experimetů vytušit, co přesně metoda dělá. Tento článek je výsledkem hraní si pro účely jednoho projektu, kam se RX extenze hodí a zjednodušují (alespoň to tak prozatím vypadá ) dost rutinních činností.
Zde j výsledek našeho snažení, abychom měli motivaci se RX Frameworkem zabývat.
var resultPosts = from sites in account.GetSites()
from site in sites.ToObservable()
from posts in site.GetPosts()
from post in posts.ToObservable()
where post.Private == false
select post;
Získání blogů (Sites) i blogpostů (post) je stále asynchronní, ale výsledný kód vypadá jako běžný LINQ (To Enumerable) dotaz. Žádné inverzní volání a práce s výsledkem, jen prostý dotaz, jehož zvláštností je pouze to, že v některých místech voláme metodu ToObservable.
Jak jsem dosáhl tohoto výsledku?
Podíváme-li se na první řádek, vidíme, že voláme metodu account.GetSites. Metoda GetSites součástí C# Posterous API není a jedná se o extenzní metodu. Tato extenzní metoda je zvláštní tím, že její návratovou hodnotou je je jedno z klíčových rozhraní v RX Frameworku – rozhraní IObservable<T>.
public static IObservable<IEnumerable<IPosterousSite>> GetSites(this IPosterousAccount account)
Rozhrani IObservable má v RX Frameworku podobný význam jako rozhraní IEnumerable v celém .Net Frameworku. Zjednodušeně můžeme rozhraní IObservable popsat jako ceduli, kterou třída implementující rozhraní dává celému světu najevo: “Miluju voyery, jestliže chcete sledovat, co se ve mně děje, dejte mi sem pozorovatele a já na sebe všechno podstatné, co se od této chvíle stane, postupně vyzvoním ”.
Rozhraní IObservable je tedy příslib, že zainteresovaný pozorovatel dostane data, která třída podporující toto rozhraní nabízí. Svůj zájem pozorovatel deklaruje tak, že předá odkaz sám na sebe do metody Subscribe.
public interface IObservable<T>
{
IDisposable Subscribe(IObserver<T> observer);
}
Pozorovatel (IObserver) reaguje (proto reaktivní framework) na informace, které jsou mu poskytnuty objektem podporujícím rozhraní IObservable.
public interface IObserver<T>
{
void OnCompleted();
void OnNext(T value);
void OnError(Exception exn);
}
Metoda OnNext je na IObserver volána vždy, když Observable objekt má k dispozici další data. Metodou OnError Observable objekt signalizuje chyby a metodou OnCompleted Observeru říká “jsem u konce, nic dalšího už pro tebe nemám”.
Naše metoda GetSites tedy říká – zavolejte mě a já vám nabídnu IObservable objekt, který, až budou data k dispozici, vašemu observeru (IObserver) vydá kolekci (IEnumerable) objektů IPosterousSite.
Extenzní metoda GetSites vypadá takto:
public static IObservable<IEnumerable<IPosterousSite>> GetSites(this IPosterousAccount account)
{
checkAccountNotNull(account);
var sitesEvents = Observable.FromEvent<EventArgsValue<IEnumerable<IPosterousSite>>>(handler => account.SitesLoaded += handler,
handler => account.SitesLoaded -= handler)
.Take(GlobalConstants.DEFAULT_TAKE_EVENTS_COUNT);
return sitesEvents.GetFinalObservableEvents(account.LoadSitesAsync);
}
Po kontrole, zda předaný IPosterousAccount není null, využijeme pomocnou metodu Observable.FromEvent z RX Frameworku, která nám vrátí IObservable objekt. Tento IObservable objekt notifikuje případného observera o každé nastalé události sites.Loaded. V našem případě Observera notifikuje o právě jedné události, protože jsme použili metodu Take (Take(GlobalConstants.DEFAULT_TAKE_EVENTS_COUNT)) a konstanta DEFAULT_TAKE_EVENTS_COUNT má hodnotu 1. Jak si můžete všimnout, metoda FromEvent nám dovoluje s událostmi, které postupně nastávají, zacházet jako (s potenciálně nekonečnou) kolekcí hodnot. Metodě FromEvent jsme pouze museli říct, jaká třída nese argumenty událost (EventArgsValue<IEnumerable<IPosterousSite>) a poskytli jsme ji dva delegáty pro registraci/deregistraci obslužných handlerů, které nám RX Framework předá (handler => account.SitesLoaded += handler, handler => account.SitesLoaded –= handler). U našeho volání metody Take bych ještě poznamenal, že po vyvolání první události dojde automaticky RX Frameworkem k deregistraci obslužného handleru.
Proměnná sitesEvents je IObserver tohoto typu.
IObservable<IEvent<EventArgsValue<IEnumerable<IPosterousSite>>>>
Argumenty události jsou vždy zabaleny do instance třidy IEvent, která je vydána zaregistrovanému observeru v jeho metodě OnNext. Všimněte si ale, že návratovou hodnotou metody GetSites je již Observable, který observeru předá hodnoty bez IEvent (IObservable<IEnumerable<IPosterousSite>>).
Vidíme, že na sitesEvents je volána další má extenzní metoda GetFinalObservableEvents, které je předán delegát Action ukazující na asynchronní metodu account.LoadSitesAsync, a výsledek volání GetFinalObservableEvents je vrácen klientovi.
Metoda GetFinalObservableEvents:
public static IObservable<TEventData> GetFinalObservableEvents<TEventData>(this IObservable<IEvent<EventArgsValue<TEventData>>> sourceEvents, Action runAction)
{
if (sourceEvents == null)
{
throw new ArgumentNullException("sourceEvents");
}
var retObservable = new DelegateObservable<TEventData>(
observer =>
{
var eventObserver = new EventObserver<TEventData>(observer);
var unsubScribe = sourceEvents.Subscribe(eventObserver);
runAction();
return unsubScribe;
});
return retObservable;
}
Metoda GetFnalObservableEvents vrací opět Observable, ale tentokrát jde o Observable typu IObservable<TEventData> - jinými slovy, v našem případě IObservable<IEnumerable<IPosterousSite>>. Jak je toho dosaženo? Zdrojový IObservable objekt nazvaný sourceEvents je předán instanci třídy DelegateObservable, což je v současném scénaři již ten hledaný Observable podporující rozhraní IObservable<IEnumerable<IPosterousSite>>. DelegateObservable je tedy adaptér, který převádí události zabalené do IEvent na “rozbalené” hodnoty očekávané observerem. DelegateObservable je můj pomocný IObservable, který dostává do konstruktoru lambdu představující tělo jeho metody Subscribe, abychom nemuseli reimplementovat rozhraní IObservable v různých třídách stále dokola.
Výpis třídy DelegateObservable
public class DelegateObservable<T> : IObservable<T>
{
private readonly Func<IObserver<T>, IDisposable> m_subscribeDel;
public DelegateObservable(Func<IObserver<T>, IDisposable> subscribeDelegate)
{
m_subscribeDel = subscribeDelegate;
if (m_subscribeDel == null)
{
throw new ArgumentNullException("subscribeDelegate");
}
}
#region Implementation of IObservable<out T>
public IDisposable Subscribe(IObserver<T> observer)
{
if (observer == null)
{
throw new ArgumentNullException("observer");
}
return m_subscribeDel(observer);
}
#endregion
}
Předaná lambda v našem případě vytvoří instanci třídy eventsObserver, což je observer, který bude zpracovávat přicházející události, a do konstruktoru mu podhodí observer předaný klientským kódem – eventsObserver je tedy další adaptér, který je zdopovědný za “rozbalení” dat z instance IEvent a za předání těchto dat klientskému (“konečnému”) observeru.
Třída EventObserver:
public class EventObserver<T> : IObserver<IEvent<EventArgsValue<T>>>
{
private readonly IObserver<T> m_innerObserver;
private bool m_exceptionOccured;
public EventObserver(IObserver<T> innerObserver)
{
if (innerObserver == null)
{
throw new ArgumentNullException("innerObserver");
}
m_innerObserver = innerObserver;
m_exceptionOccured = false;
}
#region Implementation of IObserver<T>
public virtual void OnNext(IEvent<EventArgsValue<T>> eventData)
{
if (eventData.EventArgs.Exception != null)
{
OnError(eventData.EventArgs.Exception);
return;
}
//Rozbalení a předání dat Observeru
m_innerObserver.OnNext(eventData.EventArgs.Value);
}
public virtual void OnError(Exception exception)
{
m_innerObserver.OnError(exception);
m_exceptionOccured = true;
}
public virtual void OnCompleted()
{
//Chyba ukončí sekvenci sama o sobě
if (!m_exceptionOccured)
{
m_innerObserver.OnCompleted();
}
}
Třída EventObserver implementuje rozhraní IObservable s těmito generickými argumenty - IObserver<IEvent<EventArgsValue<T>>> . V C# Posterous API všechny události předávají svá data v instanci třídy EventArgsValue<T>, což znamená, že pro naše účely je EventObserver univerzálně použitelný observer pro zpracování výsledků asynchronní operace.
Pro úplnost zde je výpis třídy EventArgsValue
public class EventArgsValue<T> : EventArgs
{
private readonly T m_value;
private readonly Exception m_exception;
internal EventArgsValue(T value, Exception exception)
{
m_value = value;
m_exception = exception;
}
public T Value
{
get
{
return m_value;
}
}
public Exception Exception
{
get
{
return m_exception;
}
}
}
V lambdě předané do objektu DelegateObservable také musíme spustit asynchronní operaci – to je volání Action delegáta runAction, který nám předala již metoda GetSites. Každý IObservable také z metody vrací objekt implementující rozhraní IDisposable – volání metody Dispose dovoluje klientovi odpojit se objektu IObservable. Lambda předaná do instance DelegateObservable vrátí IDisposable objekt, který je vydán po připojení EventObservera k “streamu události“ (sourceEvents).
Přidání extenzních metod k dalším třídám je triviální – zde je extenzní metoda pro IPosterousSite, která nahraje všechny blogposty.
public static IObservable<IEnumerable<IPosterousPost>> GetPosts(this IPosterousSite site)
{
throwIfSiteNull(site);
var postsEvents = Observable.FromEvent<EventArgsValue<IEnumerable<IPosterousPost>>>(handler => site.PostsLoaded += handler,
handler => site.PostsLoaded -= handler)
.Take(GlobalConstants.DEFAULT_TAKE_EVENTS_COUNT);
return postsEvents.GetFinalObservableEvents(site.LoadAllPostsAsync);
}
A nyní se můžeme znovu podívat, jak naše API použijeme v klientském kódu – díky alternativní implementaci Query vzoru v RX frameworku můžeme používat staré dobré známé LINQ dotazy.
Metoda loadPosts:
private void loadPosts()
{
toolStripStatusLabel1.Text = TEXT_LOAD_DATA_START;
IPosterousApplication app = PosterousApplication.Current;
IPosterousAccount account = app.GetPosterousAccount("<Posterous user name>", "Posterous password");
var syncContext = SynchronizationContext.Current;
var resultPosts = from sites in account.GetSites()
from site in sites.ToObservable()
from posts in site.GetPosts()
from post in posts.ToObservable()
where post.Private == false
select post;
resultPosts.Subscribe(post =>
{
lock (m_threadsSet)
{
m_threadsSet.Add(Thread.CurrentThread.ManagedThreadId);
}
syncContext.Post(_ =>
{
var UCpost = new UC_Post
{
Title = post.Title,
Body = post.Body
};
flowLayoutPanel1.Controls.Add(UCpost);
},
null
);
},
ex => syncContext.Post(_ =>
{
throw new ApplicationException(ASYNC_EXCEPTION_TEXT, ex);
},
null),
() => syncContext.Post(_ =>
{
lock (m_threadsSet)
{
m_threadsSet.Run(tId => lstThreads.Items.Add(tId));
}
toolStripStatusLabel1.Text = TEXT_LOAD_DATA_END;
},
null
));
}
V proměnné resultPosts jsou uloženy všechny blogposty (IPosterousPost) ze všech blogů (IPosterousSite). Blogy i blogposty jsou nahrány asynchronně, ale v klientském kódu nevidíme žádná specialitky kvůli asynchronnímu nahrávání dat. Na proměnných sites i posts v dotazu je volána další extenzní metoda z RX Frameworku ToObservable, protože jak víme, výsledkem volání asynchronních metod byly typy IEnumerable<IPosterousPost> a IEnumerable<IPosterousSite>.
Důležité je, že zpracování dotazu je opět “lazy” – to znamená, že k získání dat dojde až poté, co k resultsPosts zaregistruju svého Observera metodou Subscribe (analogie “Lazy” vyhodnocování v LINQ To IEnumerable a procházení dotazu v cyklu foreach). Metoda Subscribe má několik variant a jedna z nich nám dovoluje pro metody OnNext, OnError a OnCompleted předat delegáty, aniž bychom byli nuceni vytvářet svou třídu implementující rozhraní IObserver.
První delegát (OnNext) vezme předaný post a vytvoří pro něj UserControl, který vloží do FlowPanelu na formuláři. Ještě předtím pro zajímavost do Hashsetu ukládám identifikátory vláken, které se na zpracování dotazu podílí. S prvky na formuláři můžeme pracovat jen z UI threadu, a proto je vložení User controlu provedeno přes SynchronizationContext uložený do proměnné syncContext před spuštěním dotazu.
Druhý delegát (OnError) pouze přes SynchronizationContext zpropaguje výjimku, která nastala při asynchronním zpracování, do UI threadu. Všimněte si, jak je zpracování výjimek jednoduché – rozdíl vynikne při srovnání s opakovaným voláním metody throwIfAsyncEx v kódu na začátku tohoto článku.
Třetí delegát (OnCompleted) naplní listbox na formuláři ID použitých threadů a změní text ve status baru.
Zde je výsledný formulář. V listboxu nahoře si můžete všimnout, že u mě byly k vykonání dotazu použity celkem 3 thready.
Tím bychom mohli skončit, ale RX Framework má pro asynchronní operace ještě další zajímavou podporu. Pomocí metody Observable.FromAsyncPattern můžeme vytvořit IObservable rychle a bezpracně ze standardního a výše již zmíněného asynchronního Begin/End vzoru. V C# Posterous API metody Begin*/End* nejsou, proto je zkusme dodat pomocí extenzních metod.
Rozhraní IPosterousAccount bude obohaceno o extenzní metody BeginLoadSites a EndLoadSite.
Metoda BeginLoadSites
public static IAsyncResult BeginLoadSites(this IPosterousAccount account, AsyncCallback callback, object context)
{
checkAccountNotNull(account);
var loadSiteAction = new Action(account.LoadSites);
return RXEventsHelper.GetAsyncResultEx(loadSiteAction, callback, context);
}
Jak vidíte, přesně dle konvencí .Net vzoru metoda vrací odkaz na rozhraní IAsyncResult a přijímá callBack, což je tedy u tohoto vzoru metoda, která má být vyvolána po dokončení asynchronního zpracování, a jak také vzor vyžaduje, posledním argumentem je libovolný objekt reprezentující libovolný “stavový token” operace, který v metodě End* klient používá pro korelací mezi požadavkem a odpovědí.
Veškerá práce je přenesena na metodu GetAsyncResultEx v mém RXEventsHelperu – metoda vyžaduje, abyste ji poslali v delegátu Action metodu, která má být spuštěna asynchronně.
Metoda RXEventsHelper.GetAsyncResultEx.
public static IAsyncResult GetAsyncResultEx(Action runAction, AsyncCallback callback, object context)
{
if (runAction == null)
{
throw new ArgumentNullException("runAction");
}
Exception ex = null;
var proxyCallback = new AsyncCallback(ar =>
{
IAsyncResult proxyResult = new AsyncResultEx(ar, runAction);
callback(proxyResult);
});
return runAction.BeginInvoke(proxyCallback, context);
}
Hlavním trikem je využití možností delegátů – každý delegát v .Net Frameworku vždy obsahuje asynchronní metody BeginInvoke a EndInvoke, které splňují nároky asynchronního vzoru. My tedy na předaném delegátu runAction zavoláme metodu BeginInvoke, ale místo klientské callBack Funkce podhodíme svou proxy funkci (proxyCallback), která po dokončení asynchronního volání připraví pro naši End metodu vlastní IAsyncResult (AsyncResultEx).
Třída AsyncResultEx zapouzdřuje původní IAsyncResult (argument ar předaný do konstruktoru v předešlém výpisu) a navíc, když na její instanci zavoláme metodu EndAction, na předaném delegátovi (argument runAction v předešlém výpisu) je zavolána metoda EndInvoke, čehož využije naše metoda EndLoadSites.
Třída AsyncResultEx
public class AsyncResultEx : IAsyncResult
{
#region private variables
private IAsyncResult m_originalAsyncResult;
private readonly Action m_originaldelegate;
#endregion private variables
public AsyncResultEx(IAsyncResult origAsyncResult, Action originaldelegate)
{
if (origAsyncResult == null)
{
throw new ArgumentNullException("origAsyncResult");
}
m_originalAsyncResult = origAsyncResult;
m_originaldelegate = originaldelegate;
}
#region properties
public virtual IAsyncResult OriginalAsyncResult
{
get
{
return m_originalAsyncResult;
}
}
public virtual Action OriginalDelegate
{
get
{
return m_originaldelegate;
}
}
#endregion properties
#region methods
public virtual void EndAction()
{
if (OriginalDelegate != null)
{
OriginalDelegate.EndInvoke(OriginalAsyncResult);
}
}
#endregion methods
#region Implementation of IAsyncResult
public virtual bool IsCompleted
{
get
{
return m_originalAsyncResult.IsCompleted;
}
}
public virtual object AsyncState
{
get
{
return m_originalAsyncResult.AsyncState;
}
}
public virtual WaitHandle AsyncWaitHandle
{
get
{
return m_originalAsyncResult.AsyncWaitHandle;
}
}
public virtual bool CompletedSynchronously
{
get
{
return m_originalAsyncResult.CompletedSynchronously;
}
}
#endregion
}
Extenzní metoda EndLoadSites
public static IEnumerable<IPosterousSite> EndLoadSites(this IPosterousAccount account, IAsyncResult result)
{
checkAccountNotNull(account);
var exResult = result as AsyncResultEx;
if (exResult == null)
{
throw new ArgumentException("result");
}
exResult.EndAction();
return account.Sites;
}
Zde vidíme volání metody EndAction na podhozené instanci AsyncResultE. Poté metoda EndLoadSites jen vrátí kolekci Sites objektu account, protože ta nyní již musí být po asynchronním volání naplněna daty.
Se stávající infrastrukturou si opět si můžeme rychle připravit další Begin a End metody. Zde jsou extenzní metody BeginLoadPosts a EndLoadPosts pro IPosterousSite.
public static IAsyncResult BeginLoadPosts(this IPosterousSite site, AsyncCallback callback, object context)
{
throwIfSiteNull(site);
var loadPostsAction = new Action(site.LoadAllPosts);
return RXEventsHelper.GetAsyncResultEx(loadPostsAction, callback, context);
}
public static IEnumerable<IPosterousPost> EndLoadPosts(this IPosterousSite site, IAsyncResult result)
{
throwIfSiteNull(site);
var exResult = result as AsyncResultEx;
if (exResult == null)
{
throw new ArgumentException("result");
}
exResult.EndAction();
return site.Posts;
}
A metoda loadPosts2, která dělá to samé, co předchozí metoda loadPosts, ale používá naše nové extenzní Begin/End metody.
private void loadPosts2()
{
toolStripStatusLabel1.Text = TEXT_LOAD_DATA_START;
IPosterousApplication app = PosterousApplication.Current;
IPosterousAccount account = app.GetPosterousAccount("posterousname", "posterouspassword");
var syncContext = SynchronizationContext.Current;
var resultPosts = from sites in Observable.Defer(() => Observable.FromAsyncPattern<IEnumerable<IPosterousSite>>(account.BeginLoadSites, account.EndLoadSites)())
from site in sites.ToObservable()
from posts in Observable.Defer(() => Observable.FromAsyncPattern<IEnumerable<IPosterousPost>>(site.BeginLoadPosts, site.EndLoadPosts)())
from post in posts.ToObservable()
where post.Private == false
select post;
resultPosts.Subscribe(post =>
{
lock (m_threadsSet)
{
m_threadsSet.Add(Thread.CurrentThread.ManagedThreadId);
}
syncContext.Post(_ =>
{
var UCpost = new UC_Post
{
Title = post.Title,
Body = post.Body
};
flowLayoutPanel1.Controls.Add(UCpost);
},
null
);
},
ex => syncContext.Post(_ =>
{
throw new ApplicationException(ASYNC_EXCEPTION_TEXT, ex);
},
null),
() => syncContext.Post(_ =>
{
lock (m_threadsSet)
{
m_threadsSet.Run(tId => lstThreads.Items.Add(tId));
}
toolStripStatusLabel1.Text = TEXT_LOAD_DATA_END;
},
null
)
);
}
Upozornil bych jen na dvě specialitky či zrádná místa, která (alespoň v této BETA verzi RX) dělají kód méně intuitvním, než by bylo žádoucí:
Jedná se o tyto dva řádky:
from sites in Observable.Defer(() => Observable.FromAsyncPattern<IEnumerable<IPosterousSite>>(account.BeginLoadSites, account.EndLoadSites)())
from posts in Observable.Defer(() => Observable.FromAsyncPattern<IEnumerable<IPosterousPost>>(site.BeginLoadPosts, site.EndLoadPosts)())
Metoda FromAsyncPattern přijímá delegáty na naše asynchronní metody, ale místo spolehnutí se na typovou inference jsem musel generický argument předat explicitně(Observable.FromAsyncPattern<IEnumerable<IPosterousSite>>) – pokud argument nezadáte, kompilátor hlásí “ambigous reference”.
Dále je patrné, že výsledek funkce FromAsyncPattern, kterým je další funkce vracející IObservable, je předán jako argument metodě Observable.Defer. Metoda Observable.Defer zajistí, že k vyhodnocení předané funkce dojde až poté, co je k výsledkům dotazu přihlášen observer – jinými slovy, metoda Defer nám pomáhá zachovat “lazy” vyhodnocení dotazu.
Dotaz bude fungovat i v této podobě (bez Defer):
var resultPosts = from sites in Observable.FromAsyncPattern<IEnumerable<IPosterousSite>>(account.BeginLoadSites, account.EndLoadSites)()
from site in sites.ToObservable()
from posts in Observable.FromAsyncPattern<IEnumerable<IPosterousPost>>(site.BeginLoadPosts, site.EndLoadPosts)()
from post in posts.ToObservable()
where post.Private == false
select post;
Ale jeho vyhodnocení už není “lazy”. Všimněte si závorek na konci výrazu - FromAsyncPattern<IEnumerable<IPosterousSite>>(account.BeginLoadSites, account.EndLoadSites)() - výsledné IObservable získám okamžitým zavoláním funkce vrácené z metody FromAsyncPattern. Vadit vám to začne v okamžiku, kdy zkonstruujete dotaz, ihned se odpálí asynchronní volání, dojde k chybě a vy budete mít v aplikaci neošetřenou výjimku v threadu na pozadí, protože IObserver ještě není přihlášen (není možné zavolat druhého delegáta - ex => syncContext.Post(_ => { throw new ApplicationException(ASYNC_EXCEPTION_TEXT, ex); }, null), ).
Snad se vám tato exkurze líbila. Já ještě na RX Framework konečný názor nemám, ale něco neodbytného ve mně říká, že by mohlo jít o další LINQ, který otřese programátorským světem. Některé extenze pravděpodobně zahrnu do samostatného jmenného prostoru v C# Posterous API.
Tuesday, 02 February 2010 07:43:00 (Central Europe Standard Time, UTC+01:00)
.NET Framework | C# Posterous API | LINQ | Návrhové vzory | RX Extensions | Windows Forms
Sunday, 20 September 2009
Pozvánka na podzimní kurzy (OOP, UML, základní a pokročilé návrhové vzory)
Aktualizace 10. 11. 2009- I veřejný kurz Objektovými principy a návrhovými vzory řízený design a vývoj kvalitních aplikací 2 je zcela obsazen včetně náhradníků. Další kurzy se budou konat na jaře 2009. Jestliže máte předběžný zájem a chcete si rezervovat místo, pište prosím na adresu petra@renestein.net.
Aktualizace 15.10.2009 - veřejný kurz Objektovými principy a návrhovými vzory řízený design a vývoj kvalitních aplikací 1 je zcela obsazen včetně náhradníků. Je možné se již hlásit pouze na kurz Pokročilé návrhové vzory a objektové principy 2.
Rád bych Vás pozval na podzimní kurzy OOP a UML a představil oficiálně InHouse kurz, který postupně vykrystalizoval z požadavků zákazníků (OOP 0 - Objektové programování a UML prakticky - rychlý úvod do světa (nejen) objektového programování).
Osnova InHouse kurzu OOP 0 – Objektové programování a UML prakticky – rychlý úvod do světa (nejen) objektového programování:
Školení má dvě varianty - pro vývojáře i u konstrukcí a prvků jazyka UML, které jsou považovány za analytické, se dělají časté odbočky do kódu, aby vývojáři pochopili, že UML ani principy OOP nejsou nějaké nesmyslné abstrakce, ale užitečné konstrukce, které sami v programovacích jazycích používají denně.
U varianty pro „čisté“ analytiky jsou digrese do kódu minimalizovány, i když v některých místech stále zdůrazňuji, jaké znalosti z oblasti vývoje aplikací musí analytik mít, aby byl pro projekt užitečný a nevytvářel jen dokumentaci pro dokumentaci, kterou vývojáři nevyužijí a (mnohdy oprávněně) považují za nesmyslnou, drahou a projektu nic nepřinášející.
V kurzu se naučíte modelovat jednoduché i složité aplikace s využitím jazyka UML tak, aby následné kódování nebylo výletem do neznáma s nejistými výsledky, ale dobře čitelnou cestou bez temných a záludných míst vedoucích k selhání projektu.
Kurz je vhodný zvláště pro ty, kteří již nejsou spokojeni s vývojem projektů naivním "hurá" způsobem, kdy bez ohledu na složitost systému nevzniká žádný návrh a ihned se přistupuje ke kódování se všemi špatnými důsledky jako jsou podcenění technické a časové náročnosti implementace nebo vytváření drahých a nespravovatelných systémů.
Kurz je určen pro vývojáře, systémové designery, analytiky a projektové manažery, kteří se chtějí se seznámit se základními principy objektového programování a s modelováním v jazyce UML.
· Požadavky na systém a modelování pomocí případů užití (+ příklady).
· Zrychlená funkční specifikace bez zbytečných formalit – příklady.
· Diagram tříd v UML - vztahy mezi elementy diagramu (asociace. agregace, generalizace, závislost, realizace) – vše vykládáno na konkrétních příkladech z praxe + ukázky nejčastějších chyb, se kterými jsem se setkal. Třída, základní principy OOP, operace, atributy, viditelnost členů třídy. Nenásilný přechod k jednoduchým návrhovým vzorům.
· Příklady složitých diagramů tříd.
· Objektový diagram + příklady.
· Sekvenční diagramy a diagramy interakce.
· Vysvětlení stavových diagramů + výhody aplikací řízených přesně definovanými stavovými automaty.
· Diagram aktivit - modelování složitých business procesů v organizaci.
· Výhody a nevýhody UML - vyzdvižení nejvíce používaných postupů, odhození nepotřebné veteše z jazyka UML.
Pokud máte o kurz zájem nebo potřebujete další informace, napište prosím na adresu petra@renestein.net.
Veřejný kurz Objektovými principy a návrhovými vzory řízený design a vývoj kvalitních aplikací 1
Datum konání kurzu: 2. 11. – 4. 11. 2009
Místo konání: Hotel VILLA Praha Okrajní 1, 100 00, Praha 10
U hotelu VILLA je možné parkovat, po celý den máme k dispozici wifi připojení.
Na kurzu jsou samozřejmě po celý den teplé a studené nápoje a v ceně kurzu jsou obědy v hotelu.
Podrobné informace o kurzu a možnost přihlásit se na kurz
Program kurzu
Výběr z ohlasů na kurz
Veřejný kurz Pokročilé návrhové vzory a objektové principy 2
Datum konání kurzu: 23. 11. – 25. 11. 2009
Místo konání: Hotel VILLA Praha Okrajní 1, 100 00, Praha 10
U hotelu VILLA je možné parkovat, po celý den máme k dispozici wifi připojení.
Na kurzu jsou samozřejmě po celý den teplé a studené nápoje a v ceně kurzu jsou obědy v hotelu.
Podrobné informace o kurzu a možnost přihlásit se na kurz
Program kurzu
Výběr z ohlasů na kurzy
Sunday, 20 September 2009 17:25:11 (Central Europe Standard Time, UTC+01:00)
Analytické drobky | Kurzy UML a OOP | Návrhové vzory
Monday, 02 February 2009
Lehká imitace některých rysů windows forms aplikací v non-windows forms aplikacích
Omluvte prosím trochu kryptický název, ale lepší a hlavně výstižnější pojmenování článku mě nenapadlo. Název je stejně jen vábnička na čtenáře, proto se podívejme, co je jím míněno.
Již několikrát mně různí vývojáři tvrdili, jak nepříjemná je pro ně práce s konzolí (windows službou, dosaďte další typy aplikací dle libosti...), protože musí řešit, aby aplikace po svém spuštění ihned neskončila, a také je pro ně problematické zajistit, aby byly některé události zpracovány vždy ve stejném threadu.
Převedeme-li emocionální stížnost do věcného jazyka, zjistíme, že to, co v těchto typech aplikací chybí, jsou následující rysy běžné windows forms aplikace:
- Windows aplikace spustí smyčku Windows zpráv (message loop) a vývojář pouze obsluhuje události formuláře. V (Compact) .Net Frameworku nám stačí zavolat Application.Run(new Form1()) a aplikace neukončí svůj běh, dokud není uzavřen poslední formulář nebo dokud ti drsnější z nás nezavolají Application.Exit. O životní cyklus aplikace, její spuštění a ukončení, se většinou nemusíme nijak starat.
- Při obsluze formuláře máme po volání metody Invoke garantováno, že předaný delegát bude vykonán v takzvaném UI threadu. Hlavním účelem metody Invoke (a sesterských metod BeginInvoke a EndInvoke) je threadově bezpečná komunikace s ovládacími prvky. Ovládací prvky ve stylu windows prvků v konzolových aplikacích (windows službách) nenajdeme, ale přesto bychom i v těchto typech aplikací občas chtěli mít nástroj, který garantuje, že všechny nebo vybrané události budou zpracovány v jednom výkonném threadu.
V tomto článku se objeví návrh, který pro non-windows forms aplikace přinese výše zmíněné rysy a přidá pár věcí navíc.
Pár vysvětlujících poznámek na úvod . Kód (přesněji řečeno draft k dalšímu rozpracování), který za chvíli uvidíte, má běžet na .Net frameworku a na Compact .Net Frameworku. Vím, že existují synchronizační kontexty pro thready, ale metodu Invoke jsem zmiňoval proto, že představuje společný jmenovatel pro obě prostředí, protože Compact .Net Framework je stále tím strýčkem - beznadějným sociálním případem, co nám nikdy nepřiveze žádné úhledně zabalené dárky, v nichž se skrývá třeba nádherná vlastnost SynchronizationContext.Current. Se znalostí tohoto omezení je také jasné, proč jsem nepoužil i další metody/vlastnosti dostupné jen ve "velkém" .Net Frameworku.
Dále v kódu jsou třídy obsahující ve svém názvu slovo *Console*. Nenechte se zmást, že mluvím dále jen o konzolových aplikacích, stejné třídy lze použít ve windows službě a dalších typech aplikací.
Zaveďme si nejdříve abstraktní třídu ConsoleTask, která je předkem všech zpracovávaných úloh v aplikaci. Zjednodušeně si můžeme třídu ConsoleTask a její potomky představit jako výchozí stavební prvky zapouzdřující chování analogické k vybraným a pro nás zajímavým rysům windows formulářů.
/// <summary>
/// Základní rozhraní pro položky zpracovávané v jedné frontě
/// </summary>
internal interface IExecuteWorkItem
{
/// <summary>
/// Implementace metody spustí úlohu
/// </summary>
void Execute();
}
/// <summary>
/// Bázová třída pro všechny úlohy
/// </summary>
abstract class ConsoleTask : IDisposable
{
#region Inner classes
/// <summary>
/// Výchozí implementace rozhraní <see cref="IExecuteWorkItem"/>
/// </summary>
private class WorkThreadItem : IExecuteWorkItem
{
#region Private variables
private Delegate m_del;
private object[] m_vals;
#endregion Private variables
/// <summary>
/// Konstruktor
/// </summary>
/// <param name="del">Delegát, který má být spuštěn ve frontě nadřazeného objektu <see cref="ConsoleTask"/></param>
/// <param name="vals">Argumenty delegáta</param>
public WorkThreadItem(Delegate del, params object[] vals)
{
if (del == null)
{
throw new ArgumentNullException("del");
}
m_del = del;
m_vals = vals;
}
/// <summary>
/// Metoda iniciuje vykonání předaného delegáta
/// </summary>
public virtual void Execute()
{
m_del.Method.Invoke(m_del.Target, m_vals);
}
}
#endregion Inner classes
#region private variables
private ManualResetEvent m_event;
private Thread m_innerWorkingThread;
private Queue<IExecuteWorkItem> m_workQueue;
private AutoResetEvent m_workingThreadEvent;
private object m_lockQueueRoot;
private bool m_continue;
private bool m_disposed;
#endregion private variables
#region constructors
/// <summary>
/// Konstruktor
/// </summary>
protected ConsoleTask()
{
m_lockQueueRoot = new object();
m_workQueue = new Queue<IExecuteWorkItem>();
m_event = new ManualResetEvent(false);
m_workingThreadEvent = new AutoResetEvent(false);
m_innerWorkingThread = new Thread(processWorkerThread);
m_continue = true;
m_disposed = false;
}
#endregion constructors
#region Properties
/// <summary>
///<see cref="WaitHandle"/> běžící úlohy
/// </summary>
public WaitHandle TaskWaitHandle
{
get
{
if (m_disposed)
{
throw new ObjectDisposedException("ConsoleTask");
}
return m_event;
}
}
/// <summary>
/// Metoda vrátí true, jestliže volající thread je odlišný od threadu, který vyřizuje položky zpracovávané v jedné frontě
/// </summary>
public virtual bool InvokeRequired
{
get
{
if (m_disposed)
{
throw new ObjectDisposedException("ConsoleTask");
}
if (SlaveWorkingTask != null)
{
return SlaveWorkingTask.InvokeRequired;
}
return (Thread.CurrentThread.ManagedThreadId != m_innerWorkingThread.ManagedThreadId);
}
}
/// <summary>
/// Volitelná instance <see cref="ConsoleTask"/>, která převezme odpovědnost za vyřizování položek zpracovávaných v jedné frontě
/// </summary>
public ConsoleTask SlaveWorkingTask
{
get;
set;
}
#endregion Properties
#region Methods
/// <summary>
/// Metoda garantuje, že dojde k vykonání předaného delegáta v threadu, ktrerý vyřizuje položky zpracovávané v jedné frontě
/// </summary>
/// <remarks>Metoda pouze zařadí položky ke zpracování a nečeká na výsledek volání delegáta. </remarks>
public virtual void Invoke(Delegate del, params object[] vals)
{
if (m_disposed)
{
throw new ObjectDisposedException("ConsoleTask");
}
if (SlaveWorkingTask != null)
{
SlaveWorkingTask.Invoke(del, vals);
return;
}
lock (m_lockQueueRoot)
{
m_workQueue.Enqueue(new WorkThreadItem(del, vals));
m_workingThreadEvent.Set();
}
}
/// <summary>
/// Metoda spustí úlohu
/// </summary>
/// <remarks>Spuštěním úlohy se rozumí spuštění kódu v přepsané metodě <see cref="DoInternalRun"/> v samostatném threadu. Metoda Run nevrátí řízení, dokud není úloha dokončena.</remarks>
public void Run()
{
if (m_disposed)
{
throw new ObjectDisposedException("ConsoleTask");
}
m_innerWorkingThread.Start();
ThreadPool.QueueUserWorkItem(obj => DoInternalRun());
m_event.WaitOne();
}
/// <summary>
/// Metoda ukončí úlohu
/// </summary>
public virtual void CloseTask()
{
m_continue = false;
m_workingThreadEvent.Set();
m_event.Set();
}
/// <summary>
/// Metoda pro explicitní uvolnění veškerých nepoužívaných zdrojů - součást implementace "Disposable" vzoru
/// </summary>
public void Dispose()
{
if (m_disposed)
{
return;
}
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// "Destruktor" - součást implementace "Disposable" vzoru
/// </summary>
~ConsoleTask()
{
Dispose(false);
}
/// <summary>
/// Interní implementace "Disposable" vzoru
/// </summary>
/// <param name="disposing">true - jestliže je metoda volána z metody Dispose, false, pokud je volána z destruktoru - metody Finalize</param>
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
try
{
((IDisposable)m_workingThreadEvent).Dispose();
((IDisposable)m_event).Dispose();
m_disposed = true;
}
catch (Exception e)
{
Trace.WriteLine(e);
}
}
}
/// <summary>
/// Metoda, která musí být přepsána v odvozených třídách a která obsahuje logiku specifickou pro každou úlohu
/// </summary>
protected abstract void DoInternalRun();
/// <summary>
/// Obsluha fronty položek, které mají být zpracovány ve stejném threadu
/// </summary>
private void processWorkerThread()
{
const int EXPECTED_MINIMUM_ITEMS = 1;
while (m_continue)
{
m_workingThreadEvent.WaitOne();
if (!m_continue)
{
continue;
}
int m_count = EXPECTED_MINIMUM_ITEMS;
IExecuteWorkItem nextItem = null;
while (m_count > 0)
{
lock (m_lockQueueRoot)
{
m_count = m_workQueue.Count();
if (m_count != 0)
{
nextItem = m_workQueue.Dequeue();
}
}
try
{
if (nextItem != null)
{
nextItem.Execute();
}
}
catch (Exception ex)
{
Trace.WriteLine(ex);
}
m_count--;
}
}
}
#endregion Methods
}
Abstraktní třída ConsoleTask obsahuje ve svém veřejném rozhraní metodu Run, kterou spustíme úlohu. Metoda Run je šablonovou metodou (Template method), protože obsahuje závazný scénář pro veškeré odvozené úlohy. Potomci třídy ConsoleTask do scénáře vstupují na přesně vymezeném místě - v metodě DoInternalRun, která je deklarována jako abstraktní a všechny konkrétní odvozené třídy ji musí přepsat a doplnit vlastní logiku. Třída ConsoleTask tedy garantuje, že je vždy nejprve spuštěn thread vyřizující požadavky, které mají být vykonány ve stejném threadu (podrobný popis viz níže), poté je třída ThreadPool použita ke spuštění kódu v metodě DoInternalRun v jiném threadu a nakonec aktuální thread pozastavíme čekáním na signalizaci instance synchronizačního objektu ManualResetEvent (proměnná m_event). Ve vlastnosti TaskWaitHandle vydáváme stejný objekt ManualResetEvent, který může jiný thread využít k synchronizaci svého běhu s instancí třídy odvozené od třídy ConsoleTask. Tím simulujeme pro uživatele objektů odvozených z třídy ConsoleTask spuštění smyčky zpráv, protože aplikace není ukončena po zavolání metody Run. Za ukončení běhu úlohy zodpovídá metoda CloseTask - metoda uvolní pracovní thread vyřizující frontu požadavků nastavením proměnné m_continue na false a signalizací synchronizační primitivy workingThreadEvent. Dále metoda CloseTask přes signalizaci synchronizačního objektu v proměnné m_event informuje o dokončení celé úlohy - thread pozastavený v metodě Run bude uvolněn.
Třída ConsoleTask dále obsahuje definici privátní třídy WorkThreadItem, která implementuje rozhraní IExecuteWorkItem a má roli adaptéru. Instance třídy WorkThreadItem jsou jednotlivé položky, které mají být vykonány v jednom pracovním threadu. Adaptérem je třída WorkThreadItem proto, že převádí rozhraní jakéhokoli předaného delegáta na rozhraní IExecuteWorkItem. Po volání metody Execute objektu WorkThreadItem je vykonána metoda, na kterou ukazuje delegát.
Jméno vlastnosti InvokeRequired by mělo znít povědomě - metoda vrátí true, jestliže thread, který zjišťuje hodnotu vlastnosti , je odlišný od threadu, který zpracovává položky typu IExecuteWorkItem. Thread poté může použít metodu Invoke, která zajistí, že předaný delegát bude vykonán v pracovním threadu. Je to zmíněno i v dokumentaci metody Invoke, ale zde zdůrazním, že metoda Invoke zařadí pouze novou položku do fronty ke zpracování a dá signál pracovnímu threadu, že je dostupná další položka voláním metody Set na proměnné m_workingThreadEvent, což je instance synchronizační primitivy AutoResetEvent. Metoda Invoke nečeká na výsledek volání delegáta a ani není zaručeno, že po návratu z metody Invoke byl již předaný delegát vykonán. Samotná obsluha fronty položek, které mají být vykonány v jednom threadu, je soustředěna do metody processWorkerThread.
U metody Invoke a vlastnosti InvokeRequired si můžete všimnout podmíněné delegace volání na instanci ve vlastnosti SlaveWorkingTask. Jestliže vlastnost SlaveWorkingTask není null, je odpovědnost za zpracování položek přenesena na jinou instanci třídy ConsoleTask. Jednotlivé tasky mohou tvořit zárodečný řetězec odpovědnosti (Chain of responsibility) a za chvíli uvidíme, k čemu můžeme toto předávání odpovědnosti na jiné instance ConsoleTask využít.
Třída ConsoleTask také implementuje běžný .Net "Disposable" vzor pro uvolňování prostředků (rozhraní IDisposable, chráněná metoda Dispose a destruktor - metoda Finalize).
Mimikry konzolové aplikace, která se v rámci námi vykolíkovaného seznamu požadavků snaží vydávat za windows forms aplikaci, vylepšíme zavedením jednoduché fasády (vzor facade), která bude simulovat metodu Application.Run.
/// <summary>
/// Facade s rozhraním pro spuštění úkolu
/// </summary>
class ConsoleApplication
{
/// <summary>
/// Metoda spustí předaný úkol (Fasáda ke spuštění úloh napodobující známou metodu Application.Run z Windows Forms aplikací)
/// </summary>
/// <param name="task">Úkol, který má být spuštěn</param>
public static void Run(ConsoleTask task)
{
if (task == null)
{
throw new ArgumentNullException("task");
}
task.Run();
}
}
Jak vidno, metoda Run zcela deleguje vykonání na předaný ConsoleTask.
Jak se prozatím s naším modelem pracuje? Nejlepší bude napsat si potomka třídy ConsoleTask a zjistit to. Zkusme vytvořit úlohu, která na Compact .Net Frameworku zpracuje příchozí SMS.
/// <summary>
/// Třída pro zpracování přijatých SMS
/// </summary>
class SMSTask : ConsoleTask
{
#region private variables
private MessageInterceptor m_interceptor;
#endregion private variables
#region Methods
/// <summary>
/// Metoda začne sledovat SMS
/// </summary>
protected override void DoInternalRun()
{
m_interceptor = new MessageInterceptor(InterceptionAction.Notify, false);
ThreadPool.QueueUserWorkItem(
(obj) =>
m_interceptor.MessageReceived += m_interceptor_MessageReceived);
}
/// <summary>
/// Obslužná metoda uálosti <see cref="MessageInterceptor.MessageReceived"/>
/// </summary>
/// <param name="sender">Odesílatel události</param>
/// <param name="e">Argument události</param>
private void m_interceptor_MessageReceived(object sender, MessageInterceptorEventArgs e)
{
if (InvokeRequired)
{
Invoke((Action<SmsMessage>)(handleMessage), e.Message as SmsMessage);
}
else
{
handleMessage(e.Message
as SmsMessage); }
}
public override void CloseTask()
{
m_interceptor.Dispose();
base.CloseTask();
}
/// <summary>
/// Zpracování SMS
/// </summary>
/// <param name="message">SMS zpráva ke zpracování</param>
private void handleMessage(SmsMessage message)
{
Console.WriteLine(message.Body);
CloseTask();
}
#endregion methods
}
Autor tříd odvozených z bázové třídy ConsoleTask má lehkou práci, protože se soustředí jen na úkol (příjem SMS) a ne na to, že jeho kód bude vykonán v konzolové aplikaci. V přepsané metodě si přihlásíme odběr události MessageReceived - zde je událost přihlášena přes ThreadPool, ale není to nutné. Obslužná metoda události MessageReceived (m_interceptor_MessageReceived) po příjmu SMS zaručí, že SMS budou vždy zpracovány ve stejném pracovním vlákně použitím vlastnosti Invoke Required a Invoke. Jestliže je událost vyvolána v jiném než pracovním threadu obsluhujícím frontu položek ke zpracování, zavoláme metodu Invoke, které předáme delegáta ukazujícího na metodu handleMessage. K vytvoření delegáta jsme použili standardního generického delegáta Action<T>, kde jsme za generický parametr T dosadili třídu SmsMessage, jejíž instanci přijímá jako argument metoda handleMessage. Přepsali jsme i metodu CloseTask, která uvolní interceptora pro příjem zpráv a poté vyvolá implementaci metody CloseTask z bázové třídy. Zde je úloha ukončena po příjmu první zprávy voláním CloseTask z metody handleMessage, ale způsob ukončení úlohy je zcela v rukou vývojáře konkrétní úlohy.
Poznámka na okraj: U naší třídy SMSTask by bylo vhodné, když chceme přijmout jen jednu SMS, ihned si odhlásit odběr dalších zpráv, nebo si v interní proměnné nastavit, že již zpráva byla přijata a další zprávy nepředávat ke zpracování.
Novou úlohu spustíme tímto nezáludným a pro vývojáře windows forms aplikací povědomým kódem:
class Program
{
static void Main(string[] args)
{
SMSTask smsTask = new SMSTask();
ConsoleApplication.Run(smsTask);
}
}
Na vývojářské práci je nejlepší, že poté, co máte nějaký nosný nápad, můžete jej rozvíjet ad libitum. Co když chceme ve stejné aplikaci nejen přijímat SMS, ale také reagovat na události v objektu, který nás informuje o spuštěných aplikacích uživatele. Nebo chceme sledovat přes třídu SystemState informace o příchozích hovorech? Napráskat vše do jedné instance potomka třídy ConsoleTask "ResimVzdyckyVsechnoNaJednomMisteAJsemTotalneVPohodeVoe" je sice řešením, ale i jen laxním zastáncům vágně formulovaného principu jedné odpovědnosti třídy (zdravím Aleši :) ) se právě teď nasucho aktivoval podmíněný reflex, protože vědí, že při správě takové aplikace po kolegovi-pohodářovi je vztekem podmíněné zoufalecké uslintávání a hlasité nadávání to nejmenší.
Chceme určitě zachovat stávající strukturu aplikace, chceme spouštět libovolné množství různorodých úloh a navíc chceme mít možnost zpracovávat položky napříč jednotlivými úlohami ve stejném threadu - pracovní frontě. Úkol jako stvořený pro jednu z možných nenásilných inkarnací návrhového vzoru Composite v aplikaci.
/// <summary>
/// Třída reprezentující kompozitní úlohu - viz návrhový vzor Composite
/// </summary>
class CompositeTask : ConsoleTask
{
#region private variables
private ICollection<ConsoleTask> m_tasks;
#endregion private variables
#region Constructors
public CompositeTask(ICollection<ConsoleTask> tasks)
{
if(tasks == null)
{
throw new ArgumentNullException("tasks");
}
if (tasks.Count == 0)
{
throw new ArgumentException("One or more tasks are required");
}
m_tasks = tasks;
}
#endregion Constructors
#region Methods
/// <summary>
/// Spuštění všech úkolů
/// </summary>
protected override void DoInternalRun()
{
foreach (var task in m_tasks)
{
ConsoleTask task1 = task;
task1.SlaveWorkingTask = this;
ThreadPool.QueueUserWorkItem((obj) => task1.Run());
}
}
/// <summary>
/// Metoda ukončí všechny úkoly
/// </summary>
/// <remarks>Metoda pouze zavolá metodu CloseTask na všech předaných objektech <see cref="ConsoleTask"/>, ale nestará se o výsledek volání</remarks>
public override void CloseTask()
{
foreach (var task in m_tasks)
{
task.CloseTask();
}
base.CloseTask();
}
#endregion Methods
}
Metoda CompositeTask je také potomkem třídy ConsoleTask, a proto můžeme ve zbytku aplikace pracovat se stejným rozhraním, na které jsme zvyklí. Jednoduchá i složená úloha mají stejné rozhraní, takže si klient tříd nemusí být skládání úloh vědom, což je mimochodem jedna z hlavních motivací pro zavedení návrhového vzoru Composite. V konstruktoru očekáváme odkaz na kolekci dceřiných úkolů. V metodě DoInternalRun zavoláme v cyklu metodu Run všech předaných úkolů. Ještě před voláním metody Run ale nastavíme u každé úlohy vlastnost SlaveWorkingTask na aktuální objekt CompositeTask, což nám zaručí, že veškeré položky ze všech jednotlivých úloh vložené do pracovní fronty voláním metody Invoke budou zpracovány v jediném pracovním vlákně CompositeTask. Zde vidíme jeden z důvodů, proč máme vlastnost SlaveWorkingTask a proč třída ConsoleTask ve členech Invoke a InvokeRequired nejprve kontroluje, jestli má zpracovat požadavek ve své pracovní frontě, anebo existuje jiný vhodný objekt - "otrok" (SlaveWorkingTask), který se o položky postará sám. Metoda CloseTask opět nejprve zavolá metodu CloseTask na všech objektech ConsoleTask, ze kterých je aktuální instance třídy CompositeTask složena.
Opět poznámka: Snad si rozumíme v tom, že navržená třída CompositeTask není jediná možná. Jiná třída CompositeTask2 nemusí přesměrovávat pracovní frontu na sebe, další po uzavření úloh nejprve vyčká na ukončení všech dceřiných úloh. Další scénáře jistě nalezne laskavý čtenář sám.
Než třídu CompositeTask vyzkoušíme, vytvoříme si dalšího potomka Consoletask, který bude zpracovávat pravidelně vyvolávanou událost z našeho objektu.
Zde je jednoduchá "demo" třída, jejíž srdce tiká v rytmu události AliveEvent.
class MyEventClass
{
public event EventHandler<EventArgs> AliveEvent;
private bool m_continue;
private const int INTERVAL = 1000;
private object m_lockObj;
public MyEventClass()
{
m_continue = true;
m_lockObj = new object();
}
public void Start()
{
ThreadPool.QueueUserWorkItem((state) =>
{
while (m_continue)
{
Thread.Sleep(INTERVAL);
AliveEvent(this, new EventArgs());
}
});
}
public void Stop()
{
lock (m_lockObj)
{
m_continue = false;
}
}
protected void OnAliveEevent(EventArgs e)
{
if (AliveEvent != null)
{
AliveEvent(this, e);
}
}
}
Naše nová konkrétní úloha zpracovává události instance MyEventClass
class ConcreteTask : ConsoleTask
{
private const int HEART_BEAT_LIMIT = 10;
private MyEventClass m_evClass;
private int m_heartBeatcount;
private bool m_processEvent;
protected override void DoInternalRun()
{
m_evClass = new MyEventClass();
m_heartBeatcount = 0;
m_processEvent = true;
m_evClass.Start();
m_evClass.AliveEvent += evClass_AliveEvent;
}
private void evClass_AliveEvent(object sender, EventArgs e)
{
Action myAction = (Action) (
() =>
{
if (!m_processEvent)
{
return;
}
Console.WriteLine("Event fired");
m_heartBeatcount++;
if (m_heartBeatcount >= HEART_BEAT_LIMIT)
{
m_evClass.Stop();
m_processEvent = false;
CloseTask();
}
}
);
if (InvokeRequired)
{
Invoke(myAction);
}
else
{
myAction();
}
}
}
Jenom pro zajímavost je ukázáno, že metodě Invoke můžeme předat složenou (statement) lambdu (nebo anonymní metodu).
Spuštění více úloh pomocí třídy CompositeTask není odlišné od spuštění jedné úlohy.
class Program
{
static void Main(string[] args)
{
var compTask = new CompositeTask(new List<ConsoleTask>
{
new SMSTask(),
new ConcreteTask()
});
ConsoleApplication.Run(compTask);
}
}
Znovu opakuji, že článek měl za cíl ukázat, jak transponovat do jiného aplikačního rámce postupy, které Windows Forms vývojáři dobře ovládají a o kterých mi tvrdili, že jsou "přirozené". Další rozpracování těchto draftů napsaných v půlnoční chvilce nespavosti je už jen variací předvedených postupů.
Monday, 02 February 2009 14:51:15 (Central Europe Standard Time, UTC+01:00)
.NET Framework | Compact .Net Framework | Návrhové vzory | Windows Forms
Tuesday, 11 November 2008
Pozvánka na kurzy - nový kurz Pokročilé návrhové vzory a objektové principy 2
Rád bych Vás pozval na další běh kurzu Objektovými principy a návrhovými vzory řízený design a vývoj kvalitních aplikací 1 a hlavně Vás chci seznámit se zcela novým kurzem Pokročilé návrhové vzory a objektové principy 2.
Kurz Pokročilé návrhové vzory a objektové principy 2 je volným pokračováním kurzu Objektovými principy a návrhovými vzory řízený design a vývoj kvalitních aplikací 1. Pojetím kurzu Pokročilé návrhové vzory a objektové principy 2 jsem se snažil vyhovět účastníkům předchozího kurzu, kteří mi, volně parafrázováno, říkali: "Nejlepší jsou ty části, kde probíráme jeden příklad za druhým a kde říkáte - takto to dělám já." Můj zákazník, můj pán (zvláště, jestliže se v záměrech zcela shodneme ) - nový kurz je prošpikován příklady, tipy, kódem, vzorovými aplikacemi. Budu se těšit na oponeturu mých postupů.
Datum konání kurzu: 9. 3. - 11. 3. 2009
Místo konání: Hotel VILLA Praha Okrajní 1, 100 00, Praha 10
U hotelu VILLA je možné parkovat, po celý den máme k dispozici wifi připojení.
Pro jistotu dodám, že na kurzu jsou samozřejmě po celý den teplé a studené nápoje a v ceně kurzu jsou obědy v hotelu.
Podrobné informace o kurzu a možnost přihlásit se na kurz
Výběr z ohlasů na předchozí kurz
Předpoklady pro absolvování kurzu:
- Znalost alespoň jednoho z rodiny "C" jazyků (C#, Java) - příklady na školení jsou v jazyce C#.
- Částečná znalost UML = neutečete zděšeni z kurzu, když zobrazím diagram tříd.
- Nenávist ke kariéře zručného klikače a zaškrtávače ve vizuálních návrhářích a "wizardech", co s velkou vášní vytváří jedno strhující uživatelské rozhraní pro číselníky za druhým.
- Vhodné, nikoliv však nutné, je i absolvovat nejdříve školení Objektovými principy a návrhovými vzory řízený design a vývoj kvalitních aplikací 1.
Program kurzu
- Layer Supertype pro další vrstvy aplikace – vrstva pro řízení procesů a business transakcí.
- Deklarativní změny v logice procesů v nasazené aplikaci prováděné samotným uživatelem.
- Evidence historie objektů - různé přístupy.
- Vlastní správce historie pro .Net Framework a Javu.
- Řešení konkurenčního přístupu k datům.
- Optimistická konkurence - různé implementace.
- Pesimistická konkurence - různé implementace.
- Pesimistická konkurence - různé implementace.
- Konkurence napříč objektovými modelem - "Coarse grained lock" - různé implementace.
- Thread Specific Storage – vlastní řešení.
- Modelovani uživatelem zadávaných výběrových podminek (např. uživatelem definované sestavy nad objednávkami) – můj „Conditions“ vzor.
- Návrh a implementace netriviálního právového frameworku.
- Různé způsoby vyhodnocování práv - změna logiky za běhu aplikace.
- Kde všude se nám hodí myšlenky návrhového vzor Accounting - modelování business aplikací jako množiny souvisejících transakcí.
- Návrhové vzory Query a Repository a jejich vazba na „Conditions“ vzor.
- Různé přístupy k vytváření uživatelského rozhraní - Model-View-Controller, Model-View-Presenter, Passive View - můj vlastní Form Controller.
- Aplikace založené na pluginech – vzorové přístupy a doporučení.
- Správa "cizích" pluginů/služeb ve vlastních aplikacích.
- Vzor Component Configurator - správa pluginů.
- Vzor Interceptor - ukázky business aplikací, které jsou rozšiřovány za běhu aplikace s minimálním úsilím a bez strastí opakovaného nasazení aplikace.
- Kdy použít vzor Special Case?
- Remote Facade a Data Transfer Object - různé přístupy k distribuované aplikaci.
- Vzory pro zpracování požadavků na aplikaci-službu.
- Kódování vzoru Acceptor-Connector.
- Asynchronous Completion Token - vlastní pomocné objekty pro zjednodušení asynchronních úloh.
- Kódování vzoru Proactor.
- Kódování vzoru Reactor.
- Thread Safe Interface - co pro nás znamená v moderních prostředích (Java a .Net Framework)
- Co jsou takzvané “Enterprise segmenty” v business aplikacích?
- V průběhu celého kurzu - kompletní případová studie existující business aplikace, v níž jsou zakódovány postupy zmiňované na kurzu - dlouhá procházka kódem. :)
Kurz Objektovými principy a návrhovými vzory řízený design a vývoj kvalitních aplikací 1
Datum konání kurzu: 20. 4. - 22. 4. 2009
Místo konání: Hotel VILLA Praha Okrajní 1, 100 00, Praha 10
U hotelu VILLA je možné parkovat, po celý den máme k dispozici wifi připojení.
Pro jistotu dodám, že na kurzu jsou samozřejmě po celý den teplé a studené nápoje a v ceně kurzu jsou obědy v hotelu.
Organizační informace ke kurzu
Program kurzu
Výběr z ohlasů na kurz
Tuesday, 11 November 2008 16:38:10 (Central Europe Standard Time, UTC+01:00)
Analytické drobky | Kurzy UML a OOP | Návrhové vzory | UML
Tuesday, 14 October 2008
Sunday, 05 October 2008
Adaptéry pro funktory v C++ => Adaptéry pro funkce v C#
V C++ je snadné napsat takzvané adaptéry pro funkce, respektive pro funktory - objekty, chovající se jako funkce. K čemu jsou adaptéry dobré? Představme si, že máme napsanou funkci equal_to, která přijímá dva argumenty a vrátí true, jestliže jsou oba argumenty shodné, jinak vrátí false. Jedná se tedy o binární funktor, protože přijímá dva argumenty. Nyní potřebujeme pomocí stl metody find_if vyhledat v naší kolekci všechny prvky, jejichž hodnota je rovna 10. Podmínku v metodě find_if musí představovat unární funktor (funktor přijímající jeden argument - prvek v kolekci - a vracející true jen v případě, že prvek v kolekci podmínku splňuje). Je zřejmé, že binární funktor nemůžeme použít na místě, kde je očekáván unární funktor. V C++ můžeme ale v této situaci namísto psaní dalšího unárního jednoúčelového funktoru využít speciálního adaptéru, jehož účelem je konverze binárního funktoru na unární. Adaptér, který přijde vhod pro naše účely, se jmenuje binder1st (zde by bylo možné použít i adaptér binder2nd). Adaptér binder1st očekává, že mu předáte binární funktor, který má být převeden na unární a hodnotu, která má být vždy použita jako první argument (proto ...1st) při volání binárního funktoru. Adaptér binder2nd se od adaptéru binder1st liší jen tím, že předaná hodnota bude použita vždy jako druhý argument předaného binárního funktoru. Jinými slovy - při volání funktoru binder1st je kolekce spokojena, že dostala unární funktor, ale náš funktor binder1st interně deleguje volání na binární funktor, kterému předá jako první argument hodnotu, kterou jsme zadali při vytvoření adaptéru binder1st, a jako druhý argument objekt z kolekce, na kterém se má otestovat platnost podmínky.
binder1st<equal_to<int> > equalPredicate = bind1st(equal_to<int>(), 10);
iterator it1 = find_if(v1.begin(), v1.end(), equalPredicate);
V předchozím kódu jsme vytvořili adaptér (unární funktor) nazvaný equalPredicate, který zprostředkovává přístup k binárnímu funktoru equal_to. Skutečnost, že je funktor equal_to binárním funktorem, poznáme z jeho deklarace.
template<class Type>
struct equal_to : public binary_function<Type, Type, bool>
{
bool operator()(
const Type& _Left,
const Type& _Right
) const;
};
Na druhém řádku příkladu adaptér equalPredicate předáme funkci find_if, která porovná každý element v kolekci (const Type& _Right) s hodnotou 10. Funkce vrátí první prvek, který vyhoví podmínce _Left==Right (konkrétně v našem případě jde o podmínku 10 == PrvekVKolekci). Konstantní hodnota 10 byla předána funkci bind1st a bude představovat při každém volání "adaptovaného" funktoru equal_to adaptérem equalPredicate hodnotu argumentu _Left operátoru(). Funkce bind1st je "syntaktickým cukrem", který zjednodušuje vytváření adaptéru, protože nemusíme specifikovat všechny typové parametry adaptéru binder1st, ale spolehneme se na typovou inferenci provedenou kompilátorem.
Konec rychlé exkurze do C++. I v C# nám mohou adaptéry pro delegáty přijít vhod. Představme si, že již máme napsanou třídu, která vrací výsledek porovnání dvou hodnot ("je menší než", "je větší než").
static class ComparerEx
{
public static bool GreaterThan<T>(T a, T b)
{
return Comparer<T>.Default.Compare(a, b) > 0;
}
public static bool LessThan<T>(T a, T b)
{
return Comparer<T>.Default.Compare(a, b) < 0;
}
}
Funkce chceme použít v LINQ podmínkách (např. můžeme chtít z kolekce celých čísel vrátit jen všechna čísla, jež jsou větší než 10). Ale také můžeme chtít sadu podmínek, které můžeme libovolně kombinovat a skládat tak jednoduše výrazy typu "všechny hodnoty z kolekce, jež jsou větší než 20, ale menší než 90". Stejně tak můžeme chtít za chvíli podmínku znegovat a máme zájem o hodnoty nepatřící do intervalu 20-90. Namísto psaní "jednoúčelových" (i anonymních) metod si můžeme jednotlivé podmínky předpřipravit a pomocí adaptérů pro delegáty je skládat do složitějších podmínek. Také můžeme chtít stejnou podmínku použít při restrikci v LINQu (Where extenze pracující s delegátem typu Func< >) i při práci se staršími metodami (např. FindAll u List<T>), které očekávají odkaz na delegáta typu Predicate. To vše nám speciální adaptéry pro delegáty v C# umožní.
Nejprve se podívejme na použití adaptérů.
class Program
{
static void Main(string[] args)
{
Random rand = new Random();
//Vygenerování náhodných čísel v rozsahu 1..100
List<int> myList = new List<int>(Enumerable.Range(1, 100).Select((i) => rand.Next(1, 100)));
//Vytvoření predikátu pro where část LINQ dotazu (všechna čísla, kromě čísel v rozsahu 10 - 90
var predicate = FuncExtension.Bind2nd<int, int, bool>(10, ComparerEx.GreaterThan);
predicate = FuncExtension.And(predicate, FuncExtension.Bind2nd<int, int, bool>(90, ComparerEx.LessThan));
predicate = FuncExtension.Not(predicate);
//LINQ dotaz - v selectu je do anonymního typu vyzvednut i index prvku v kolekci
var result = myList
.Where(predicate)
.Select((elem, index) => new {elem, index});
//Výpis LINQ dotazu
foreach (var res in result)
{
Console.WriteLine("{0}:{1}", res.index, res.elem);
}
//Ukázka konverze podmínky (Func<?, bool> na delegáta typu Predicate očekávaného funkcí FindAll
var vals = myList.FindAll(FuncExtension.ToPredicate(predicate));
//Musíme dostat stejné výsledky jako v předchozím dotazu s využitím LINQu
foreach (var val in vals)
{
Console.WriteLine(val);
}
Console.ReadLine();
}
}
V příkladu jsme si naplnili myList náhodnými čísly v intervalu od 1 do 100. Proměnná predicate představuje podmínku.
Použitím adaptéru Bind2nd(FuncExtension.Bind2nd<int, int, bool>(10, ComparerEx.GreaterThan);) vytvoříme podmínku "všechna čísla větší než 10". Vidíme, že jsme funkci ComparerEx.GreaterThan, která očekává dva argumenty, "adaptovali-převedli" na funkci (přesněji řečeno na delegáta), který očekává jeden argument. Druhým argumentem funkce ComparerEx.GreaterThan je vždy konstantní hodnota 10 předaná při volání funkce Bind2nd.
V dalším kroku vytvoříme podmínku ("všechna čísla menší než 90" - FuncExtension.Bind2nd<int, int, bool>(90, ComparerEx.LessThan)); ) a zkombinujeme ji s předchozí podmínkou pomocí speciálního adaptéru, který představuje operátor And (FuncExtension.And(predicate, FuncExtension.Bind2nd<int, int, bool>(90, ComparerEx.LessThan))). Operátor And je pro zbytek aplikace stále jen obyčejným (unární) delegátem na funkci, která přijímá jeden argument a vrací true nebo false. Nyní máme tedy podmínku "všechna čísla větší než 10 a menší než 90".
Naše konečná podmínka ale má mít podobu (""všechna čísla s výjimkou čísel větších než 10 a menších než 90"). Proto použijeme další speciální adaptér Not, který v předchozích krocích sestavenou podmínku zneguje (FuncExtension.Not(predicate);)
Z kolekce myList vybereme přes LINQ všechna čísla splňující podmínku (proměnná predicate s definicí podmínky je argumentem extenzní metody Where) a vypíšeme je do konzole.
Nakonec ještě stejnou podmínku chceme předat metodě FindAll. Metoda FindAll ale očekává delegáta nazvaného Predicate, a proto použijeme další "adaptující" funkci ToPredicate, který stávající definici podmínky konvertuje na Predicate.
Jak adaptéry pracují? Podívejme se na funkci Bind2nd.
public static Func<T0, R> Bind2nd<T0, T1, R>(T1 bindValue, Func<T0, T1, R> originalFunc)
{
return (arg => originalFunc(arg, bindValue));
}
Bind2nd je generická funkce, která jako argument (T1 bindValue) očekává hodnotu, která bude představovat vždy druhý argument adaptovaného delegáta (Func<T0, T1, R> originalFunc - funkce přijímající dva argumenty, první typu T0, druhý typu T1 a vracející R). Funkce vrátí nového delegáta (Func<T0, R>), který ukazuje na funkci očekávající jeden argument typu T0 a vracející instanci generického typu R. Delegát při svém spuštění pouze vezme předaný argument (arg) a poskytne jej jako první argument delegátovi originalFunc, kterému současně vždy předá jako druhý argument hodnotu v původním argumentu bindValue.
Podobně fungují i další adaptéry. Pro zajímavost se podívejme na adaptér ToPredicate, který z předaného delegáta vytvoří delegáta typu Predicate, čehož jsme využili v předchozím příkladu.
public static Predicate<T> ToPredicate<T>(Func<T, bool> originalFunc)
{
return arg => originalFunc(arg);
}
Funkce očekává ve svém argumentu originalFunc odkaz na delegáta typu Func, který přijímá jeden argument typu T a vrací bool. My vrátíme delegáta typu Predicate<T>, přičemž vrácený lambda výraz deleguje vykonání funkce na původního delegáta originalFunc. Pro zbytek aplikace je delegát Func<T, bool> skryt za rozhraním adaptéru Predicate<T>, který nám pomohl pro funkci FindAll přeložit podmínku "v neznámém jazyce" do srozumitelné řeči.
Následuje kompletní výpis kódu adaptérů. To, co nám prozatím chybí, je ekvivalent funkce bind1st (bind2nd) z C++, který by nám zjednodušil zápis podmínek bez nutnosti zadávat "ručně" generické argumenty. Ale o tom popřemýšlím zase "někdy jindy".
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace FunctionExtensions
{
static class FuncExtension
{
public static Func<T1, R> Bind1St<T0, T1, R>(T0 bindValue, Func<T0, T1, R> originalFunc)
{
return (arg => originalFunc(bindValue, arg));
}
public static Func<T0, R> Bind2nd<T0, T1, R>(T1 bindValue, Func<T0, T1, R> originalFunc)
{
return (arg => originalFunc(arg, bindValue));
}
public static Func<T0, bool> Not<T0>(Func<T0, bool> originalFunc)
{
return (arg => !originalFunc(arg));
}
public static Func<T0, T1, bool> Not<T0, T1>(Func<T0, T1, bool> originalFunc)
{
return ((arg1, arg2) => !originalFunc(arg1, arg2));
}
public static Func<T0, T1, T2, bool> Not<T0, T1, T2>(Func<T0, T1, T2, bool> originalFunc)
{
return ((arg1, arg2, arg3) => !originalFunc(arg1, arg2, arg3));
}
public static Func<T0, T1, bool> And<T0, T1>(Func<T0, T1, bool> originalFunc, Func<T0, T1, bool> originalFunc2)
{
return ((arg1, arg2) => originalFunc(arg1, arg2) && originalFunc(arg1, arg2));
}
public static Func<T0, bool> And<T0>(Func<T0, bool> originalFunc, Func<T0, bool> originalFunc2)
{
return (arg1 => originalFunc(arg1) && originalFunc2(arg1));
}
public static Func<T0, T1, bool> Or<T0, T1>(Func<T0, T1, bool> originalFunc, Func<T0, T1, bool> originalFunc2)
{
return ((arg1, arg2) => originalFunc(arg1, arg2) || originalFunc(arg1, arg2));
}
public static Func<T0, bool> Or<T0>(Func<T0, bool> originalFunc, Func<T0, bool> originalFunc2)
{
return (arg1 => originalFunc(arg1) || originalFunc2(arg1));
}
public static Predicate<T> ToPredicate<T>(Func<T, bool> originalFunc)
{
return arg => originalFunc(arg);
}
}
}
Sunday, 05 October 2008 17:34:45 (Central Europe Standard Time, UTC+01:00)
.NET Framework | Compact .Net Framework | LINQ | Návrhové vzory
Monday, 08 September 2008
Sunday, 18 May 2008
Wednesday, 27 June 2007
Monday, 11 September 2006
Fórum o OOP, UML, návrhových vzorech, MDA, DSL ... - chtěli byste?
Nadpis vyjadřuje v kostce vše. Hraji si právě teď s překladem a nastavením YetAnotherForum a napadlo mě, že bych na doméně forum.renestein.net spustil fórum, kde bychom společně diskutovali o návrhu aplikací, systémovém designu, OOP, UML, Model Driven Architecture, DSL, zuřivě bychom se hádali nad best practices, vášnivě "flamovali" nad podporou OOP v různých programovacích jazycích :) nebo bychom si vyměňovali linky na zajímavé články. Pro každé větší téma by existovalo samostatné fórum.
Vím. že některá česká fóra se OOP a analýzou zabývají, ale kvůli svému neodvolatelně finálním stavu "mrtvé" fórum s občasnými "self" přechody, spuštěnými přijetím jedné OT zprávy s nabídkou domácích zásob viagry nějakého momentálně insolventního a celoživotně impotentního spammera, se v nich nic zajímavého neděje.
Takže - máte zájem? :)
Monday, 11 September 2006 14:55:02 (Central Europe Standard Time, UTC+01:00)
Návrhové vzory | Ostatní | UML
Sunday, 20 August 2006
Sunday, 09 April 2006
Bázová třída pro typové kolekce v .NET Frameworku 2.0
V souvislosti s uvedením generiky v .NET Frameworku se v různých článcích dočtete, jak generika usnadní vytváření a použití typových kolekcí. To je sice pravda, ale v článku se kromě zjednodušených příkladů a frikulínských hlášek o dokonalosti .NETu 2.0 z úst (respektiva pera) excitovaných jedinců po právě dokonaném intimním styku se softwarovou emanací božstva Microsoftu málokdy dovíte, jak by taková typová kolekce měla vypadat v běžné aplikaci.
Proč nevyhovuje obyčejné použití generického typu List? (deklarace typové kolekce objednávek ve tvaru List<Order> orderCollection = new List<Order>()).
- Jednou z dobrých praktik u generik je co nejvíce před uživateli (dalšími vývojáři) skrývat informaci, že pracují s generickým typem. Ač mně syntaxe pro práci s generickými typy připadá intuitivní, nemusí si to myslet všichni, a mnoho vývojářů stále asi raději používá důvěrně známý kód OrderCollection orderCollection = new OrderCollection() místo výše zmíněného kódu List<Order> orderCollection = new List<Order>()). Tento požadavek by ale List<T> splnil - typová kolekce může být potomkem List<T>.
- Ve třídě List nejsou metody Add a Remove virtuální. To znamená, že nemůžete po přidání nebo odebrání položky z/do kolekce vyvolávat vlastní události. A to je problém, protože po přidání/odebrání položek z kolekce můžeme chtít nastavit/zrušit rodiče nebo přepočítát sumární hodnoty za položky v kolekci apod.
Bázová třída pro všechny kolekce, kterou používám, je otevřeným generickým typem a jejím předkem je třída Collection<T> z jmenného prostoru System.Collections.ObjectModel. Třída Collection<T> nabízí virtuální chráněné metody InsertItem a RemoveItem, ve kterých můžete vyvolávat potřebné události. Jestliže používate návrhový vzor Layer SuperType a máte tedy jednu bázovou třídu pro všechny business objekty (BusinessObjectBase), je vhodné, aby bázová třída pro kolekce kladla na generický typ T omezení, že musí být potomkem třídy BusinessObjectBase. Omezení slouží k tomu, abyste ve svých kolekcích mohli intuitivně používat všechny atributy a metody deklarované na úrovni společného předka BusinessObjectBase.
Kód kolekce:
public class BusinessCollectionBase<T> : Collection<T>
where T : BusinessObjectBase
{
#region Events
public event EventHandler<CollectionChangeEventArgs> ItemAdded;
public event EventHandler<CollectionChangeEventArgs> ItemRemoved;
#endregion Events
#region Protected methods
/// <summary>
/// Přidání položky do kolekce
/// </summary>
/// <param name="index">Index položky</param>
/// <param name="item">Vkládaná položka</param>
protected override void InsertItem(int index, T item)
{
base.InsertItem(index, item);
OnItemAdded(new CollectionChangeEventArgs(item));
}
/// <summary>
/// Přidání položky do kolekce
/// </summary>
/// <param name="index">Index položky</param>
protected override void RemoveItem(int index)
{
T item = this[index];
base.RemoveItem(index);
OnItemRemoved(new CollectionChangeEventArgs(item));
}
/// <summary>
/// Metoda odpovědná za vyvolání události ItemRemoved
/// </summary>
/// <param name="e">Argumenty události</param>
protected void OnItemRemoved(CollectionChangeEventArgs e)
{
if (ItemRemoved != null)
{
ItemRemoved(this, e);
}
}
/// <summary>
/// Metoda odpovědná za vyvolání události ItemAdded
/// </summary>
/// <param name="e"></param>
protected void OnItemAdded(CollectionChangeEventArgs e)
{
if (ItemAdded != null)
{
ItemAdded(this, e);
}
}
/// <summary>
///
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public virtual T FindById(Guid id)
{
List<T> mylist = (List<T>) Items;
T objectMeetsCriteria = null;
objectMeetsCriteria = mylist.Find(delegate(T iterObject)
{
if (iterObject.Id == id)
{
return true;
}
else
{
return false;
}
});
return objectMeetsCriteria;
}
/// <summary>
/// Nalezení všech objektů splňujících zadanou podmínku
/// </summary>
/// <param name="criteria">Podmínka výběru</param>
/// <returns>List s objekty, které splňují zadanou podmínku</returns
public virtual List<T> Find(Predicate<T> criteria)
{
List<T> mylist = (List<T>) Items;
return (mylist.FindAll(criteria));
}
/// <summary>
/// Spuštění akce nad všemi elementy v kolekci
/// </summary>
/// <param name="action">Akce, která se má provést</param>
public virtual void ForEach(Action<T> action)
{
List<T> mylist = (List<T>)Items;
mylist.ForEach(action);
}
#endregion Protected methods
}
}
Jak vidíme:
- Třída BusinessCollectionBase je potomkem třídy Collection<T> a vyžaduje, aby typ T byl vždy potomkem BusinessObjectBase. Motivace pro toto rozhodnutí jspu popsány výše.
- Nadeklarovali jsme dvě události ItemAdded a ItemRemoved, které jsou vyvolávany v přepsaných metodách InsertItem (ItemAdded) a RemoveItem (ItemRemoved). Pokud bych měl zájem, mohu jednoduše přidat i události vyvolávané před přidáním/odebráním položky z kolekce.
- Do rozhraní BusinessCollectionBase jsem také přidal několik zajímavých metod.
- Metoda FindById nalezne podle předaného Id (unikátní identifikátor instance) objekt v kolekci. V této metodě opět používáme nové konstrukce z .Net Frameworku 2.0. Implementační objekt kolekce (starý známý List<T> ) vystavuje metodu Find, která očekává generického delegáta Predicate z jmenného prostoru System.
public delegate bool Predicate( T obj);Delegát Predicate je "ukazatelem" na metodu, která očekává jeden generický argument T a vrací true nebo false. Delegát Predicate tedy zastupuje metodu s podmínkou, která je pro předaný argument obj pravdivá nebo nepravdivá. My pro vytvoření podmínky použijeme anonymní metodu, která vrátí true pouze tehdy, když se Id objektu v kolekci shoduje s předaným Id. Atribut Id u generického typu T kolekce můžeme používat právě proto, že jsme zavedli pro typ T omezení (musíš být potomkem BusinessObjectBase) a atribut Id je deklarován ve třídě BusinessOBjectBase.
- Pro pokročilejší operace s elementy kolekce jsme z objektu List<T> zveřejnili metody FindAll A ForEach. Metoda FindAll podle předané podmínky (delegát Predicate) nalezne a vrátí všechny objekty, které jí vyhovují. Metoda ForEach spustí pro všchny elementy v kolekci "akci - činnost" implementovanou v metodě, na níž "ukazuje" další užitečný delegát Action<T>.
public delegate void Action<T> ( T obj);
Když tedy budete chtít všechny objekty v kolekci zrušit, místo psaní cyklu foreach napíšete kód podobný tomuto:
myCol.ForEach(delegate(OrderItem item)
{
item.Discard();
});
Vytváření vlastních typových kolekcí je jednoduché:
/// <summary>
/// Kolekce objektů OrderItem
/// </summary>
public class OrderItemCollection : BusinessCollectionBase<OrderItem>
{
}
Pro úplnost sem dávám triviální kód třídy pro argumenty události CollectionChanged.
/// <summary>
/// Objekt v kolekci
/// </summary>
public class CollectionChangeEventArgs : EventArgs
{
#region Private variables
private BusinessObjectBase m_collectionObject;
#endregion Private variables
#region Constructors
/// <summary>
/// Konstruktor
/// </summary>
/// <param name="collectionObjekt">Objekt v kolekci, kterého se událost týká</param>
public CollectionChangeEventArgs(BusinessObjectBase collectionObject)
{
BasicValidations.AssertNotNull(collectionObject, "collectionObject");
m_collectionObject = collectionObject;
}
/// <summary>
/// Objekt v kolekci, kterého se událost týká
/// </summary>
public BusinessObjectBase CollectionObject
{
get
{
return m_collectionObject;
}
}
#endregion Constructors
}
Související články:
Bázová třída pro business objekty - návrhový vzor Layer Supertype
Cachování řádků z databáze pro business objekty - třída DataCacheHelper
Ukázka použití třídy BusinessObjectBase
>
Sunday, 09 April 2006 14:34:33 (Central Europe Standard Time, UTC+01:00)
.NET Framework | Compact .Net Framework | Návrhové vzory
Sunday, 05 March 2006
Termíny kurzů o OOP a UML, partnerství s EIITE, InHouse školení
UPDATE:
Podrobné a aktuální informace o dalších termínech školení a nabídce dalších služeb naleznete nyní zde.
Jak jsem již naznačil v předchozím spotu, uzavřel jsem "strategické partnerství" s novým školícím střediskem - se společností EIITE. Mé kurzy budou pořádány pod hlavičkou Skilldrive.
Všechny kurzy se budou konat v Praze.
Přesná adresa:
EIITE Praha
Karlovo nám. 17
120 00 Praha 2
Termíny kurzů:
- Objektovými principy a návrhovými vzory řízený design a vývoj kvalitních aplikací - 3-5.5 2006 (každý den od 9:00 do 16:00). Program a další informace o kurzu naleznete v předchozím spotu.
Přihlášky na kurz je možné stále posílat na adresu petra@renestein.net.
Kurz je obsazen: Pokud máte stále zájem o kurz, napište nám prosím opět na adresu petra@renestein.net a my Vás budeme informovat ihned po vyhlášení dalšího termínu školení. Je také možné stále objednávat InHouse školení (viz níže).
- Kurz UML pořádaný a administrativně zajišťovaný společností EIITE - 28-29.8. 2006. Přihlášení na kurz.
InHouse školení
Pokud máte zájem o inhouse školení zaměřené na OOP, návrhové vzory, UML, .NET Framework, Compact .Net Framework nebo chcete vytvořit kompletní systémový design aplikace včetně naprogramování klíčových částí, napiště prosím svůj požadavek také na adresu petra@renestein.net nebo nás kontaktujte na telefonním čísle +420 603 266 732. Omezení pro inhouse školení, o kterých jsem zde psal dříve, již neplatí!
Sunday, 05 March 2006 12:36:23 (Central Europe Standard Time, UTC+01:00)
Kurzy UML a OOP | Návrhové vzory | UML
Sunday, 26 February 2006
Několik informací ke kurzům UML a OOP
Takže předběžně ke kurzům UML a OOP, protože se mi nechce psát stále stejné věci každému zvlášť do mailu. V souvislosti se změnou zaměstnání se mění i některé věci ke kurzům.
- Kurzy OOP a UML již nebudou pořádány ve školícím středisku DIGI TRADE, ale ve velmi pěkných prostorách jiné společnosti, s níž jsem uzavřel "strategické partnerství" a jejíž jméno oznámím příští týden.
- První termín (pravděpodobně konec dubna/začátek května) patří kurzu Objektovými principy a návrhovými vzory řízený design a vývoj kvalitních aplikací., na kterém již mnozí z vás byli. Pro připomenutí níže vkládám program kurzu. Pokud máte již nyní zájem předběžně si zarezervovat místo na kurzu, pošlete prosím email na adresu petra@renestein.net. Cena kurzu je 13 000 Kč.
- Dále cca za měsíc zveřejním podrobný program a termín dalšího mého školení s pracovním podtitulem "Design aplikací a návrhové vzory prakticky - od chaosu k řádu".
- S novým školícím střediskem budeme také pořádat "jednodušší" (přesněji řečeno jinak zaměřený a více na základní znalosti orientovaný) kurz, uvádějící posluchače prakticky do světa UML (2.0). Podrobnosti a termín zveřejním do 14 dnů.
- Fungovat to bude takto - první dva kurzy je po administrativní stránce zcela na mě, kurz o UML bude po admistrativní stránce plně zajišťovat nové školící středisko. I program kurzu o UML jsem samozřejmě připravil i já, nikdy bych nemohl školit nějaké "předžvýkané" řečičky někoho jiného. Uvádím to zde jen proto, aby vás nemátlo, když se u některých kurzů objeví informace, že máte přihlášky posílat mně (respektive mé ženě) a u jiných budete přihlášky směřovat rovnou na nové školící středisko. Kurzy se ale budou vždy konat na stejném místě.
- Omlouvám se těm, kdo absolvovali první kurz a čekají na již poměrně dlouho slibované uvolnění studijních materiálů. Prozatím jsem nenašel čas (respektive vždy jsem našel zajímavější práci ;) , než je "vyčištění" materiálů k veřejnému použití) a také se mi nechce kvůli špatným zkušenostem uvolňovat materiály před konáním dalších kurzů, protože jsem se už v reálu setkal s tím, jak materiály neurčené široké veřejnosti začnou po internetu rychle kolovat, po čemž opravdu nijak netoužím. :(
1. den
- Přivítání účastníků kurzu.
- Úvodní informace o zaměření a organizaci kurzu.
- Základní pojmy OOP a UML..
- Mýty o OOP a UML.
- Vysvětlení rozdílů mezi business analýzou, systémovým designem a implementací aplikace na konkrétní platformě.
- Světlo v temnotách – Model Driven Architecture (MDA)
- Základní architektura a rozvrstvení aplikace.
- Statický pohled na systém – vytváříme základní diagram tříd a ověřujeme, že jsou v něm zaneseny všechny informace, jež jsou nám známy z případů užití.
- Zvolení složitosti diagramu tříd. Potřebujeme vždy flexibilní doménový model?
- Zapouzdření objektů, polymorfismus, návrh metod.
i. Důležitost principů kovariance a kontravariance.
ii. Různé typy soudržnosti metod.
iii. Rozhodnutí o typu viditelnosti u každého člena třídy.
iv. Jaké konstruktory by měl nabízet každý objekt z problémové domény? Jak určit vlastnosti pouze pro čtení.
v. Ověření bezpečného chování třídy vůči potenciálním klientům.
- Precizní definice vztahů mezi třídami. Asociace, kompozice, agregace, závislost, realizace, generalizace.
- Vysvětlení rozdílů mezi abstraktní třídou a rozhraním (interface).
i. Vztah mezi typem a podtypem.
ii. Rozpoznání primárního účelu (hlavního smyslu) třídy i jejich sekundárních odpovědností vynucených vztahy s objekty z různých vrstev.
- Praktický příklad - ukázka implementace vzorových vztahů mezi objekty, perzistence objektů z problémové domény a zobrazování dat. (jazyk C#)
- Separace kódu pro ukládání a obnovení objektů z perzistentního úložiště v samostatné vrstvě.
- Jak zajistíme, že v paměti počítače existuje nanejvýš jedna instance objektu se stejnou identitou.
- Ukázky různých způsobů mapování agregace, kompozice, generalizace a asociace do databáze.
- Zajištění existence maximálně jedné instance objektu v systému.
- Efektivní ukládání a nahrávání kolekcí.
- Jak se slučuje objektový přístup a přímé použití DataSetu (recordsetu) v uživatelském rozhraní?
- Odpovědi na dotazy frekventantů kurzu.
2. den
- Vysvětlení pojmu návrhový vzor.
- Kdy byste měli používat návrhové vzory?
- Základní vzory (GoF vzory)
- Vzory pro řízení vzniku objektů.
- Strukturální vzory.
- Vzory pro chování objektů.
- Začlenění návrhového vzoru do designu aplikace. Kreativní aplikace vzorů.
- Kompozice vzorů do vyšších sémantických celků.
- Příklady odvozených návrhových vzorů často používaných při designu informačního systému.
- Kdy byste neměli používat návrhové vzory?
- Příklad - ukázky implementace složitějších vzorů. (jazyk C#).
- Odpovědi na dotazy frekventantů kurzu.
3. den
- Typické problémy při modelování informačního systému a jejich řešení.
- Modelování složitých organizačních struktur.
- Výhody vytváření fasád (Facade) pro aplikace s více než jedním typem uživatelského rozhraní (lehký klient, těžký klient).
- Evidence kompletní historie objektu.
- Aplikační role a práva uživatelů.
- Vytvoření flexibilního systému, jehož chování je změněno bez rekompilace aplikace.
- Příklad – ukázky řešení problémů při modelování informačního systému. (jazyk C#).
- Odpovědi na dotazy frekventantů kurzu.
- Ukončení kurzu.
Sunday, 26 February 2006 20:27:49 (Central Europe Standard Time, UTC+01:00)
Kurzy UML a OOP | Návrhové vzory | UML
Sunday, 05 February 2006
Ukázka použití třídy BusinessObjectBase
Máme-li napsanou třídu BusinessObjectBase, která hraje roli společného předka všech objektů v business vrstvě všech našich aplikací, je čas ukázat, jak vypadají typičtí potomci.
Předchozí spoty
Bázová třída pro business objekty - návrhový vzor Layer Supertype
Cachování řádků z databáze pro business objekty - třída DataCacheHelper
Zobrazit kód
Třída Order reprezentuje v našem systému objednávku. U objednávky nás zajímá, jakému patří zákazníkovi (vlastnost Customer), rozhodli jsme se, že budeme vyžadovat u každé instance objednávky popisek (vlastnost Description) a samořejmě samotná objednávka je složena z položek objednávky (kolekce Items). Položkou objednávky budeme rozumět třídu, v níž je uloženo, jaké zboží a kolik kusů si zákazník objednal a celková cena položky (kusová cena objednaného zboží * počet). Celková cena objednávky je sumou cen jednotlivých položek. Pro jednoduchost vidíte v kódu zatím jen třídu Objednávka.
- Třída Objednávka má dva konstruktory. Prvni konstruktor, přijímající popisek objednávky a zákazníka, vyvolává bezparamerický konstruktor třídy BusinessObjectBase. Jak jsem psal v předchozím spotu, bezparametrický konstruktor slouží k sestrojení objektů bez obrazu v databázi. Chcete-li založit novou objednávku, stačí když si uložíte odkazy na objekty předané v konstruktoru a zbytek práce delegujete na BusinessObjectBase, která nastaví všechny potřebné příznaky nutné pro práci s objektem bez perzistence. Druhý konstruktor přijímá Id (unikátní identifikátor) objednávky, která je uložena v databázi. Jeho tělo je prázdné, veškerou činnost provádí BusinessObjectBase - nastavení příznaků a uložení Id pro pozdější nahrání objektu z databáze. Objekt je po vykonání konstruktoru "prázdnou schránkou", duchem (GHOST), který načte svá data z databáze teprve tehdy, když použijeme některou metodu nebo vlastnost pracující s instančními a perzistovanými proměnnými. Z hlediska uživatele naší třídy (myšleno programátora) je ale vše ve starých kolejích - rozhraní objektu bude reagovat tak, jak očekává, a existenci pozdní incializace objektu (Lazy Load) může ignorovat. Poslední větu si přečtěte ještě jednou a tiše s k ní přidejte pravidlo: Nikdy nesmím nutit uživatele svých tříd volat nějaké speciální "Init" metody předtím, než začnou s třídou pracovat. Jedinou legální "Init" metodou je konstruktor.
- Jak k pozdní incializaci a tedy nahrání dat objektu dojde? Vlastnosti, jejichž hodnoty jsou uložené v databázi (Id zákazníka jako klíč v tabulce Objednávek a popisek (Description) objednávky) mají na prvním řádku svých get/set přístupových metod volání metody TriggerLoad. Jak víme, jde o volání metody z BusinessObjectBase, která sama rozhoduje, zda již byla data objektu nahrána z databáze. Pokud data nebyla nahrána, třída BusinessObjectBase řídí scénář nahrávání objektu v metodě Load. Jestliže jsou data pro objednávku v třídě DataCacheHelper, je volána metoda DoInternalLoad(DataRow row), která načte data objednávky z předaného řádku. Když v třídě DataCacheHelper data pro aktuální instanci nemáme, BusinessObjectBase volá variantu metody DoInternalLoad bez argumentů. Metoda DoInternalLoad vyzvedne řádek z databáze s využitím databázové komponenty (což je Singleton nebo Thread specific storage - jednoduše třída zapoudřující API pro práci s datovým zdrojem) a tento řádek předá své stejně nazvané sestřičce, o níž byla řeč výše.
Důležité je, že celý scénář nahrávání řídí bázová třída - v odvozené třídě jen v přesně vymezených bodech scénáře "dosazuje" třída Order své vlastní specifické chování a používá své speciální atributy.
- V set přístupových metodách u vlastností využíváme metodu CheckEquals z BusinessObjectBase. Jak už víte z minulého spotu, metoda CheckEquals zkontroluje rovnost dvou objektů a podle toho nastaví příznak IsDirty - byl objekt změněn proti datům v databázi a musí tedy dojít k jeho uložení?
- Jak je to nahráním položek objednávky - vlastnost Items? Často se stává, že potřebujete pracovat se samotným objektem, ale je zbytečné při nahrání dat objektu z databáze ihned plnit i všechny jeho kolekce. Kolekce Items je naplněna při prvním přístupu ke kolekci - příznak, zda byla kolekce nahrána nese pomocná instanční proměnná m_itemsLoaded. Jestliže kolekce naplněna nebyla, je volána privátní metoda loadItems, v níž s pomocí databázové komponenty vyzvedneme z databáte všechny položky objednávky - filtrem, který je použit v SQL dotazu je samozřejmě Id objednávky. Poté projdeme vrácenou tabulku a všechny řádky uložíme do třídy DataCacheHelper. Zde přepokládáme, že metoda DbComponent.Instance.OrderItem_GetByOrderId, vrací z databáze všechna data potřebná pro obnovení objektu z databáze, nejen jejich Id. To je běžná praxe, pokud máte uloženou proceduru, která vybírá záznamy z jedné tabulky (pro jednu třídu) podle různých podmínek. Důležité je, že za pomoci identitní mapy (Identity Map) sestrojíme objekty - "duchy" - OrderItem, kterým předáme jejich Id, a třída BusinessObjectBase již zajistí, že když instance OrderItem budou chtít nahrát svá data z databáze, tak jí bude vrácen řádek uložený v instanci třídy DataCacheHeper. To znamená, že třída OrderItem nebude zbytečně zatěžovat databázi duplicitními dotazy a přitom kontrola na existenci cachovaného řádku pro instance jakékoli třídy je součástí společného předka BusinessObjectBase a "nezasviníme" si stejným kódem celou business vrstvu.
- Metoda Order také přepisuje metodu DoInternalSave, v níž uloží své atributy do databáze. Jak uvidíme dále, metodě Order_Update předáváme hodnotu zděděného a bázovou třídou spravovaného příznaku IsNew - tento příznak metoda interně použije k rozhodnutí, zda provede SQL příkaz INSERT nebo UPDATE1. Opět - po volání metody Save uživatelem jen třída BusinessObjectBase jakožto finální instance rozhoduje, zda je potřeba data ukládat a zda je tedy vubec nutné a účelné volat metodu DoInternalSave. Opět vidíte čistý řez mezi kontrolou, kdy je nutné objekt uložit (třída BusinessObjectBase), a samotným ukládáním (třída Order i další potomci BusinessObjectBase). Kromě uložení vlastních hodnot odpovídá třída za uložení agregovaných objektů OrderItem. Proto v metodě DoInternalSave nejprve zkontroluje, jestli byly objekty v kolekci nahrány (příznak m_itemsLoaded) a pokud ano, tak jen zavolá pomocnou metodu SaveCollection, která je implementována, jak jinak že;), v třídě BusinessObjectBase.
I když z příkladu byste měli začít tušit, proč je BusinessObjectBase tak výhodná, stále neřešíme některé problémy.
- V příkladu není vůbec řešeno odebírání a přidávání položek do kolekce - to znamená nastavování/zrušení "rodiče" u agregovaných objektů. Nijak jsme neřešili m:n relace a odpovědnost za zakládání/rušení záznamů ve vazebních tabulkách. Myslíte, že i zde půjde využít třída BusinesObjectBase? ;)
- Co když si budeme chtít u některých potomků BusinessObjectBase "vynutit" jiné chování - třeba zamezit použití DataCacheHelperu?
- A co transakce? V našem příkladu zatím nijak neřešíme ukládání objektů v transakci, což bychom měli, protože určitě nechceme mít v systému objednávky s polovinou objednaných položek. Jaký objekt má spouštět a řídit transakci? Kdo musí ošetřit chyby vzniklé při ukládání objektu?
To be continued... :)
Poznámky:
- Jsem si vědom střídavé soudržnosti metody související s "přepínačem" IsNew . V dalších dílech bude vysvětleno, proč střídavá soudržnost zrovna u této metody nevadí.
Sunday, 05 February 2006 14:44:36 (Central Europe Standard Time, UTC+01:00)
Návrhové vzory
Sunday, 21 August 2005
Bázová třída pro business objekty - návrhový vzor Layer Supertype
Na konci spotu, v němž byla vytvořena třída DataCacheHelper, jsem slíbil, že ukážu, jak si vynutit použití objektu DataCacheHelper všemi třídami v business vrstvě. K tomu nám poslouží vzor Layer SuperType, jehož definice se dá parafrázovat takto: zaveďme společného předka pro všechny třídy jedné vrstvy aplikace, v němž zapoudříme společný kód a sdílené chování.
Nyní si popíšeme rozhraní a implementaci vzorového předka pro business objekty. Zobrazit kód. (C#)
Abstraktní třída BusinessObjectBase obsahuje dva konstruktory. Bezparameterický konstruktor je potomky vyvoláván při zakládání nové instance bez obrazu v databázi ("bez platného - perzistentního id). V tomto konstruktoru nastavíme, že objekt není změněn vůči datům v databázi (m_isDirty = false), přidělíme mu dočasné id uložené v konstantě NO_ID a voláním privátních metod setNew a setLoaded označíme objekt jako "nový - bez obrazu v databázi" a současně "není třeba nahrávat data z databáze". U každého nového objektu se také předpokládá, že jde o aktivní objekt - tedy o objekt, který nemá příznak smazán (Discarded) ani neaktivní (InActive) - viz enumerace ObjectState na konci výpisu.
Druhý konstruktor, který přijímá argument id, používají odvozené objekty, jestliže již byly dříve uloženy do databáze. Opět nastavíme příznak, že objekt není změněn vůči datům v databázi (m_isDirty), dále nastavíme proměnnou m_isLoaded na false, což je příznak, který signalizuje, že data objektu ještě nebyl nahrána z databáze, a uložíme si id objektu.
Veřejná vlastnost IsDirty vrátí true, jestliže byl objekt změněn vůči datům v databázi, vlastnost IsNew vrátí true, jestliže se jedná o nový objekt bez perzistence - čeká na "Insert".
Vlastnost IsDeleted má hodnotu true, když již byla data objektu v databázi smazána příkazem Delete (objekt jen "dožívá" v business vrstvě) nebo když u objektů, jejichž data se například kvůli zachování referenční integrity fyzicky nemažou, byl do databáze zapsán stav Discarded.
Vlastnost Id vrací unikátní identifikátor objektu, jestliže byl objekt uložen do databáze, jinak vrací konstantu NO_VALUE.
Vlastnost ObjectState vrací stav objektu s využitím hodnot enumerace ObjectState (aktivní objekt, dočasně neaktivní objekt, smazaný objekt).
Metoda Load je pro nás zajímavá hlavně tím, že si vynutíme použití třídy DataCacheHelper. Jedná se metodu se šablonou (vzor Template method), která nejprve zkontroluje, jestli jde o objekt, který již byl nahrán a/nebo který byl již smazán - pokud je jedna z těchto podmínek pravdivá, metoda Load ihned ukončí svoji činnost. Když jsou všechny předběžné podmínky pro vyvolání metody Load splněny, metoda se pokusí vyzvednout dříve uložený řádek s daty pro svou instanci z třídy DataCacheHelper. Jestliže řádek existuje, pak je vyvolána varianta metody DoInternaLoad přijímající odkaz na řádek, jinak je vyvolána metoda DoInternalLoad, která musí řádek z databáze teprve získat. Metody DoInternalLoad jsou chráněné a virtuální a za jejich implementaci odpovídají odvozené třídy - třída BusinessObjectBase dbá na dodržení základní kostry algoritmu, ale implementaci jednotlivých kroků aloritmu ponechává (musí ponechat) na podtřídách, což je hlavní idea vzoru Template Method. Po úspěšném nahrání objektu je volána metoda setLoaded, která označí objekt jako nahraný (IsLoaded bude nyní vracet true).
Poznámka: V metodě Load se zatím předpokládá, že data objektu potřebujeme nahrát pouze jednou a metoda tedy neumožňuje aktualizaci dat objektu z databáze po prvním nahrání. Jak aktualizaci dat z databáze odvozeným třídám umožníme, si ukážeme v dalším pokračování.
Metoda Save neumožní uložení objektu, jestliže jde o nenahraný objekt ("není co ukládat, data nemohla být změněna"), jestliže jde o vymazaný objekt, nebo pokud objekt nebyl změněn a současně nejde o nový objekt. Metoda tedy již na úrovni předka ošetří, že nebudou zbytečně ukládány objekty, jejichž data byla nahrána, ale uživatel je nijak nezměnil. Metoda pouze volá chráněnou virtuální metodu DoInternalSave a jak asi ti chytřejší z vás tuší, jedná se o další příklad vzoru Template Method. Voláním privátní metody setClear po úspěšném uložení objektu nastavíme na false příznak "změněn" (IsDirty) a "nový objekt" (IsNew). Jestliže byl ukládán objekt, u nějž bylo vyžádáno smazání (ObjectState=ObjectState.Discarded), je nastaven příznak IsDeleted voláním chráněné metody SetDeleted na true.
Metoda Discard slouží ke zrušení objektu, přesněji řečeno k převedení objektu do stavu ObjectState.Discarded - pokud je objekt v tomto stavu a dojde k uložení objektu, metoda DoInternalSave buď záznam v databázi fyzicky vymaže (DELETE) nebo do databáze uloží stav ObjectState.Discarded (UPDATE). Metoda Discard pouze zkontroluje, zda nepracujeme nad smazaným objektem (IsDeleted=true), u nějž opakované rušení nemá smysl, a jestliže tomu tak není, nastaví objektu stav ObjectState.Discarded a označí objekt voláním metody SetDirty jako změněný.
Chráněnou statickou metodu SaveCollection používají odvozené třídy pro ukládání objektů ve svých kolekcích. (vztah agregace nebo kompozice). Výhodou je, že v odvozených třídách nemusí být stále stejný a otravný cyklus foreach, který se liší jen typem iterovaného objektu.
Metody DoInternalLoad a DoInternalSave jsme již zmiňovaly - jsou to "sloty" používané odvozenými třídami k zavedení vlastní aplikační logiky do schématu vysokoúrovňového scénáře. Bázovou implementaci metody DoInternalLoad přijímající řádek s daty objektu mohou zavolat odvozené třídy, jestliže chtějí nahrát stav objektu (ObjectState) bez zbytečné duplikace kódu - obnovení a ukládání hodnot vlastních specifických vlastností je samozřejmě plně v režii odvozených tříd. Metoda DoInternalLoad bez argumentů ani metoda DoInternalSave nejsou abstraktní, i když by abstraktní být mohly - v business vrstvě ale občas máme třídy, u nichž nemusíme (nechceme) psát perzistenci a proto mají metody v bázové třídě prázdné tělo, abychom nebyli nuceni zcela zbytečně ve více odvozených třídách poskytovat symbolické "prázdné implementace".
Metodu Init mohou odvozené třídy použít pro vlastní inicializační kód (vytváření kolekcí a přihlašování jejich událostí atd.) - příklad uvidíte v pokračování spotu.
Metoda SetDirty nastavuje příznak "změněn oproti datům v databázi" - příznak nemá význam pro objekty pro objekty bez perzistence a pro smazané objekty.
Metoda CheckEquals porovná předané objekty - jestliže se objekty nerovnají, je objekt označen jako změněný. Jak asi tušíte, metodu volají odvozené třídy v set sekci svých vlastností (CheckEquals(oldValue, newValue)) a jsou tak zbaveni nutnosti reagovat na výsledek porovnání a ukládat si vždy každá sama za sebe příznak "jsem změněn". Když uživatel zmáčkne tlačítko uložit na formuláři a my data z formuláře přeneseme do objektu, metoda CheckEquals zajistí, že objekt bude označen jako změněný jen tehdy, pokud uživatel opravdu nějaké změny na formuláři provedl (třeba vybral jinou hodnotu v listboxu). Výhody jsou zřejmé, kód na formuláři je stále týž - pouze "tupě" přenáší data ze vstupních prvků formuláře do vlastností business objektu a ten si již potutelně a sám na úrovni předka ošetří, zda nově předané hodnoty vyžadují uložení, nebo ne, což může nastat, když uživatel z pouhého rozmaru klikne na tlačítko uložit a přitom si detail objektu celou dobu jen prohlíží.
Metoda TriggerLoad iniciuje nahrání objektu, jestliže objekt ještě nebyl nahrán, nejde o nový objekt a také nesmí jít o smazaný objekt. Metodu volají odvozené třídy při přístupu k jakékoli vlastnosti nebo metodě objektu (vzory Lazy Load, Ghost).
Význam dalších chráněných a privátních metod byl již myslím dostatečně osvětlen v předchozím textu při výkladu veřejného rozhrani, takže se nebudu opakovat.
Rozhraní IPersistable vyjadřuje minimální nárok, který musejí splňovat všechny objekty ukládající a obnovující svá data z/do perzistentního úložiště - výhodnou výchozí a závaznou osnovu realizace rozhraní poskytuje třída BusinessObjectBase.
Z enumarace ObjectState pro vyjádření základních stavů objektu prozatím nepoužívame stav InActive - význam ostatních stavů by již vám měl být po přečtení spotu dostatečně zřejmý.
Příště si ukážeme vzorovou implementaci třídy, která dědí z BusinessObjectBase.
Sunday, 21 August 2005 21:29:28 (Central Europe Standard Time, UTC+01:00)
.NET Framework | Návrhové vzory
Sunday, 07 August 2005
Cachování řádků z databáze pro business objekty - třída DataCacheHelper
Při psaní business vrstvy libovolné aplikace jste jistě narazili na následující problém, který můžeme demonstrovat na profláknutém příkladu s objednávkami a jejich položkami. Pod položkou rozumíme instanci třídy, ve které jsou uloženy informace o vybraném produktu, počtu objednaných kusů produktu a celkové ceně. Objednávka a položka objednávky jsou třídy, které jste už určitě psali tolikrát, že nemáte rádi ani jejich názvy ;)
Každá objednávka má 0..n položek a vyžádáme-li si konkrétní položku z kolekce všech položek (Items) u objednávky, která již byla uložena do databáze, musí dojít nejprve k nahrání kolekce. Je jedno, zda položky nahráváte ihned privátní metodou nazvanou třeba loadItems volanou z konstruktoru třídy Objednávka, nebo zda (a lépe) používáte zpožděné nahrávání, kdy kolekce Items je naplněna stejnou metodou teprve po prvním přístupu a konstruktory všech business objektů pouze uloží předané unikátní identifikátory (Id) a naplní své vlastnosti uloženými daty (kromě kolekcí) až po prvním přístupu k nějaké vlastnosti (kombinace vzorů Lazy Load a Ghost). Na přístupu k plnění dat objektů nezáleží, jen vždy musíte garantovat, že uživatel objektu musí přistupovat k datům objektu, aniž by si byl vědom provedených výkonnostních optimalizací - interní impementace je i zde nedotknutelné privatissimum objektu.
Jak zjistíte v metodě loadItems, jaké položky objednávka (třída Order obsahuje? Přes datovou vrstvu spustíte dotaz, která vrátí všechny záznamy s daty pro všechny položky objednávky - parametrem dotazu je samozřejmě Id objednávky. Kruciálním problémem je ale vytvoření položek objednávky (třída OrderItem) z vrácených záznamů. Jaké máme možnosti?
- Vytvoříme novou instanci OrderItem a předáme jí do konstruktoru její Id. Výhodou je, že třída Order není s třídou OrderItem nijak svázána a pouze jí předává její identifikátor. Velkou nevýhodou ale je to, že object OrderItem při obnovování svých dat z databáze bude znovu posílat dotaz do databáze "dej mi data pro mé Id", což nebude nijak efektivní přístup zvláště, když v objednávce budou desítky nebo stovky položek a každá z nich s opulentní rozhazovačností pošle svůj dotaz do databáze. Navíc je to zcela zbytečné, protože data již byla vyzvednuta v metodě loadItems, ale "jen" nebyly objektu OrderItem předána.
- V metodě loadItems přečteme záznamy položek objednávky, vyzvedneme data ze všech sloupců a pro každou položku vyvoláme konstruktor, který akceptuje všechny vlastnosti položky. Tedy kromě Id předáme i celkovou cenu, počet kusů atd. Sice jsme se již zbavili opakovaného dotazování do databáze položkami objednávky, ale objevily se další zásadní nevýhody. Třída objednávka je zatížena zbytečnou znalostí rozhraní (konstruktoru) třídy OrderItem a při změně třídy OrderItem, jakými jsou přidání nebo odebrání vlastnosti, budeme muset provést i změny ve třídě Order. Při tomto řešení jsme se právě vydali na strastiplnou a rozháranou životní cestu, nazývanou moderními mudrci ze softwarových pousteven ve svých písemných testamentech "Maintenance Nightmare". Stejný problém budeme mít při plnění vlastností OrderItem, navíc bychom u každé vlastnosti museli mít i set přístupovou metodu, z čehož by se měl všem autokritickým vývojářům obracet žaludek.
- Třídě OrderItem přidáme konstruktor, který přijímá záznam z databáze - v případě .Net Frameworku tedy objekt DataRow. Tohle řešení sejme povinnost nést břemeno znalosti rozhraní třídy OrderItem z třídy Order, ale zanechá nás s třídami, jejichž veřejné (nebo minimálně "internal") rozhraní je zapráskáno objekty z nižších vrstev. Proč by třída měla ukazovat, že je závislá na objektech DataRow, jejichž hlavní doménou je databázová vrstva? Perzistence je u business objektů nutné zlo, ne alfou a omegou a hlavním důvodem jejich existence. Bohužel, i v mnoha složitých projektech jsou stále k vidění jen truchlivé krabičky s daty z databáze a metodami Load a Save doplněné o honosnou etiketu "business vrstva" od nějakého rekvalifikovaného outsidera s kriplovsky laděným elánem Horsta Fuchse, co se v pomatení smyslů rozhodl pomoci saturovat deficit pracovních sil na trhu s vývojáři.
Než přejdu ke cachování dat, jen drobná poznámka, abych předešel dotazům. Metoda loadItems může poslat dotaz, který vrátí jen id položek objednávky, takže se data na klienta netahají dvakrát a můj problém pak působí jen jako vykonstruovaná obsese. I v tomte případě se ale místo jednoho dotazu budete potýkat s desítkami dotazů, které vyzvedávají - většinou zcela zbytečně - data jen pro jednu instanci OrderItem. Jestliže mám k tomu příležitost, je lepší vyzvednou všechna data pro všechny objekty najednou jedním "úsporným" dotazem a přitom dovolit v případě potřeby objektu přímé obnovení dat z databáze svým separátním dotazem.
Když tedy není pro nás přijatelný konstruktor s objektem DataRow, musíme při vytváření objektu "OrderItem" sdělit, odkud může načíst svá data. Jinými slovy, objekt OrderItem musí vědět, kam mu třída Order "schovala" objekt DataRow. Proč bychom ale neefektivně každému objektu extra sdělovali, odkud může načíst svá data? Zavedeme raději jeden centrální objekt - cache na příchozí data z databáze. Pak stačí objektu OrderItem předávat jen Id jako v první variantě a přitom využívat všech výhod cachování dat.
Co od takové datové cache požadovat?
- Třída musí být globálně viditelná pro všechny objekty. Půjde tedy o Singleton, respektive pro webové aplikace bude vhodné použít PseudoSingleton (Thread Specific Storage).
- Rozhraní, které se nemění s přidáváním dalších business tříd a které mohou používat jednotným způsobem všechny objekty. Takže chceme univerzální metody pro ukládání i vyzvedávání dat jakéhokoli objektu, ne nějaké stále bobtnající rozhraní s metodami StoreOrdeItem, StoreOrder, StoreXX, StoreXY, ... .
Zde je jednoduchá implementace třídy, která zatím splňuje naše nároky. Zobrazit kód
Myslím, že kód je sám o sobě dostatečně vypovídající, takže jen stručný komentář. Statický atribut Instance vrátí pro každý thread specifickou instanci třídy, která se pro jeden thread chová jako běžný Singleton. Metoda CloseInstance odstraní instanci třídy pro daný thread a tuto metodu je vhodné volat ve webové aplikaci na konci každého požadavku v obsluze události EndRequest v souboru global.asax. Metoda StoreData uschová předaný datový řádek (argument row) pro objekt daného typu (argument type) - metoda předpokládá, že řádek obsahuje sloupec Id, který je jednoznačným identifikátorem každého business objektu. Metoda GetData vrátí dříve uložený řádek pro typ v argumentu type a pro objekt, jehož id bylo předáno v argumentu businessObjectId. Po vrácení dat je uložený řádek odstraněn, aby nebyly vlastnosti business objektu permanentně obnovovány z dříve uložených a již neaktuálních dat.
Přístě si ukážeme, jak si u business objektů vynutit používání třídy DataCacheHelper bez duplikace kódu a rizika, že zapomenete v nově přidané třídě řádky z instance DataCacheHelper vyzvedávat, a také postupně z DataCacheHelperu vydělíme různé aspekty jeho chování, abychom umožnili rekonfiguraci DataCacheHelperu za běhu aplikace a adekvátně vyladili jeho činnost pro různé typy aplikací.
Sunday, 07 August 2005 19:26:34 (Central Europe Standard Time, UTC+01:00)
.NET Framework | Návrhové vzory
Saturday, 30 July 2005
Řešení hádanky "Znáte dobře návrhové vzory"
Protože nikdo nedodal kompletní řešení k hádance z 26.7., zde je odpověď.
- Třída MessageReceiverBase - událost MessageReceived, návrhový vzor Observer.
- Třída MessageReceiverBase -metoda CreateMessage, návrhový vzor Factory Method. Metoda CreateMessage vrací přímo instancí třídy Message nebo její potomky. Potomci třídy MessageReceiverBase mohou metodu přepsat a vrátit z metody například instanci OrderMessage, aniž by byly dotčeny vysokoúrovňové scénaře pracující pouze s rozhraním Message a deklarované na úrovni MessageReceiverBase.
- Třídy MessageReceivePoint, MessageReceiverBase a její potomky můžeme považovat za participanty návrhového vzoru Bridge. Třída MessageReceivePoint je abstrakcí - logickým přístupovým bodem, který dokáže přijímat data na daném URI a který interně využívá konkrétní fyzický přístupový bod, jímž je potomek třídy MessageReceiverBase zapouzdřující detaily komunikace po zvoleném přenosovém protokolu. Metoda Listen třídy MessageReceivePoint deleguje volání na metodu Listen třídy MessageReceiverBase. Samozřejmě lze o potomcích třídy MessageReceiveBase uvaživat i jako o strategiích, jak zaznělo v diskuzi o spotu, i když si myslím, že vzor Bridge lépe vyjadřuje role tříd.
- Třída AddMesageAttributesProcessor - jak vyjadřuje její název, jedná se o realizátora vzoru Content Enricher - k přijaté zprávě dodává další informace, které nebyly přímo její součástí ale které jsou důležité pro další zpracování zprávy. Příklad - messagingový systém přijme objednávku a Content Enricher ke zprávě doplní údaje o platební morálce zákazníka.
- Třída FilterMessageAttributesProcessor - opět dle názvu můžete usuzovat, že jde o návrhový vzor Content Filter. Procesor z přijaté zprávy odstraní všechny informace, které nejsou důležité pro další zpracování a které by pouze zbytečně vytěžovaly zdroje serveru. Příklad - messagingový systém přijme obrázové přílohy, které není třeba posílat k dalšímu zpracování, ale pouze se archivují v DMS, takže nemá smysl hnát obrázky celým procesem vyřizování objednávky. Content Filter obrázky ze zprávy před jejím dalším zpracováním odstraní.
Jak někteří z vás (Petr :) ) správně vytušili, diagram také svádí k tomu, aby byl rozšířen o Intercepting filter nebo o vzor Pipes&Filters - tyto vzory v něm ale v současné podobě nalezneme poze jako latentní možnosti, které můžeme uskutečnit doplněním a úpravou vztahů mezi existujícími třídami.
Saturday, 30 July 2005 17:22:25 (Central Europe Standard Time, UTC+01:00)
Návrhové vzory | Programátorské hádanky
Wednesday, 13 April 2005
Sunday, 03 April 2005
Prezentace z .NET Developer Group o UML ke stažení
Na čtvrteční přednášce o UML jsem do svého rezervoáru životních paradoxů přidal pár dalších :)
1) I když sebekriticky zredukuju prezentaci o 150 slidech na 70, poté se intenzivní a zostřenou psychoanalytickou rychloseancí, na které by i těžkotonážní barokně rozsochatá lady Halina Pawlowská prodělala akutní záchvat mentální anorexie, jenž by byl excelentně konvertibilní na finační částku za nějakou další a zatraceně rychle natočenou reklamu na hubnutí s bonusem ve formě nezbytných JoJo baculatých efektů pro frustrované paničky, které jako ona chytře snídají dietní Colu s čokoládovými tyčinkami. :), vypořádám s pocity viny, jejichž zdrojem je obsedantní představa, že svatokrádežně zjednodušuju svá milovaná témata hodná nejvyšší úcty, a přesto nenásleduje kýžená odměna, jíž měl suplovat fakt, že jsem ve vyhrazených 3 hodinách vykleštěné téma alespoň stihl probrat. ;)
2) Je zvláštní přijít tak rozpolcený z přednášky na výsostné půdě Microsoftu, protože jsem navzdory svému obdivu vůči mnoha MS produktům musel po dotazech posluchačů doporučovat modelovací nástroje od jiných firem a dát najevo svoji vytříbenou nechuť k "vytváření návrhů" ve Visiu a také si opět otravně povzdychnout nad tím, že Visual Studio Team System, MSF a UML nejsou a nebudou nerozlučným triem - killerem konkurenčních nástrojů pro návrh a vývoj aplikací. ;)
Zájemci si mohou stáhnout prezentaci, i když na návrhové vzory nezbyl ve čtvrtek čas a budou v rozšířené podobě tématem až nějaké další .Net Developer Group.
Sunday, 03 April 2005 18:28:00 (Central Europe Standard Time, UTC+01:00)
Návrhové vzory | UML
Sunday, 26 September 2004
Logování událostí v mediátoru
V mediátoru, kterého jsem popisoval v jednom dřívějším spotu, potřebujeme při testování logovat všechny příchozí události, které jsou předdefinovaným a bezpečným způsobem distribuovány dalším objektům. Nejméně náročnou cestou na napsání, volenou většinou vývojáři, kteří se pravděpodobně do IT rekrutují z rekvalifikovaných potomků nejzarytějších stachanovců;), je logování každé události zvláštní metodou. Po registaci nových událostí do mediátora je vždy i změněna komponenta pro logování, takže o údržbě takového kódu si můžete nechat zdát jen mrazivé sny o opětovném plíživém nahrazování kvality kvantitou.*
Na údržbu nenáročné a univerzální řešení spočívá ve vytvoření preprocesoru (viz spot o mediátoru), který je vyvolán před distribucí událostí ostatním objektům a který parametry události vypíše přes objekt Trace z jmenného prostoru System.Diagnostics.
Základní myšlenky v preprocesoru.
1) Většina (ne li každý) objekt v business vrstvě má unikátní identifikátor s názvem Id. Preprocesor události vypíše typ objektu, který událost vyvolal a jeho unikátní Id, takže je snadné dohledat zdroj problematických událostí.
2) Objekty nesoucí parametry událostí dědí vždy z třídy EventArgs. Přes reflection vypíšeme rekurzívně hodnoty vlastností do úrovně nastavené ve vlastnosti EndDumpLevel.
using System;
using System.Diagnostics;
using System.Reflection;
namespace RStein.Utilities
{
/// <summary>
/// Třída EventsLog pro logování událostí
/// </summary>
public class EventsLog
{
#region private constants
private const int BEGIN_DUMP_LEVEL = 1;
private const int DEFAULT_END_DUMP_LEVEL = 2;
#endregion private constants
#region private variables
private int m_endLevel;
#endregion private variables
#region Constructors
/// <summary>
/// Konstruktor
/// </summary>
public EventsLog() : base()
{
m_endLevel = DEFAULT_END_DUMP_LEVEL;
}
#endregion Constructors
#region Public properties
/// <summary>
/// Koncová úroveň rekurzivního výpisu vlastností objeudálosti
/// </summary>
public int EndDumpLevel
{
get
{
return m_endLevel;
}
set
{
if (value < 1)
throw new ArgumentOutOfRangeException("Value must be greater than 1");
m_endLevel = value;
}
}
#endregion Public properties
#region Public methods
/// <summary>
/// Metoda pro výpis informace o událostech
/// </summary>
/// <param name="sender">Zdroj události</param>
/// <param name="e">Parametry události</param>
public void ProcessEvent(object sender, EventArgs e)
{
Debug.Assert((sender != null) && (e != null), "EventsLog - Invalid event data");
if ((sender == null)||
(e == null))
{
return;
}
Trace.WriteLine(new string('-',25));
dumpId(sender);
dumpEventProperties(e);
}
#endregion Public methods
#region Private methods
//Zapsání Id a názvu objektu
private void dumpId (object sender)
{
Type senderType = sender.GetType();
PropertyInfo idInfo = senderType.GetProperty("Id");
string id = String.Empty;
if (idInfo != null)
id = idInfo.GetValue(sender, null).ToString();
Trace.WriteLine(String.Format("Přijata událost od objektu {0}, Id = {1}", senderType.ToString(), id));
}
//Vypsani vlastností objektu s parametry události
private void dumpEventProperties(EventArgs e)
{
Type eventType = e.GetType();
Trace.WriteLine(String.Format("Objekt události - {0}", eventType.ToString()));
PropertyInfo[] eventInfo = eventType.GetProperties();
Trace.WriteLine("Výpis hodnot událostí");
writeEventValues(eventInfo, BEGIN_DUMP_LEVEL, e);
}
//Metoda pro rekurzivní výpis hodnot vlatností
private void writeEventValues(PropertyInfo[] eventInfo, int level, object o)
{
foreach (PropertyInfo info in eventInfo)
{
//Nezpracovávat indexované vlastnosti
if (info.GetIndexParameters().Length != 0)
continue;
object propValue = info.GetValue(o, null);
if (propValue == null)
continue;
Trace.Write(new String('\t', level));
Trace.WriteLine(String.Format("Jméno - {0}, Hodnota = {1}", info.Name, propValue.ToString()));
if (level < EndDumpLevel)
writeEventValues(propValue.GetType().GetProperties(), level + 1, propValue);
}
}
#endregion Private methods
}
}
*Pozorný čtenář z tohoto spotu dokáže vyčíst i mou politickou orientaci a nechuť k určitému druhu myšlení;)
Sunday, 26 September 2004 17:02:00 (Central Europe Standard Time, UTC+01:00)
.NET Framework | Návrhové vzory
Sunday, 06 June 2004
Synchronizace stavových automatů. Rozumíte návrhovému vzoru Mediator?
Návrhový vzor Mediator zamezuje těsným vztahům mezi objekty zavedením prostředníka (mediatora), který jejich interakci zapouzdří. Tak praví strohá, ale výstižná definice. Řečeno jinak, Mediator snižuje přímé i nepřímé zatížení třídy jinými třídami.
Vzor Mediator je většinou vykládán na příkladu interakce komponent v uživatelském rozhraní, které spolu komunikují přes prostředníka, jímž je formulář. Listbox se seznamem zemí neví nic o Listboxu se sezamem měst - po výběru jiné země je vyvolána událostní procedura formuláře, v níž je kód pro naplnění druhého Listboxu městy ve vybrané zemi. Každý ovládací prvek spolupracuje jen s formulářem, který notifikuje o změně ve svých vlastnostech, a na formuláří je kód propagující změny do dalších prvků. Na způsobu chování nezáleží, formulář může být u prvku zaregistrován jako Observer (v terminologii jazyka JAVA Listener), jehož metoda je zavolána po změně stavu prvku, nebo v .NET zaregistrujeme u ovládacího prvku delegáta s obslužnou metodou, jejíž signatura se shoduje se signaturou události. Komplexního chování prezentační vrstvy není dosaženo přímou spoluprací mnoha objektů, ale zavedením inteligentní mezivrstvy, která je recipientem heterogenních notifikačních zpráv objektů a současně autorem a distributorem nových aktualizačních zpráv pro další objekty, jež po jejich přijetí změní svůj stav a přitom mohou prostředníkovi odeslat další notifikační zprávy. Zdůraznil bych, že prostředník je autorem zpráv, takže informaci o notifikaci nemusí jen přeposlat, ale také dokáže argumenty původní notifikační zprávy před odesláním kreativně pozměnit, filtrovat, anebo dokonce rozešle události v jiném pořadí. Jak uvidíme, všechny tyto charakteristiky jsou klíčové pro mediatora koordinujícího synchronizaci stavových automatů business objektů.
Kdyby ovládací prvky mezi sebou komunikovaly přímo, tak i jednoduchá změna chování prezentační vrstvy roztočí spirálu sisyfovského dohledávání a ošetřování dopadů změny v kódu všech prvků.
Použití vzoru Mediator ale není omezeno na prezentační vrstvu a jeho kvality se plně projeví až při řízení složité interakce stavových automatů tříd v business vrstvě.
Představte si systém pro nábor zaměstnanců na různé pozice. V systému jsou evidovány nabízené pracovní pozice a uchazeči. Když uchazeč projeví zájem o pozici, je vytvořen nový objekt pro relaci mezi uchazečem a pozicí, v němž jsou uložena data o průběhu procesu výběru jednoho uchazeče. Platí pravidlo, že v jednom okamžiku může být Uchazeč přiřazen jen k jedné Pozici. Pozice, Uchazeč i Relace mají své stavové diagramy. Uveďme si příklady stavů.
Pozice může být ve stavu:
Nová - Zahájení životního cyklu pracovní pozice.
Přiřazen alespoň jeden uchazeč – O pozici se zajímá alespoň jeden uchazeč. Tento stav je důležitý pro manažery, kteří jsou odpovědni za obsazení pozice a zajímá je, zda například týden po vypsání místa je o pozici zájem, nebo je třeba investovat více peněz do propagace.
Přijat alespoň jeden uchazeč – K pozici se může vázat více volných míst, tento stav manažery náboru informuje, že již byl vybrán alespoň jeden vhodný zaměstnanec.
Obsazená – Pozice byla obsazena. Počet nabízených míst je roven počtu přijatých uchazečů.
Zrušená – Pozice byla zrušena. Pozici je možné kdykoli zrušit, což značí, že k ní již není možné dále přiřadit uchazeče a přiřazení uchazeči, kteří ještě nebyli přijati, jsou ihned odmítnuti.
Stavy uchazeče
Nový – Založení uchazeče.
Zajímá se o pozici –Uchazeč byl přiřazen k pozici a prochází různými zkouškami. Nelze mu přiřadit další pozici.
Přijat – Uchazeč byl přijat na pozici. Pozice musí být notifikována o přijetí uchazeče.
Odmítnut – Uchazeč byl odmítnut, to znamená, že je ho možné přiřadit k jiné pozici.
Propuštěn – Uchazeč (zaměstnanec) byl propuštěn. Opět ho je možné přiřadit k jiné pozici.
Zrušen – Uchazeč byl zrušen. Zrušit lze jen uchazeče ve stavu Nový, Odmítnutý a Propuštěný. Je nutné notifikovat aktivní relaci mezi Uchazečem a Pozicí.
Stavy relace mezi uchazečem a pozicí
Nová – Nová relace mezi uchazečem a pozicí založena. Je nutné notifikovat pozici i uchazeče, že byla mezi nimi vytvořena relace.
Uchazeč prošel psychotesty – Uchazeč úspěšně prošel psychotesty.
Uchazeč odmítnut po psychotestech –Uchazeč neprošel psychotesty, notifikovat pozici a uchazeče o odmítnutí.
Uchazeč splnil všechny požadavky – Notifikovat pozici o přijetí dalšího uchazeče, notifikovat uchazeče.
Zrušena – Došlo ke zrušení relace, důvodem může být to, že uchazeč o pozici ztrati zájem nebo je zrušena pozice. Notifikovat uchazeče a pozici o zrušení.
Jde sice jen o reprezentativní množinu tříd a jejich stavů, ale i mezi takto malým počtem tříd existuje poměrně složitá spolupráce.
Například při zrušení pozice musejí být notifikovány všechny aktivní Relace, ty přejdou do stavu zrušena . Je notifikován Uchazeč, že byl odmítnut. Relace zpětně notifikuje o svém zrušení i Pozici, protože důvodem zrušení může být jindy ztráta zájmu ze strany Uchazeče.
Další příklad. Když relace přejde do stavu Uchazeč splnil všechny požadavky, tak musí být notifikována pozice, u níž dojde ke změně stavu jen tehdy, když se jedná o prvního přiřazeného uchazeče. Dále je notifikován uchazeč, který přejde do stavu Přijat.
Představme si nyní, že třídy Uchazeč, Pracovní pozice a Relace informaci o změně svého stavu nabídnou okolí ve formě událostí. Každý objekt z jedné třídy si přihlásí odběr událostí asociovaných objektů z dalších tříd a pak všichni spustí divokou událostní přestřelku se zběsilými kulkami v podobě vzájemných notifikací.
Když Pozice přejde do stavu zrušena, informuje první relaci, ta změní svůj stav na zrušena, relace svoji změnu stavu zašle uchazeči, ten přejde do stavu „Odmítnut“ a vyvolá svoji událost „Změna stavu“, tu odchytí Relace, která na stav „Odmítnut“ u uchazeče nereaguje. Relace informaci o svém zrušení zasílá vždy recipročně objektu Pozice. Pozice pod dokončení kolečka s první Relací notifikuje další Relaci a kolečko s mírně obměněnými aktéry proběhne znovu. Všechny objekty jsou při tomto vzájemném přeřvávání kandidáty na slušivou svěrací kazajku.
V čem je ale hlavní problém? Události nemusí být zpracovány ve správném pořadí. Představme si, že u Pozice z nějakého důvodu zavedeme další tranzientní stavy „Vyžádáno zrušení pozice“, „Pozice je rušena“ a teprve finálním stavem je náš původní stav „Zrušena“. Do stavu „Vyžádáno zrušení pozice“ přejde pozice ihned po žádosti uživatelem (přesněji po volání metody Cancel), do stavu „Pozice je rušena“ , když je zpětně notifikována první Relací o jejím přechodu do stavu Zrušena. Pozice přejde do finálního stavu „Zrušena“ až po obdržení potvrzení o svém zrušení od všech dalších relací. Tady ale narazíme – když Pozice přejde do stavu „Pozice je rušena“, tak začne tuto událost rozesílat všem Relacím.
To znamená, že Relace2 a následující obdrží nejdříve informaci o přechodu Pozice do stavu „„Pozice je rušena“ a později o ( již nepravdivém) přechodu do stavu „Vyžádáno zrušení pozice“. Když se zkomplikují i stavové automaty dalších objektů, staneme se nechtěnými svědky rozhovoru bandy otrlých lhářů.:)
Pořadí při rozesílání událostí není sekvenční, a i když bychom mohli do objektu Pozice a dalších dopsat logiku, která se o sekvenční distribuci postará, nepůjde o šťastné řešení. Aby třída konvenovala svému okolí, dosadíme do ní znalost, jak si okolí komunikací představuje, a porušíme tak princip zapouzdření. Třída svému okolí sděluje, jaké změny v ní nastaly, ale nesmí se starat o reakce okolí. Třída musí být vždy nonkonformní a kašlat na své sousedy, jež ji nutí, aby se adaptovala na jejich způsob komunikace a chování. (BTW: Zjsitil jsem, že miluji OOP a návrhové vzory, protože k sousedům mám stejný postoj :) )
Odpovědnost za sekvenční rozesílání událostí přidělíme nové třídě Mediator. Po svém vytvoření budou objekty zaregistrovány v Mediatoru, který si přihlásí odběr jejich událostí. Třídám Pozice, Relace a Uchazeč přidáme metody, které budou volány mediátorem, když nastane událost. Například objekt Pozice bude mít metodu „NotifikujUchazečOdmítnut“, která bude mediátorem zavolána, když Relace přejde do stavu „Uchazeč odmítnut po psychotestech“ nebo do stavu „Zrušena“.
Aby mediátor nebyl příliš komplikovaný a nepřehledný, vytvoříme pro každou třídu jednoho zpracovatele události. Každý zpracovatel událost bude implementovat rozhraní s jedinou metodou „ZpracujUdálost“, které přijímá dva argumenty. Prvním argumentem je odkaz na objekt, který událost vyvolal, a druhým jsou parametry události. Zpracovatel „ví“, jaké metody musí u objektů volat pro každou jedinečnou kombinaci argumentů události. (Když zpracovatel objektu Relace obdrží událost „Přechod do stavu Zrušena“, tak zavolá u asociovaného objektu Pozice metodu NotifikujUchazečOdmítnut“ a u Uchazeče metodu „ZůstávášNezaměstnaným“:) )
Jak zajistíme sekvenční zpracování?
V událostních metodách mediátor odchytne událost a zavolá vždy jen obecnou metodu handleEvent, které kromě odesílatele a argumentů události přijímá odkaz na zpracovatele (m_JobHandler), jenž reaguje na danou událost. Objekt m_JobHandler zpracovává událost „StavZměněn“ třídy Pracovní pozice.
handleEvent(sender, e, m_JobHandler);
Metoda handleEvent
private void handleEvent(object sender, EventArgs e, BProcessEventHandlerBase processor)
{
EventStateHolder holder = new EventStateHolder(e, sender, processor);
m_eventsQueue.Enqueue(holder);
if (!m_inEventBubbling)
initEventBubbling();
}
Metoda handleEvent nejdříve uloží všechny informace o události do nové instance naší třídyEventStateHolder. Zde mimochodem vidíte kouzlo polymorfismu, protože naše třída EventStateHolder se nestará o detaily událostí, ale vyžaduje jen, aby argumenty události byly odvozeny z třídy EventArgs a zpracovatelé událostí z třídy (rozhraní) BProcessEventHandlerBase. Poté je událost zařazena do fronty (v .NET třída Queue) a jestliže se jedná o první událost, je zavolána metoda initEventBubbling, která se postará o předání události do zpracovatele. Tento kód je důležitý – bublání událostí je zahájeno jen při první události, všechny další události libovolných business objektů, které jsou přímou nebo nepřímou reakcí na první událost, jsou jen zařazeny do fronty a k novému spuštění distribuce událostí realizovaného metodou initEventBubbling nesmí dojít.
private void initEventBubbling()
{
Debug.Assert(m_eventsQueue.Count > 0, "BMediator events queue is empty!");
m_inEventBubbling = true;
while (m_eventsQueue.Count > 0)
{
EventStateHolder eh = (EventStateHolder) m_eventsQueue.Dequeue();
eh.Processor.ProcessEvent(eh.Sender, eh.EventArguments);
}
m_inEventBubbling = false;
}
V metodě initEventBubbling se nejdříve ujistíme, že fronta událostí není prázdná. Pak nastavíme privátní proměnnou m_inEventBubbling na true, aby metoda handleEvent již při zpracovávání návazných událostí metodu initEventBubbling nevolala. V jednoduchém cyklu while zpracujeme sekvenčně všechny události postupně přidávané do fronty po rozeslání každé události.
Mediatora můžete vylepšit. Ke každé události mohou být registrovány preprocesory, kteří například zalogují všechny události a postprocesory, odesílající manažerovi informaci o provedených změnách. Když budeme chtít rychle změnit chování aplikace bez zásahu do existujícího kódu, můžeme napsat nový preprocesor, který událost zpracuje zcela jinak, a protože z metody vrátí true (nepokračovat v distribuci událostí, již jsem vše udělal), tak se originální zpracovatel události nedostane ke slovu. Preprocesory načteme dynamicky při startu aplikace s využitím reflection API.
private void initEventBubbling()
{
Debug.Assert(m_eventsQueue.Count > 0, "BMediator events queue is empty!");
m_inEventBubbling = true;
while (m_eventsQueue.Count > 0)
{
EventStateHolder eh = (EventStateHolder) m_eventsQueue.Dequeue();
bool continueEventBubbling = distributeEventToPrePostProcessors(eh.Sender, eh.EventArguments, m_preProcessors);
if (continueEventBubbling)
{
eh.Processor.ProcessEvent(eh.Sender, eh.EventArguments); distributeEventToPrePostProcessors(eh.Sender, eh.EventArguments, m_postProcessors);
}
}
m_inEventBubbling = false;
}
private bool distributeEventToPrePostProcessors(object sender, EventArgs e, ProcessorCollection processors)
{
bool result = true;
foreach (IProcessor processor in processors)
{
if ((processor.ProcessEvent(sender, e)))
{
result = false;
break;
}
}
return result;
}
Také můžete přes mediátora řídit distribuci událostí transakčně s potvrzováním a vracením jednotlivých kroků a ukládat změny ve všech participujících objektech na konci úspěšné transakce do databáze.
Vzor Mediator je nepominutelnou dominantou každého návrhu aplikace, jež vyžaduje komplexní komunikaci mezi stavovými automaty různých tříd.
Sunday, 06 June 2004 19:53:00 (Central Europe Standard Time, UTC+01:00)
Analytické drobky | Návrhové vzory | UML