CQRS a zabezpečovanie udalostí v Jave

1. Úvod

V tomto tutoriáli preskúmame základné koncepty návrhových vzorov oddelenia zodpovednosti za zodpovedanie príkazov (CQRS) a získavania udalostí.

Aj keď sú často uvádzané ako doplňujúce sa vzory, pokúsime sa ich pochopiť osobitne a nakoniec uvidíme, ako sa navzájom dopĺňajú. Existuje niekoľko nástrojov a rámcov, ako napríklad Axon, ktoré nám pomôžu tieto vzory osvojiť, ale na pochopenie základných princípov vytvoríme v Jave jednoduchú aplikáciu.

2. Základné pojmy

Tieto vzorce najskôr pochopíme teoreticky, až potom sa ich pokúsime implementovať. Pretože celkom dobre stoja ako jednotlivé vzory, pokúsime sa ich pochopiť bez toho, aby sme ich zmiešali.

Upozorňujeme, že tieto vzory sa v podnikovej aplikácii často používajú spoločne. V tejto súvislosti tiež profitujú z niekoľkých ďalších vzorov podnikovej architektúry. Keď ideme ďalej, o niektorých z nich hovoríme.

2.1. Sourcing udalostí

Sourcing udalostí nám dáva nový spôsob pretrvávajúceho stavu aplikácie ako usporiadaného sledu udalostí. Tieto udalosti môžeme selektívne dopytovať a kedykoľvek môžeme rekonštruovať stav aplikácie. Aby to dobre fungovalo, musíme samozrejme každú zmenu stavu aplikácie znova vykresliť ako udalosti:

Tieto udalosti sú tu sú fakty, ktoré sa stali a nemôžu byť zmenené - inými slovami, musia byť nemenné. Obnovenie stavu aplikácie je iba otázkou prehratia všetkých udalostí.

Toto tiež otvára možnosť selektívne prehrať udalosti, obrátene prehrať niektoré udalosti a oveľa viac. V dôsledku toho môžeme samotný stav aplikácie považovať za sekundárneho občana, pričom primárnym zdrojom pravdy je protokol udalostí.

2.2. CQRS

Zjednodušene povedané, CQRS je o segregácii príkazovej a dopytovej strany aplikačnej architektúry. CQRS je založený na princípe Command Query Separation (CQS), ktorý navrhol Bertrand Meyer. CQS navrhuje, aby sme operácie s doménovými objektmi rozdelili do dvoch samostatných kategórií: Dotazy a príkazy:

Dotazy vrátia výsledok a nezmenia pozorovateľný stav systému. Príkazy menia stav systému, ale nemusia nevyhnutne vracať hodnotu.

Dosiahneme to čistým oddelením príkazových a dotazovacích strán doménového modelu. Môžeme sa posunúť o krok ďalej, samozrejme, tiež rozdeliť stranu na ukladanie a zápis údajov, a to zavedením mechanizmu na ich synchronizáciu.

3. Jednoduchá aplikácia

Začneme popisom jednoduchej aplikácie v Jave, ktorá vytvára doménový model.

Aplikácia ponúkne operácie CRUD na doménovom modeli a bude obsahovať aj perzistenciu pre doménové objekty. CRUD je skratka pre Create, Read, Update a Delete, čo sú základné operácie, ktoré môžeme vykonávať na doménovom objekte.

Rovnakú aplikáciu použijeme na zavedenie Event Sourcing a CQRS v ďalších častiach.

V tomto procese využijeme v našom príklade niektoré koncepty z Domain-Driven Design (DDD).

DDD sa zaoberá analýzou a návrhom softvéru, ktorý sa opiera o komplexné znalosti špecifické pre danú doménu. Vychádza z myšlienky, že softvérové ​​systémy musia byť založené na dobre vyvinutom modeli domény. DDD ako prvý predpísal Eric Evans ako katalóg vzorov. Na zostavenie nášho príkladu použijeme niektoré z týchto vzorov.

3.1. Prehľad aplikácií

Vytvorenie užívateľského profilu a jeho správa je typickou požiadavkou v mnohých aplikáciách. Definujeme jednoduchý model domény, ktorý zachytáva profil používateľa spolu s perzistenciou:

Ako vidíme, náš doménový model je normalizovaný a vystavuje niekoľko operácií CRUD. Tieto operácie sú iba na ukážku a v závislosti od požiadaviek môže byť jednoduchá alebo zložitá. Úložisko perzistencie tu navyše môže byť v pamäti alebo môže namiesto toho používať databázu.

3.2. Implementácia aplikácie

Najskôr budeme musieť vytvoriť triedy Java predstavujúce náš doménový model. Toto je pomerne jednoduchý doménový model a nemusí vyžadovať ani zložitosť dizajnových vzorov ako Event Sourcing a CQRS. Toto však ponecháme jednoduché, aby sme sa zamerali na pochopenie základných vecí:

verejná trieda User {private String userid; private String meno; private String priezvisko; súkromné ​​Nastaviť kontakty; adresy súkromnej sady; // getters and setters} public class Contact {private String type; súkromný detail reťazca; // getters and setters} public class Address {private String city; súkromný štát reťazca; súkromný PSČ; // zakladatelia a zakladatelia}

Definujeme tiež jednoduché úložisko v pamäti pre perzistenciu stavu našej aplikácie. To samozrejme nepridáva žiadnu hodnotu, ale postačuje to na našu demonštráciu neskôr:

verejná trieda UserRepository {súkromný obchod s mapami = nový HashMap (); }

Teraz definujeme službu, ktorá odhalí typické operácie CRUD na našom doménovom modeli:

verejná trieda UserService {súkromné ​​úložisko UserRepository; public UserService (UserRepository repository) {this.repository = repository; } public void createUser (String userId, String firstName, String lastName) {User user = new User (userId, firstName, lastName); repository.addUser (userId, užívateľ); } public void updateUser (reťazec userId, nastavenie kontaktov, nastavenie adries) {User user = repository.getUser (userId); user.setContacts (kontakty); user.setAddresses (adresy); repository.addUser (userId, užívateľ); } public Set getContactByType (String userId, String contactType) {User user = repository.getUser (userId); Nastaviť kontakty = user.getContacts (); spätné kontakty.stream () .filter (c -> c.getType (). sa rovná (contactType)) .collect (Collectors.toSet ()); } public Set getAddressByRegion (String userId, String state) {User user = repository.getUser (userId); Nastaviť adresy = user.getAddresses (); návratové adresy.stream () .filter (a -> a.getState (). equals (state)) .collect (Collectors.toSet ()); }}

To je vlastne všetko, čo musíme urobiť, aby sme nastavili našu jednoduchú aplikáciu. Toto je zďaleka nie je kód pripravený na výrobu, ale odhaľuje niektoré dôležité body o ktorom sa budeme ďalej baviť v tomto návode.

3.3. Problémy v tejto aplikácii

Predtým, ako v diskusii s Event Sourcing a CQRS budeme pokračovať, stojí za to prediskutovať problémy súčasného riešenia. Rovnakými problémami sa budeme zaoberať aplikáciou týchto vzorov!

Z mnohých problémov, ktoré si tu môžeme všimnúť, by sme sa radi zamerali na dva z nich:

  • Doménový model: Operácie čítania a zápisu prebiehajú na rovnakom doménovom modeli. Aj keď to nie je problém jednoduchého modelu domény, ako je tento, môže sa to zhoršiť, pretože model domény sa stáva zložitejším. Možno budeme musieť optimalizovať náš doménový model a základné úložisko, aby vyhovovalo individuálnym potrebám operácií čítania a zápisu.
  • Vytrvalosť: Perzistencia, ktorú máme pre naše doménové objekty, ukladá iba najnovší stav doménového modelu. Aj keď to postačuje pre väčšinu situácií, niektoré úlohy sú náročné. Napríklad, ak musíme vykonať historický audit toho, ako objekt domény zmenil stav, tu to nie je možné. Aby sme to dosiahli, musíme doplniť naše riešenie o niektoré protokoly auditu.

4. Predstavujeme CQRS

Začneme riešiť prvý problém, o ktorom sme hovorili v poslednej časti, zavedením vzoru CQRS do našej aplikácie. V rámci toho oddelíme doménový model a jeho perzistenciu pri spracovávaní operácií zápisu a čítania. Pozrime sa, ako vzor CQRS reštrukturalizuje našu aplikáciu:

Diagram tu vysvetľuje, ako zamýšľame čisté oddelenie našej aplikačnej architektúry od strán pre zápis a čítanie. Zaviedli sme tu však niekoľko nových komponentov, ktorým musíme lepšie porozumieť. Upozorňujeme, že to nesúvisí výlučne s CQRS, ale CQRS z nich má veľké výhody:

  • Agregát / agregátor:

Agregát je vzor popísaný v Domain-Driven Design (DDD), ktorý logicky zoskupuje rôzne entity väzbou entít na agregovaný koreň. Agregovaný vzor poskytuje transakčnú konzistenciu medzi entitami.

CQRS prirodzene ťaží z agregovaného modelu, ktorý zoskupuje model domény zápisu a poskytuje transakčné záruky. Agregáty zvyčajne udržiavajú stav v medzipamäti pre lepší výkon, ale bez neho môžu fungovať perfektne.

  • Projekcia / projektor:

Projekcia je ďalším dôležitým vzorom, ktorý výrazne prospieva CQRS. Projekcia v podstate znamená predstavovanie doménových objektov v rôznych tvaroch a štruktúrach.

Tieto projekcie pôvodných údajov sú iba na čítanie a sú vysoko optimalizované, aby poskytovali lepší zážitok z čítania. Môžeme sa opäť rozhodnúť pre medzipamäť projekcií pre lepší výkon, ale to nie je nevyhnutnosť.

4.1. Implementácia strany zápisu aplikácie

Najskôr implementujme stranu aplikácie na zápis.

Začneme definovaním požadovaných príkazov. A príkaz je zámer mutovať stav modelu domény. Či bude úspešný alebo nie, závisí od obchodných pravidiel, ktoré nakonfigurujeme.

Pozrime sa na naše príkazy:

verejná trieda CreateUserCommand {private String userId; private String meno; private String priezvisko; } verejná trieda UpdateUserCommand {private String userId; adresy súkromnej sady; súkromné ​​Nastaviť kontakty; }

Jedná sa o veľmi jednoduché triedy, ktoré obsahujú údaje, ktoré chceme mutovať.

Ďalej definujeme agregát, ktorý je zodpovedný za prijímanie príkazov a manipuláciu s nimi. Agregáty môžu prijať alebo odmietnuť príkaz:

verejná trieda UserAggregate {private UserWriteRepository writeRepository; public UserAggregate (UserWriteRepository repository) {this.writeRepository = repository; } public User handleCreateUserCommand (príkaz CreateUserCommand) {User user = nový Užívateľ (command.getUserId (), command.getFirstName (), command.getLastName ()); writeRepository.addUser (user.getUserid (), užívateľ); návratový užívateľ; } public User handleUpdateUserCommand (príkaz UpdateUserCommand) {Užívateľ user = writeRepository.getUser (command.getUserId ()); user.setAddresses (command.getAddresses ()); user.setContacts (command.getContacts ()); writeRepository.addUser (user.getUserid (), užívateľ); návratový užívateľ; }}

Agregát používa úložisko na získanie aktuálneho stavu a na vykonanie akýchkoľvek zmien. Okrem toho môže ukladať aktuálny stav lokálne, aby sa pri spracovaní každého príkazu zabránilo nákladom na spiatočnú cestu do úložiska.

Nakoniec potrebujeme úložisko na udržanie stavu doménového modelu. Spravidla to bude databáza alebo iný odolný obchod, ale tu ich jednoducho nahradíme dátovou štruktúrou v pamäti:

verejná trieda UserWriteRepository {súkromný obchod s mapami = nový HashMap (); // prístupcovia a mutátory}

Týmto sa končí naša stránka zápisu.

4.2. Implementácia strany na čítanie aplikácie

Prejdime teraz na stranu na čítanie aplikácie. Začneme definovaním strany na čítanie doménového modelu:

verejná trieda UserAddress {súkromná mapa addressByRegion = nový HashMap (); } verejná trieda UserContact {súkromná mapa contactByType = new HashMap (); }

Ak si spomenieme na naše operácie čítania, nie je ťažké vidieť, že tieto triedy dokonale zvládajú ich zvládnutie. To je krása vytvorenia modelu domény zameraného na dotazy, ktoré máme.

Ďalej definujeme úložisko na čítanie. Opäť použijeme iba dátovú štruktúru v pamäti, aj keď to bude odolnejšie úložisko dát v skutočných aplikáciách:

public class UserReadRepository {private Map userAddress = new HashMap (); private Map userContact = new HashMap (); // prístupcovia a mutátory}

Teraz definujeme požadované dotazy, ktoré musíme podporovať. Dotaz je zámer získať údaje - nemusí nutne vyústiť do údajov.

Pozrime sa na naše otázky:

verejná trieda ContactByTypeQuery {private String userId; private String contactType; } verejná trieda AddressByRegionQuery {private String userId; súkromný štát reťazca; }

Opäť ide o jednoduché triedy Java, ktoré obsahujú údaje na definovanie dotazu.

Teraz potrebujeme projekciu, ktorá zvládne tieto dotazy:

verejná trieda UserProjection {private UserReadRepository readRepository; public UserProjection (UserReadRepository readRepository) {this.readRepository = readRepository; } public set handle (ContactByTypeQuery query) {UserContact userContact = readRepository.getUserContact (query.getUserId ()); vrátiť userContact.getContactByType () .get (query.getContactType ()); } public set handle (dotaz AddressByRegionQuery) {UserAddress userAddress = readRepository.getUserAddress (query.getUserId ()); návrat userAddress.getAddressByRegion () .get (query.getState ()); }}

Projekcia tu používa úložisko na čítanie, ktoré sme definovali skôr, na riešenie otázok, ktoré máme. Týmto sa do veľkej miery končí aj čítaná stránka našej aplikácie.

4.3. Synchronizácia údajov na čítanie a zápis

Jeden kúsok tohto puzzle ešte nie je vyriešený: nie je k čomu synchronizovať naše úložiská na zápis a čítanie.

Tu budeme potrebovať niečo známe ako projektor. A projektor má logiku premietnuť model zápisu do modelu čítanej domény.

Existujú oveľa sofistikovanejšie spôsoby, ako to vyriešiť, ale necháme si to pomerne jednoduché:

verejná trieda UserProjector {UserReadRepository readRepository = nový UserReadRepository (); public UserProjector (UserReadRepository readRepository) {this.readRepository = readRepository; } public void project (User user) {UserContact userContact = Optional.ofNullable (readRepository.getUserContact (user.getUserid ())) .orElse (nový UserContact ()); Mapa contactByType = new HashMap (); pre (Contact contact: user.getContacts ()) {Set contacts = Optional.ofNullable (contactByType.get (contact.getType ())) .orElse (new HashSet ()); contacts.add (kontakt); contactByType.put (contact.getType (), contacts); } userContact.setContactByType (contactByType); readRepository.addUserContact (user.getUserid (), userContact); UserAddress userAddress = Optional.ofNullable (readRepository.getUserAddress (user.getUserid ())) .orElse (nová UserAddress ()); Mapa addressByRegion = nový HashMap (); pre (Address address: user.getAddresses ()) {Set addresses = Optional.ofNullable (addressByRegion.get (address.getState ())) .orElse (new HashSet ()); addresses.add (adresa); addressByRegion.put (address.getState (), addresses); } userAddress.setAddressByRegion (addressByRegion); readRepository.addUserAddress (user.getUserid (), userAddress); }}

To je skôr veľmi hrubý spôsob, ako to dosiahnuť, ale poskytuje nám dostatok informácií o tom, čo je potrebné aby CQRS fungovala. Okrem toho nie je nutné, aby úložiská na čítanie a zápis ležali v rôznych kamenných obchodoch. Distribuovaný systém má svoj vlastný podiel problémov!

Upozorňujeme, že je nie je vhodné projektovať aktuálny stav domény zápisu do rôznych modelov domény čítania. Príklad, ktorý sme si tu vzali, je dosť jednoduchý, a preto nevidíme problém.

S pribúdajúcimi modelmi zápisu a čítania sa však bude čoraz ťažšie projektovať. Môžeme to vyriešiť pomocou projekcie založenej na udalostiach namiesto štátnej projekcie so zabezpečovaním udalostí. Uvidíme, ako to dosiahnuť neskôr v tutoriále.

4.4. Výhody a nevýhody CQRS

Diskutovali sme o vzore CQRS a naučili sme sa, ako ho zaviesť do typickej aplikácie. Kategoricky sme sa pokúsili vyriešiť problém súvisiaci s rigiditou doménového modelu pri práci s čítaním aj zápisom.

Poďme teraz diskutovať o ďalších výhodách, ktoré CQRS prináša do aplikačnej architektúry:

  • CQRS nám poskytuje pohodlný spôsob výberu samostatných doménových modelov vhodné pre operácie zápisu a čítania; nemusíme vytvárať komplexný doménový model podporujúci oboje
  • Pomáha nám to vyberte úložiská, ktoré sú individuálne vhodné na zvládnutie zložitosti operácií čítania a zápisu, ako je vysoká priepustnosť pre zápis a nízka latencia pri čítaní
  • Prirodzene dopĺňa programovacie modely založené na udalostiach v distribuovanej architektúre poskytnutím oddelenia obáv, ako aj jednoduchších doménových modelov

To však nepríde zadarmo. Ako je zrejmé z tohto jednoduchého príkladu, CQRS dodáva architektúre značnú zložitosť. V mnohých scenároch to nemusí byť vhodné alebo bolestivé:

  • Iba komplexný doménový model môže byť prospešný z pridanej zložitosti tohto modelu; bez toho všetkého sa dá spravovať jednoduchý doménový model
  • Prirodzene vedie k duplikácii kódu do istej miery, čo je prijateľné zlo v porovnaní so ziskom, ku ktorému nás vedie; odporúča sa však individuálny úsudok
  • Samostatné úložiská viesť k problémom s konzistencioua je ťažké udržiavať úložiská na zápis a čítanie vždy v dokonalej synchronizácii; často sa musíme uspokojiť s prípadnou konzistenciou

5. Predstavujeme zabezpečovanie udalostí

Ďalej sa budeme venovať druhému problému, o ktorom sme hovorili v našej jednoduchej aplikácii. Ak si spomenieme, súviselo to s našim úložiskom perzistencie.

Na riešenie tohto problému zavedieme Event Sourcing. Sourcing udalostí dramaticky mení spôsob, akým uvažujeme o ukladaní stavu aplikácie.

Pozrime sa, ako to zmení naše úložisko:

Tu sme vytvorili štruktúru naše úložisko na uloženie zoradeného zoznamu udalostí domény. Každá zmena objektu domény sa považuje za udalosť. Aká hrubá alebo jemná by mala byť udalosť, je vecou doménového dizajnu. Tu je dôležité vziať do úvahy to udalosti majú časový poriadok a sú nemenné.

5.1. Implementácia udalostí a obchodu udalostí

Základnými objektmi v aplikáciách riadených udalosťami sú udalosti a získavanie udalostí sa nelíši. Ako sme už videli skôr, udalosti predstavujú konkrétnu zmenu stavu doménového modelu v konkrétnom okamihu. Začneme teda definovaním základnej udalosti pre našu jednoduchú aplikáciu:

verejná abstraktná trieda Udalosť {public final UUID id = UUID.randomUUID (); verejné konečné dátum Vytvorené = nový Dátum (); }

Toto len zaisťuje, že každá udalosť, ktorú generujeme v našej aplikácii, dostane jedinečnú identifikáciu a časovú pečiatku vytvorenia. Sú potrebné na ich ďalšie spracovanie.

Samozrejme, môže nás zaujímať niekoľko ďalších atribútov, napríklad atribút na určenie pôvodu udalosti.

Ďalej vytvorme niekoľko udalostí špecifických pre doménu dediacich z tejto základnej udalosti:

verejná trieda UserCreatedEvent rozširuje udalosť {private String userId; private String meno; private String priezvisko; } verejná trieda UserContactAddedEvent rozširuje udalosť {private String contactType; private String contactDetails; } verejná trieda UserContactRemovedEvent rozširuje udalosť {private String contactType; private String contactDetails; } verejná trieda UserAddressAddedEvent rozširuje udalosť {private String city; súkromný štát reťazca; súkromný reťazec postCode; } verejná trieda UserAddressRemovedEvent rozširuje udalosť {private String city; súkromný štát reťazca; súkromný reťazec postCode; }

Jedná sa o jednoduché POJO v Jave, ktoré obsahujú podrobnosti o udalosti domény. Tu je však potrebné poznamenať podrobnosť udalostí.

Mohli sme vytvoriť jednu udalosť pre aktualizácie používateľov, ale namiesto toho sme sa rozhodli vytvoriť samostatné udalosti na pridanie a odstránenie adresy a kontaktu. Voľba je namapovaná na to, čo zefektívňuje prácu s doménovým modelom.

Teraz samozrejme potrebujeme úložisko na uchovávanie udalostí našej domény:

public class EventStore {súkromná mapa store = new HashMap (); }

Toto je jednoduchá dátová štruktúra v pamäti na uchovávanie udalostí našej domény. V realite, existuje niekoľko riešení špeciálne vytvorených na spracovanie údajov o udalostiach, ako napríklad Apache Druid. Existuje veľa univerzálnych distribuovaných dátových skladov schopných spracovávať zdroje udalostí, vrátane Kafku a Cassandru.

5.2. Generovanie a konzumácia udalostí

Teraz sa teda zmení naša služba, ktorá spracúvala všetky operácie CRUD. Teraz namiesto aktualizácie presunutého stavu domény pripojí udalosti domény. Rovnako použije rovnaké udalosti domény na odpovedanie na dotazy.

Pozrime sa, ako to môžeme dosiahnuť:

verejná trieda UserService {súkromné ​​úložisko EventStore; public UserService (úložisko EventStore) {this.repository = úložisko; } public void createUser (String userId, String firstName, String lastName) {repository.addEvent (userId, new UserCreatedEvent (userId, firstName, lastName)); } public void updateUser (reťazec userId, nastavenie kontaktov, nastavenie adries) {User user = UserUtility.recreateUserState (úložisko, userId); user.getContacts (). stream () .filter (c ->! contacts.contains (c)) .forEach (c -> repository.addEvent (userId, new UserContactRemovedEvent (c.getType (), c.getDetail ()) )); contacts.stream () .filter (c ->! user.getContacts (). contains (c)) .forEach (c -> repository.addEvent (userId, new UserContactAddedEvent (c.getType (), c.getDetail ()) )); user.getAddresses (). stream () .filter (a ->! addresses.contains (a)) .forEach (a -> repository.addEvent (userId, new UserAddressRemovedEvent (a.getCity (), a.getState (), a.getPostcode ()))); addresses.stream () .filter (a ->! user.getAddresses (). contains (a)) .forEach (a -> repository.addEvent (userId, new UserAddressAddedEvent (a.getCity (), a.getState (), a.getPostcode ()))); } public Set getContactByType (String userId, String contactType) {User user = UserUtility.recreateUserState (úložisko, userId); vrátiť user.getContacts (). stream () .filter (c -> c.getType (). equals (contactType)) .collect (Collectors.toSet ()); } public Set getAddressByRegion (String userId, String state) hodí Exception {User user = UserUtility.recreateUserState (repozitár, userId); návrat user.getAddresses (). stream () .filter (a -> a.getState (). equals (state)) .collect (Collectors.toSet ()); }}

Upozorňujeme, že v rámci spracovania používateľskej operácie aktualizácie tu generujeme niekoľko udalostí. Je tiež zaujímavé poznamenať, ako sa máme generovanie aktuálneho stavu modelu domény prehraním všetkých doteraz vygenerovaných udalostí domény.

V skutočnej aplikácii to samozrejme nie je uskutočniteľná stratégia a budeme musieť udržiavať lokálnu vyrovnávaciu pamäť, aby sme sa vyhli generovaniu stavu zakaždým. Proces môžu urýchliť aj ďalšie stratégie, ako napríklad snímky a súhrn v úložisku udalostí.

Týmto je ukončená naša snaha zaviesť v našej jednoduchej aplikácii sourcing udalostí.

5.3. Výhody a nevýhody sourcingu udalostí

Teraz sme úspešne prijali alternatívny spôsob ukladania doménových objektov pomocou získavania udalostí. Zdroj udalostí je silný vzor a pri vhodnom použití prináša architektúre aplikácií veľa výhod:

  • Robí operácie zápisu oveľa rýchlejšie pretože nie sú potrebné žiadne údaje na čítanie, aktualizáciu a zápis; write je iba pridanie udalosti do protokolu
  • Odstráni objektovo-relačnú impedanciu a teda potreba komplexných nástrojov na mapovanie; samozrejme, stále musíme znovu vytvárať objekty späť
  • Stáva sa poskytnúť protokol auditu ako vedľajší produkt, ktorý je úplne spoľahlivý; môžeme presne vyladiť, ako sa zmenil stav doménového modelu
  • Umožňuje to podpora časových otázok a dosiahnutie cestovania v čase (stav domény v minulosti)!
  • Je to prirodzené vhodné na navrhovanie voľne spriahnutých komponentov v architektúre mikroslužieb, ktoré komunikujú asynchrónne výmenou správ

Ako vždy však ani získavanie udalostí nie je strieborná guľka. Núti nás to prijať dramaticky odlišný spôsob ukladania údajov. To sa nemusí ukázať ako užitočné v niekoľkých prípadoch:

  • Je tu súvisí s krivkou učenia a je potrebný posun v myslení prijať zabezpečovanie udalostí; na začiatok to nie je intuitívne
  • Robí to dosť ťažké na zvládnutie typických otázok pretože potrebujeme znovu vytvoriť stav, pokiaľ si ponecháme stav v lokálnej pamäti cache
  • Aj keď sa dá použiť na akýkoľvek doménový model, je to tak vhodnejšie pre model založený na udalostiach v architektúre riadenej udalosťami

6. CQRS so zabezpečovaním udalostí

Teraz, keď sme videli, ako individuálne predstaviť Event Sourcing a CQRS pre našu jednoduchú aplikáciu, je čas ich spojiť. To by malo byť pomerne intuitívne teraz, keď tieto vzory môžu navzájom výrazne prospievať. V tejto časti to však objasníme.

Najprv sa pozrime, ako ich aplikačná architektúra spája:

To by už teraz nemalo byť žiadnym prekvapením. Nahradili sme stranu zápisu úložiska, aby bola obchodom udalostí, zatiaľ čo strana pre čítanie úložiska je stále rovnaká.

Upozorňujeme, že to nie je jediný spôsob, ako využiť Event Sourcing a CQRS v architektúre aplikácie. My môže byť celkom inovatívne a používať tieto vzory spolu s inými vzormi a prísť s niekoľkými možnosťami architektúry.

Dôležité je tu zabezpečiť, aby sme ich používali na zvládnutie zložitosti, nielen na ďalšie zvyšovanie zložitosti!

6.1. Spojenie CQRS a sourcingu udalostí

Po individuálnej implementácii Event Sourcing a CQRS by nemalo byť také ťažké pochopiť, ako ich môžeme spojiť.

Budeme začať s aplikáciou, kde sme predstavili CQRS, a stačí vykonať príslušné zmeny aby sme zaistili zabezpečenie udalosti. Využijeme tiež rovnaké udalosti a obchod s udalosťami, ktoré sme definovali v našej aplikácii, kde sme zaviedli získavanie udalostí.

Je len niekoľko zmien. Začneme zmenou agregátu na generovať udalosti namiesto aktualizácie stavu:

verejná trieda UserAggregate {private EventStore writeRepository; public UserAggregate (úložisko EventStore) {this.writeRepository = úložisko; } public List handleCreateUserCommand (príkaz CreateUserCommand) {UserCreatedEvent event = nový UserCreatedEvent (command.getUserId (), command.getFirstName (), command.getLastName ()); writeRepository.addEvent (command.getUserId (), udalosť); návrat Arrays.asList (udalosť); } verejný zoznam handleUpdateUserCommand (príkaz UpdateUserCommand) {Používateľ užívateľ = UserUtility.recreateUserState (writeRepository, command.getUserId ()); Zoznam udalostí = nový ArrayList (); Zoznam kontaktovToRemove = user.getContacts (). Stream () .filter (c ->! Command.getContacts (). Obsahuje (c)) .collect (Collectors.toList ()); pre (Contact contact: contactsToRemove) {UserContactRemovedEvent contactRemovedEvent = new UserContactRemovedEvent (contact.getType (), contact.getDetail ()); events.add (contactRemovedEvent); writeRepository.addEvent (command.getUserId (), contactRemovedEvent); } Zoznam kontaktovToAdd = command.getContacts (). Stream () .filter (c ->! User.getContacts (). Obsahuje (c)) .collect (Collectors.toList ()); pre (Contact contact: contactsToAdd) {UserContactAddedEvent contactAddedEvent = new UserContactAddedEvent (contact.getType (), contact.getDetail ()); events.add (contactAddedEvent); writeRepository.addEvent (command.getUserId (), contactAddedEvent); } // podobne spracovať adresyToRemove // ​​podobne spracovať adresyToAdd vrátiť udalosti; }}

Jediná ďalšia požadovaná zmena je v projektore, ktorý teraz musí spracovávať udalosti namiesto stavov doménových objektov:

verejná trieda UserProjector {UserReadRepository readRepository = nový UserReadRepository (); public UserProjector (UserReadRepository readRepository) {this.readRepository = readRepository; } public void project (String userId, List events) {for (Event event: events) {if (event instanceof UserAddressAddedEvent) apply (userId, (UserAddressAddedEvent) event); if (event instanceof UserAddressRemovedEvent) apply (userId, (UserAddressRemovedEvent) event); if (event instanceof UserContactAddedEvent) apply (userId, (UserContactAddedEvent) event); if (event instanceof UserContactRemovedEvent) apply (userId, (UserContactRemovedEvent) event); }} public void apply (String userId, UserAddressAddedEvent event) {Address address = new Address (event.getCity (), event.getState (), event.getPostCode ()); UserAddress userAddress = Optional.ofNullable (readRepository.getUserAddress (userId)) .orElse (nová UserAddress ()); Nastaviť adresy = Optional.ofNullable (userAddress.getAddressByRegion () .get (address.getState ())) .orElse (nový HashSet ()); addresses.add (adresa); userAddress.getAddressByRegion () .put (address.getState (), adresy); readRepository.addUserAddress (userId, userAddress); } public void apply (reťazec userId, udalosť UserAddressRemovedEvent) {adresa adresa = nová adresa (event.getCity (), event.getState (), event.getPostCode ()); UserAddress userAddress = readRepository.getUserAddress (userId); if (userAddress! = null) {Set addresses = userAddress.getAddressByRegion () .get (address.getState ()); if (addresses! = null) addresses.remove (address); readRepository.addUserAddress (userId, userAddress); }} public void apply (String userId, UserContactAddedEvent event) {// Podobne spracovať udalosť UserContactAddedEvent} public void apply (String userId, UserContactRemovedEvent event) {// Podobne spracovať udalosť UserContactRemovedEvent}}

Ak si spomenieme na problémy, o ktorých sme diskutovali pri riešení stavovej projekcie, je to možné riešenie.

The projekcia založená na udalostiach je dosť pohodlná a ľahšie realizovateľná. Všetko, čo musíme urobiť, je spracovať všetky vyskytujúce sa udalosti domény a aplikovať ich na všetky prečítané modely domény. Typicky by v aplikácii založenej na udalostiach projektor počúval udalosti domény, ktoré ho zaujímajú, a nespoliehal by sa na to, že by ich niekto zavolal priamo.

To je skoro všetko, čo musíme urobiť, aby sme Event Sourcing a CQRS spojili v našej jednoduchej aplikácii.

7. Záver

V tomto tutoriáli sme diskutovali o základoch návrhových vzorov Event Sourcing a CQRS. Vyvinuli sme jednoduchú aplikáciu a tieto vzory sme na ňu aplikovali individuálne.

V tomto procese sme pochopili výhody, ktoré prinášajú, a nevýhody, ktoré prinášajú. Nakoniec sme pochopili, prečo a ako obidva tieto vzory začleniť do našej aplikácie.

Jednoduchá aplikácia, o ktorej sme hovorili v tomto tutoriále, sa ani zďaleka nevyrovná odôvodneniu potreby CQRS a Event Sourcing. Naším zameraním bolo porozumieť základným pojmom, preto bol príklad triviálny. Ale ako už bolo spomenuté vyššie, výhody týchto vzorov je možné realizovať iba v aplikáciách, ktoré majú primerane zložitý doménový model.

Ako obvykle, zdrojový kód tohto článku nájdete na GitHub.