Co jsou doménové eventy a proč je používat
Doménové eventy jsou velice silný koncept, který nám pomáhá s dekompozicí kódu, synchronizací s externími systémy, či auditováním. S tímto konceptem se setkáte nejčastěji v systémech, kde se při implementaci používají principy Domain Driven Design. Řada z vás se jistě s nějakou formou eventu setkala při použití Symfony, Doctrine, či Nette formulářů. V čem jsou tedy doménové eventy jiné?
Co je to doména
Doména je nějaká oblast zájmu nebo činnosti. Jako příklad může být zdravotnictví, hoteliérství, logistika a mnoho dalších. Každá tato oblast obsahuje celou řadu činností, objektů či pravidel, které jsou pro ni typické.
Při uplatňování principů Domain Driven Design je zcela zásadní, aby software, který pro danou oblast píšeme, byl jejím obrazem. Aby software přesně reflektoval reálně existující objekty, procesy a jejich pojmenování. Bohužel se musí velice často business přizpůsobit tomu, jak funguje software, místo aby to bylo naopak.
Pro nás v Pecce je denní chleba e-commerce svět a někdo by si mohl myslet, že se jedná o jednu doménu pojmenovanou e-commerce. A to by byla šeredná chyba. Realita je taková, že e-commerce zahrnuje celou řadu domén. Pro příklad můžeme zmínit produktový katalog, správu obsahu, objednávku, řízení skladu, logistiku, marketing a mnoho dalších.
Každá oblast má svůj vlastní jazyk a terminologii, kterou používají lidé, kteří se v dané oblasti pohybují. Je velice časté, že se některé pojmy vyskytují ve vícero doménách. Ovšem kontext, ve kterém se vyskytují, je jiný, očekáváme od daných objektů jiné chování a mají jiné vlastnosti.
Říká se, že při programování je nejtěžší věci pojmenovat. To může být způsobeno například jmenným konfliktem. Vezměme si například produkt. Produkt se typicky vyskytuje v produktovém katalogu a objednávce. Je to ale pokaždé ten stejný pojem? Pokud s ním pracuji v katalogu, typicky mě zajímá jeho název, popis, obrázek, parametry a cena. Pokud jsem v objednávce, zajímá mě název, cena a množství. I přesto, že některé vlastnosti produktu jsou stejné, má každý produkt jiný kontext, jiné použití a jiná omezení.
Obecná poučka tvrdí, že pro každou doménu by měla být samostatná a oddělená aplikace. Tímto se předejde jmenným konfliktům a umělým názvům jako CatalogueProduct a BasketProduct. Tento přístup také pomáhá s lepším pochopením dané oblasti a umožňuje nám soustředit se na správné pojmenování.
Poslední příklad za všechny je mix uživatele a zákazníka. Uživatel je někdo, kdo používá nějaký software. Typicky chceme uživatele autentizovat, abychom mohli říct, o koho se jedná. K tomuto účelu se používá uživatelské jméno a heslo. Je tedy uživatel a zákazník opravdu ten jeden a ten stejný pojem? Dává smysl mít u zákazníka heslo? Je každý zákazník automaticky uživatel? Co když si objednává zboží přes telefon a nebo emailem?
Tolik slov o doméně a článek měl být o eventech – nebojte, už se k nim dostaneme. Snad se mi podařilo vysvětlit co je to doména, jak je pojmenování důležité a že se stejné pojmy mohou vyskytovat ve vícero doménách a přitom mají pokaždé jiný význam.
Co je to event
Než oba pojmy spojím a vysvětlím důvody, proč je dobré je chtít, řekněme si ještě něco o eventech, česky událostech. Event má tyto základní vlastnosti:
- má unikátní pojmenování, které je v minulém čase a vyjadřuje děj, který se již stal
- je neměnný – co se v minulosti stalo, nejde už změnit
- obsahuje přesné datum a čas vzniku
- nese užitečné informace pro jeho další zpracování a použití
Jak už bylo zmíněno na začátku, nejspíš jste se již setkali s eventy v souvislosti s nějakým frameworkem či ORM. V čem jsou tedy doménové eventy jiné?
Doménové eventy
Doménový event je nějaká významná událost, která se v našem systému děje. Tím, kdo určuje zda-li je daná událost významná či nikoliv, není programátor, ale doména a doménoví experti, kteří nám s modelováním systému pomáhají.
Představme si systém pro řízení skladu. Máme skladovou kartu pro produkt a s tím, jak přicházejí objednávky, se množství daného produktu postupně snižuje a s naskladněním zase zvyšuje. Při snížení zásoby o jeden kus bychom tedy mohli emitovat event StockChanged. Opravdu nás to ale zajímá? Možná nás zajímá pouze to, kdy se produkt vyprodal, protože pokud už nemáme žádný kus, chceme přepočítat jeho dostupnost. Nebo nás zajímá to, kdy se produkt znovu naskladnil, abychom mohli odeslat e-mailové notifikace zákazníkům, kteří si zapnuli hlídání dostupnosti daného produktu.
Modelování tedy záleží na způsobu použití a nelze říci, co je dobře a co špatně. To, co lze označit za podezřelé, jsou tzv. CRUD eventy typu ProductCreated, ProductUpdated, ProductDeleted. Je možné, že vám stav vašeho kódu neumožní namodelovat lepší eventy a i přesto může být vhodné a výhodné doménové eventy implementovat. Jedná se ovšem o zdvižený prst a rozhodně se nejedná o ideální podobu doménových eventů.
Typickým příkladem je anémický model, kde entity používáme pouze jako obálky na data a jedná se tak o shluk get/set metod. Takovému kódu schází vyjádření úmyslu a to se následně odráží i v možnostech modelování eventu.
Uvedu příklad. Zákazník si změnil doručovací adresu. V anémickém modelu se jedná o volání metod pro změnu ulice, města a PSČ. V plnohodnotném a bohatém modelu jde o jednu metodu changeDeliveryAddress. Tato metoda jasně vyjadřuje úmysl co se má stát. Přímo ve volání této metody pak můžeme umístit emitování události DeliveryAddressChanged. V případě anémického modelu bychom mohli umístit vyhazování události do každé set metody, ovšem řešení by bylo pracnější, méně elegantní a nakonec by zaplevelovalo historii události velkým množstvím nevýznamných událostí, na které by se nám velice špatně reagovalo.
Proč to stojí za tu námahu
Implementace doménových eventů má celou řadu výhod, které rozšiřují naše možnosti, jak můžeme systém modelovat, ale také mají celou řadu příjemných vedlejších efektů.
Monitorování a debug
Při práci s doménovými eventy pracujeme také s pojmem event store. Jedná se o úložiště všech událostí, které se v systému staly. Díky tomu můžeme vidět, co se v našem systému děje a při napojení na metriky také kontrolovat, zda-li je všechno v pořádku. Díky tomu, že má každá událost přesný čas vzniku, můžeme sledovat dění systému v souvislostech a pokud hledáme chybu, lépe pochopíme, co se stalo.
Synchronizace s dalšími systémy
Doménové eventy můžeme používat při komunikaci s dalšími systémy (oddělené samostatně funkční aplikace). Jednou z možností je použít RabbitMQ (nebo Kafku) a všechny eventy posílat jako zprávy do jedné exchange. Do této exchange jsou pak skrze routovací klíče (název eventu) nabindované fronty, které řeší konkrétní úlohu, třeba export objednávky do CRM.
Druhou možností je zpřístupnění event store skrze REST API. Externí systém pak typicky čte vzniklé události od momentu poslední synchronizace. To, jestli mu stačí data, které obsahuje samotný event a nebo jestli se skrze REST API doptá na potřebná data, je už jenom implementační detail.
Modelování procesů
Každý systém vždy obsahuje celou řadu procesů, otázka ale je, jestli tak s nimi pracujeme a jestli si jejich přítomnost uvědomujeme. Přítomnost procesů a jejich očekávané chování nám opět musí popsat doménový expert. Popis jednoduchého procesu by mohl vypadat například takto: „Všichni uživatelé v našem systému musí mít ověřený e-mail, bez toho jim neumožníme se přihlásit. Po úspěšné registraci systém odešle uživateli e-mail s odkazem pro ověření e-mailu. Po úspěšném ověření e-mailu se může uživatel přihlásit a zároveň mu odešleme uvítací e-mail.”
Nyní se zaměříme na část „Po úspěšné registraci systém odešle uživateli email s odkazem pro ověření e-mailu“. Všichni si umíme představit implementaci, kdy nejdříve vytvoříme záznam o uživateli v DB a následně rovnou odešleme e-mail pro ověření. Šlo by to ale udělat lépe? Co když má mailingová služba výpadek a mail nemůžeme odeslat?
V první fázi vytvoříme novou entitu uživatele a přímo v konstruktoru emitujeme event UserRegistered. Na tuto událost naslouchá RabbitMQ consumer, který vytvoří e-mail a zajistí jeho odeslání skrze mailingovou službu. Pokud má služba výpadek, můžeme zprávu přesunout do jiné fronty a e-mail odeslat později.
Nyní si už asi umíte představit, jak by se implementovala část „Po úspěšném ověření e-mailu se může uživatel přihlásit a zároveň mu odešleme uvítací e-mail.“ Uživatel klikl na odkaz, my si uložíme, že došlo k ověření a emitujeme event EmailVerified. Na základě této události posíláme uvítací e-mail.
Krom výhod asynchronní komunikace s externím systémem získáváme tímto přístupem také mnohem lepší udržitelnost. Pokud se objeví nový požadavek „Jakmile si uživatel ověří email, chceme ho vyexportovat do našeho CRM“, nemusíme v tomto případě sahat na logiku samotné registrace a pouze si na událost EmailVerified napojíme další operaci.
Doporučení pro implementaci
Doménový event je ve světě DDD plnohodnotný koncept, stejně jako entita nebo value objekt. Eventy by měly být co nejblíže entitě, které se týkají. Dále chceme, aby eventy emitovala přímo entita. Díky tomu zajistíme, že eventy se budou správně emitovat vždy, když se zavolá daná metoda, bez ohledu na to v jakém use-case.
Implementace na hrubo
Jelikož v jedné transakci můžeme pracovat s celou řadou entit, je potřeba mít globální stav, do kterého se budou ukládat všechny vzniklé eventy. Pro tento účel může vzniknout statický singleton, který jsme pak schopní používat i přímo v entitách. Tomuto singletonu říkáme EventPublisher.
Pro obecnou práci s eventy je potřebujeme sjednotit. Typicky nám vznikne interface, který bude obsahovat metody, které musí mít každý event. Určitě to budou metody pro získání id, názvu, času vzniku a data eventu v nějaké serializovatelné podobě, třeba pole.
Pro nasazení event systému do entit můžeme vytvořit abstraktní třídu, která bude mít metodu emit a ta bude přijímat obecně pouze interface eventu. Uvnitř metody se pak použije EventPublisher, který si zapamatuje vzniklý event.
Každý event pak implementujeme jako samostatnou třídu, která implementuje daný interface.
Na co si dát pozor
Implementace event-systému musí respektovat transakčnost systému. Pokud při provádění use-case dojde k chybě, neuloží se žádné změny na entitě a systém je v původním stavu. Stejně tak se v takový moment nesmí dostat ven ani vzniklé eventy a to i přesto, že už jsou v EventPublisheru.
Každý event by se měl ještě ve stejné transakci společně s daty uložit do nějakého persistentního úložiště a až následně jej můžeme poslat například do RabbitMQ. V žádném případě se nesmí event dostat ven dříve, než je ukončená transakce v DB. V takovém případě by se consumer snažil číst data, která ještě neexistují a zbytečně bychom si způsobili chyby.
Čas vzniku eventu je čas, kdy vznikl objekt eventu a ne moment jeho uložení, na to velký pozor. Typicky si tedy čas eventu ukládáme už v konstruktoru.
Závěr
Pokud jste dočetli až sem, doufám, že jsem vás obohatil o nové vědomosti, nasadil vám brouka do hlavy, či změnil pohled, jakým se dají vyvíjet pokročilé informační systémy. Bylo to okénko spíše do světa softwarového inženýrství, než ukázky hotového kódu, ale takové téma, by bez teorie a vysvětlení souvislostí uchopit nešlo.
Sdílet na facebooku anebo twitteru