Události (events) nemusí být jen záležitostí uživatelského rozhraní, ale mohou najít uplatnění i v ostatních částích aplikace, i když pak jsou události jinak technicky provedené.
Vezměte si např. situaci, kdy se uživatel zaregistruje do naší aplikace. Co je třeba udělat? Někam tuto informaci persistovat a odeslat na zadaný e-mail ověřovací kód:
public class RegistrationService : IRegistrationService { public void RegisterUser(UserToRegister user) { Repository.Add(user); MailingService.SendConfirmation(user); } }
Co když budeme chtít na tuto událost reagovat ještě nějak jinak? Třeba budeme chtít zobrazovat seznam posledních deseti registrovaných uživatelů, stranou si štosovat uživatele, co použili jako registrační mail z GMailu atd.
Kdybychom přidali třeba jen další dvě volání nějakých dalších Services, začal by nám kód pěkně smrdět, protože by třída RegistrationService měla mnoho závislostí. Jak z toho ven?
Nadefinujme si tato dvě velmi jednoduchá rozhraní:
public interface IEventPublisher { void Publish(object e); } public interface IEventHandler<TEvent> { void Handle(TEvent e); }
Rozhraní IEventPublisher bude používat třída, která vyvolává nějakou událost. V našem příkladu bude mít třída RegistrationService závislost na tomto rozhraní, která při použití constructor injection vypadá nějak takto:
public RegistrationService(IRepository repository, IEventPublisher eventPublisher) { // save parameters to the properties or fields Repository = repository; EventPublisher = eventPublisher; }
V metodě RegisterUser pak vykonáme jen „primární“ záležitosti (uložení do databáze) a posléze vyvoláme novou událost pomocí volání EventPublisher.Publish(new UserRegistered(user));. Třída UserRegistered reprezentuje událost a je to obyčejná POCO třída.
Každý, kdo chce být informován o události UserRegistered, pak musí implementovat druhé definované rozhraní – IEventHandler<UserRegistered>. Tzn. v metodě Handle pak máme kód, který reaguje na vyvolání události.
public class MailingService : IMailingService, IEventHandler<UserRegistered> { public void Handle(UserRegistered e) { // send the confirmation e-mail } }
Tím docílíme pěkného oddělení zdroje události od konzumentů události.
Implementace
Samozřejmě musíme nějak zajistit, že po volání Publish dojde k zavolání těch správných event-handlerů. Jednoduchá implementace IEventPublisher založená na kontejneru může vypadat nějak takto:
public class SimpleEventPublisher : IEventPublisher { public void Publish(object e) { var messageType = e.GetType(); var eventHandlerType = typeof(IEventHandler<>).MakeGenericType(messageType); foreach(var eh in Container.ResolveAll(eventHandlerType)) // we must register all event handlers somewhere { eh.Handle(e); } } }
Tato implementace je velmi naivní, neefektivní a především nejde zkompilovat, protože iterovaná proměnná nebude odpovídajícího generického typu IEventHandler<>. To je ale jen implementační detail, který jde snadno vyřešit pomocí trošky reflexe…
Princip všech implementací IEventPublisher bude ale stejný – odněkud zjistit všechny event-handlery pro daný typ události a pro všechny zavolat metodu Handle. Jednoduché. Ale velmi silné! Jsme totiž schováni za velmi jednoduchým rozhraním a můžeme dělat, co je nám libo.
Např. můžeme každé volání metody Handle obalit try-catchem a tak zajistit, že dojde vždy k zavolání všech event-handlerů.
Nemusíme ani event-handlery volat synchronně – můžeme si událost uložit do nějaké fronty (Message Queue?) a event-handlery volat až později (a zbytečně neblokovat) nebo je třeba zpracovávat na úplně jiném stroji.
Můžeme si event-publisher nakonfigurovat tak, že některé události bude odbavovat synchronně, jiné asynchronně.
Můžeme ale změnit i logiku vyhledávání event-handlerů. Můžeme začít třeba podporovat „dědičnost událostí“, takže IEventHandler<object> by reagoval na všechny události.
A v neposlední řadě můžeme celé toto delegovat na nějaký existující messagingový framework.
Prostě fantazii se meze nekladou! 🙂
Použití
Nemá cenu cpát tento koncept do malých nebo jednoduchých aplikací – ideální použití událostí je v komplexnějších aplikacích, kde je hodně závislostí mezi různými částmi aplikace.
Je dokonce možné si nakonfigurovat event-publisher tak, aby se některé vybrané události publikovali i mimo naši aplikaci a informovali o změně stavu nějaké návazné aplikace. Pak přichází ke slovu message bus, což je zjednodušeně řečeno one-way (pub/sub) komunikace mezi aplikacemi.
Zajímavé může být použití konceptu událostí v datové vrstvě. Každý jistě zná triggery z databází – můžete si zaregistrovat funkci, která se při vložení/změně/smazání dat automagicky zavolá. Když ale používáme úložiště, které triggery nepodporuje, nebo máme více úložišť, která mezi sebou neumí na této úrovni spolupracovat, pak potřebujeme posunout triggery o úroveň výše, tj. do našeho kódu.
Události takového typu asi nebudeme chtít vyvolávat ručně, ale budeme to chtít nějak zautomatizovat. Můžeme si třeba napsat aspekt (~wrapper), který zajistí automatické vyvolávání událostí, když se volají metody Add/Remove na Repozitáři.
V praxi to pak může vypadat třeba tak, že ačkoliv máme data uložena v MySql, tak si můžeme napsat event-handler pro událost ProductRemoved, ve kterém invalidujeme položku v Memcached.
Perzistence založená čistě na událostech se nazývá EventSourcing, ale to osobně považuji za extrém, který najde využití u velmi malého počtu projektů.
Takže?
Nadefinovali jsme si dvě rozhraní, která budeme používat v naší aplikaci jako takové. Díky tomu si nazabordelíme projekt referencemi na nějaký specifický framework a můžeme kdykoliv velmi jednoduše změnit implementaci/chování událostí.
Události najdou uplatnění jak v aplikaci jako takové („doméně“), tak na úrovni přístupu k datům. Pokud bychom dotáhli použití událostí do konce, dostali bychom EventSourcing, což je ale dle mého názoru u většiny projektů zbytečné.
Události ale jistě stojí za pozornost minimálně kvůli tomu, že dokáží snížit provázanost jednotlivých částí aplikace.
http://alexgorbatchev.com/pub/sh/current/scripts/shCore.js
http://alexgorbatchev.com/pub/sh/current/scripts/shBrushCSharp.js
if (SyntaxHighlighter) {
SyntaxHighlighter.all();
}
Raději než definovat vlastní rozhraní je lepší použít již existující. 😉 Viz. http://rarous.net/weblog/403-lepsi-udalosti-v-csharp.aspx
To se mi líbíTo se mi líbí
Noo ale kdybych použil místo mého IEventHandler standardní IObserver, tak bych musel implementovat všechny 3 metody, což nechci.
To se mi líbíTo se mi líbí
Dalšími výhodami event agregátoru mohou být v závislosti na implementaci například weak reference na jednotlivé subscribery nebo volání handleru v určitém specifickém kontextu (např. na UI vlákně). Líbí se mi implementace v Caliburn.Micro – bez vazby na cokoliv jiného v tomto frameworku, úsporná, jednoduchá a přesto efektivní.
Zároveň ale tento vzor narozdíl od klasických událostí některé věci zakrývá. Jen z pohledu na rozhraní třídy nevidím, které zprávy odesílá. (U implementací, které namísto rozhraní á la IEventHandler spoléhají na delegáty, je pak zakryto i to, které zprávy jsou přijímány.) Je to logické, ale je třeba s tím při návrhu počítat a podle mě vynahradit tento nedostatek u větších projektů nějakou formou dokumentace.
To se mi líbíTo se mi líbí
>> Jen z pohledu na rozhraní třídy nevidím, které zprávy odesílá.
Tohle máme vyřešené tak, že když třída chce odesílat zprávy, tak si nenechává injectovat obecné IEventPublisher, ale konkrétní generické IEventPublisher<TEvent>.
To se mi líbíTo se mi líbí