Užitečné události

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();
}

4 thoughts on “Užitečné události

  1. 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í

  2. >> 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í

Zanechat odpověď

Vyplňte detaily níže nebo klikněte na ikonu pro přihlášení:

Logo WordPress.com

Komentujete pomocí vašeho WordPress.com účtu. Odhlásit /  Změnit )

Facebook photo

Komentujete pomocí vašeho Facebook účtu. Odhlásit /  Změnit )

Připojování k %s

Tento web používá Akismet na redukci spamu. Zjistěte více o tom, jak jsou data z komentářů zpracovávána.