CQRS i izvor izvora u Javi

1. Uvod

U ovom uputstvu istražit ćemo osnovne koncepte razdvajanja odgovornosti za naredbeni upit (CQRS) i obrasce dizajna izvora izvora događaja.

Iako se često navode kao komplementarni obrasci, pokušat ćemo ih zasebno razumjeti i konačno vidjeti kako se međusobno nadopunjuju. Postoji nekoliko alata i okvira, poput Axona, koji pomažu usvojiti te obrasce, ali stvorit ćemo jednostavnu aplikaciju na Javi kako bismo razumjeli osnove.

2. Osnovni pojmovi

Prvo ćemo te obrasce razumjeti teoretski prije nego što ih pokušamo primijeniti. Također, kako sasvim dobro stoje kao pojedinačni obrasci, pokušat ćemo ih razumjeti bez miješanja.

Imajte na umu da se ti obrasci često koriste zajedno u poslovnoj aplikaciji. S tim u vezi, oni također imaju koristi od nekoliko drugih obrazaca arhitekture poduzeća. Raspravljat ćemo o nekima od njih dok budemo išli dalje.

2.1. Izvor događaja

Izvor događaja daje nam novi način trajnog stanja aplikacije kao uređeni slijed događaja. Možemo selektivno ispitivati ​​ove događaje i rekonstruirati stanje aplikacije u bilo kojem trenutku. Da bismo to uspjeli, moramo svaku sliku stanja aplikacije zamišljati kao događaje:

Ovi događaji ovdje su činjenice koje su se dogodile i ne mogu se mijenjati - drugim riječima, oni moraju biti nepromjenjivi. Ponovno stvaranje stanja aplikacije samo je pitanje ponovnog prikazivanja svih događaja.

Imajte na umu da ovo također otvara mogućnost selektivnog ponavljanja događaja, ponavljanja nekih događaja unatrag i još mnogo toga. Kao posljedicu, prema samoj državi prijave možemo se odnositi kao prema sekundarnom građaninu, a zapis događaja kao primarni izvor istine.

2.2. CQRS

Pojednostavljeno, CQRS jest o razdvajanju naredbene i upitne strane arhitekture aplikacije. CQRS se temelji na principu razdvajanja naredbenih upita (CQS) koji je predložio Bertrand Meyer. CQS predlaže da operacije na objektima domene podijelimo u dvije različite kategorije: Upiti i naredbe:

Upiti vraćaju rezultat i ne mijenjaju vidljivo stanje sustava. Naredbe mijenjaju stanje sustava, ali ne moraju nužno vratiti vrijednost.

To postižemo čistim odvajanjem naredbene i upitne strane modela domene. Možemo napraviti korak dalje, razdvajajući i stranicu zapisivanja i čitanja spremišta podataka, naravno, uvođenjem mehanizma za njihovo usklađivanje.

3. Jednostavna aplikacija

Započet ćemo opisom jednostavne aplikacije na Javi koja gradi model domene.

Aplikacija će nuditi CRUD operacije na modelu domene, a također će sadržavati trajnost objekata domene. CRUD je kratica za stvaranje, čitanje, ažuriranje i brisanje, što su osnovne radnje koje možemo izvesti na objektu domene.

Istu ćemo aplikaciju upotrijebiti za uvođenje izvora događaja i CQRS-a u kasnijim odjeljcima.

U tom ćemo procesu koristiti neke od koncepata iz dizajna vođenog domenom (DDD) u našem primjeru.

DDD se bavi analizom i dizajnom softvera koji se oslanja na složeno znanje specifično za domenu. Gradi se na ideji da se softverski sustavi moraju temeljiti na dobro razvijenom modelu domene. DDD je prvi put propisao Eric Evans kao katalog uzoraka. Koristit ćemo neke od ovih obrazaca za izgradnju svog primjera.

3.1. Pregled aplikacije

Stvaranje korisničkog profila i upravljanje njime tipičan je zahtjev u mnogim aplikacijama. Definirat ćemo jednostavan model domene koji bilježi korisnički profil uz postojanost:

Kao što vidimo, naš model domene je normaliziran i izlaže nekoliko CRUD operacija. Te su operacije samo za demonstraciju i može biti jednostavno ili složeno ovisno o zahtjevima. Štoviše, ovdje spremište trajnosti može biti u memoriji ili umjesto toga koristiti bazu podataka.

3.2. Implementacija aplikacije

Prvo ćemo morati stvoriti Java klase koje predstavljaju naš model domene. Ovo je prilično jednostavan model domene i možda čak ne zahtijeva složenost dizajnerskih uzoraka poput izvora događaja i CQRS-a. Međutim, držat ćemo ovo jednostavno kako bismo se usredotočili na razumijevanje osnova:

javna klasa User {private String userid; private String firstName; private String lastName; privatni skup kontakata; privatni skup adresa; // getters and setters} javna klasa Contact {private String type; detalj privatnog niza; // getters and setters} javna klasa Adresa {private String city; privatna država gudača; privatni poštanski broj; // geteri i postavljači}

Također ćemo definirati jednostavno spremište u memoriji radi trajanja našeg stanja aplikacije. Naravno, to ne dodaje nikakvu vrijednost, ali je dovoljno za našu demonstraciju kasnije:

javna klasa UserRepository {private map store = new HashMap (); }

Sada ćemo definirati uslugu za izlaganje tipičnih CRUD operacija na našem modelu domene:

javna klasa UserService {privatno spremište UserRepository; javna UserService (spremište UserRepository) {this.repository = spremište; } public void createUser (String userId, String firstName, String lastName) {User user = new User (userId, firstName, lastName); repository.addUser (userId, user); } javna void updateUser (niz userId, Postavi kontakte, Postavi adrese) {User user = repository.getUser (userId); user.setContacts (kontakti); user.setAddresses (adrese); repository.addUser (userId, user); } javni Postavi getContactByType (String userId, String contactType) {User user = repository.getUser (userId); Postavi kontakte = user.getContacts (); vratiti kontakte.stream () .filter (c -> c.getType (). jednako (contactType)) .collect (Collectors.toSet ()); } javni Postavi getAddressByRegion (Niz userId, stanje niza) {User user = repository.getUser (userId); Postavi adrese = user.getAddresses (); vratiti adrese.stream () .filter (a -> a.getState (). jednako (stanje)) .collect (Collectors.toSet ()); }}

To je uglavnom ono što moramo učiniti da bismo postavili našu jednostavnu aplikaciju. Ovo je daleko od toga da je kôd spreman za proizvodnju, ali otkriva neke važne točke o čemu ćemo raspravljati kasnije u ovom vodiču.

3.3. Problemi u ovoj aplikaciji

Prije nego što nastavimo dalje u našoj raspravi s izvorima događaja i CQRS-om, vrijedi razgovarati o problemima s trenutnim rješenjem. Napokon, rješavat ćemo iste probleme primjenjujući ove uzorke!

Od mnogih problema koje ovdje možemo primijetiti, voljeli bismo se usredotočiti samo na dva od njih:

  • Model domene: Operacije čitanja i pisanja odvijaju se na istom modelu domene. Iako ovo nije problem za jednostavan model domene poput ovog, može se pogoršati kako se model domene usložnjava. Možda ćemo trebati optimizirati svoj model domene i temeljnu pohranu kako bi odgovarali individualnim potrebama operacija čitanja i pisanja.
  • Upornost: Upornost koju imamo za objekte naše domene pohranjuje samo najnovije stanje modela domene. Iako je to dovoljno za većinu situacija, neke zadatke čini izazovnima. Na primjer, ako moramo izvršiti povijesnu reviziju kako je objekt domene promijenio stanje, to ovdje nije moguće. Da bismo to postigli, svoje rješenje moramo dopuniti nekim zapisnicima revizije.

4. Predstavljamo CQRS

S prvim problemom o kojem smo raspravljali započet ćemo započinjanjem uvođenja uzorka CQRS u našu aplikaciju. Kao dio ovoga, razdvojit ćemo model domene i njegovu postojanost u rukovanju operacijama pisanja i čitanja. Pogledajmo kako obrazac CQRS restrukturira našu aplikaciju:

Dijagram ovdje objašnjava kako namjeravamo čisto odvojiti našu arhitekturu aplikacije za pisanje i čitanje strana. Međutim, ovdje smo uveli nekoliko novih komponenata koje moramo bolje razumjeti. Imajte na umu da oni nisu strogo povezani s CQRS-om, ali CQRS im od velike koristi:

  • Agregat / agregator:

Agregat je obrazac opisan u Domain-Driven Design (DDD) koji logički grupira različite entitete vezujući entitete za agregatni korijen. Skupni obrazac pruža transakcijsku dosljednost između entiteta.

CQRS prirodno koristi agregatni obrazac, koji grupira model domene upisa, pružajući transakcijska jamstva. Agregati obično drže predmemorirano stanje radi boljih performansi, ali mogu savršeno raditi bez njega.

  • Projekcija / Projektor:

Projekcija je još jedan važan obrazac koji uvelike koristi CQRS-u. Projekcija u osnovi znači predstavljanje objekata domene u različitim oblicima i strukturama.

Ove projekcije izvornih podataka samo su za čitanje i visoko su optimizirane kako bi pružile poboljšani doživljaj čitanja. Možda ćemo se opet odlučiti za predmemoriranje projekcija radi boljih performansi, ali to nije nužno.

4.1. Implementacija strane za pisanje aplikacije

Prvo implementiramo stranu za pisanje aplikacije.

Započet ćemo definiranjem potrebnih naredbi. A naredba je namjera mutiranja stanja modela domene. Hoće li uspjeti ili ne, ovisi o poslovnim pravilima koja konfiguriramo.

Pogledajmo naše naredbe:

javna klasa CreateUserCommand {private String userId; private String firstName; private String lastName; } javna klasa UpdateUserCommand {private String userId; privatni skup adresa; privatni skup kontakata; }

To su prilično jednostavne klase koje sadrže podatke koje namjeravamo mutirati.

Dalje, definiramo agregat koji je odgovoran za primanje naredbi i rukovanje njima. Agregati mogu prihvatiti ili odbiti naredbu:

javna klasa UserAggregate {private UserWriteRepository writeRepository; javni UserAggregate (spremište UserWriteRepository) {this.writeRepository = spremište; } javni korisnik handleCreateUserCommand (naredba CreateUserCommand) {Korisnik = novi korisnik (command.getUserId (), command.getFirstName (), command.getLastName ()); writeRepository.addUser (user.getUserid (), user); povratni korisnik; } javni korisnik handleUpdateUserCommand (naredba UpdateUserCommand) {Korisnik korisnik = writeRepository.getUser (command.getUserId ()); user.setAddresses (command.getAddresses ()); user.setContacts (command.getContacts ()); writeRepository.addUser (user.getUserid (), user); povratni korisnik; }}

Agregat koristi spremište za dohvaćanje trenutnog stanja i ustrajanje u svim promjenama na njemu. Štoviše, može pohraniti trenutno stanje lokalno kako bi izbjegao povratni trošak do spremišta tijekom obrade svake naredbe.

Konačno, trebamo spremište za zadržavanje stanja modela domene. To će obično biti baza podataka ili drugo trajno spremište, ali ovdje ćemo ih jednostavno zamijeniti strukturom podataka u memoriji:

javna klasa UserWriteRepository {private map store = new HashMap (); // pristupnici i mutatori}

Ovim se završava strana pisanja naše prijave.

4.2. Implementacija strane primjene za čitanje

Prebacimo se sada na stranu za čitanje aplikacije. Počet ćemo definiranjem strane čitanja modela domene:

javna klasa UserAddress {privatna karta addressByRegion = novi HashMap (); } javna klasa UserContact {privatna karta contactByType = nova HashMap (); }

Ako se prisjetimo naših operacija čitanja, nije teško vidjeti da se ove klase savršeno dobro mape kako bi se mogle njima baviti. U tome je ljepota stvaranja modela domene usredotočenog na upite koje imamo.

Zatim ćemo definirati spremište za čitanje. Opet, koristit ćemo samo strukturu podataka u memoriji, iako će ovo biti trajnija pohrana podataka u stvarnim aplikacijama:

javna klasa UserReadRepository {private Map userAddress = new HashMap (); privatna karta userContact = novi HashMap (); // pristupnici i mutatori}

Sada ćemo definirati potrebne upite koje moramo podržati. Upit je namjera dobivanja podataka - ne mora nužno rezultirati podacima.

Pogledajmo naše upite:

javna klasa ContactByTypeQuery {private String userId; private String contactType; } javna klasa AddressByRegionQuery {private String userId; privatna država gudača; }

Opet, ovo su jednostavne Java klase koje sadrže podatke za definiranje upita.

Ono što nam sada treba je projekcija koja može obraditi ove upite:

javna klasa UserProjection {private UserReadRepository readRepository; javni UserProjection (UserReadRepository readRepository) {this.readRepository = readRepository; } javna postavka za postavljanje (upit ContactByTypeQuery) {UserContact userContact = readRepository.getUserContact (query.getUserId ()); vratiti userContact.getContactByType () .get (query.getContactType ()); } javna postavka za postavljanje (AddressByRegionQuery upit) {UserAddress userAddress = readRepository.getUserAddress (query.getUserId ()); vratiti userAddress.getAddressByRegion () .get (query.getState ()); }}

Projekcija ovdje koristi spremište za čitanje koje smo prethodno definirali za adresiranje upita koje imamo. Ovim se u velikoj mjeri zaključuje i pročitana strana naše prijave.

4.3. Sinkronizacija podataka za čitanje i pisanje

Jedan dio ove zagonetke još uvijek nije riješen: nema se što sinkronizirajte naša spremišta za pisanje i čitanje.

Ovdje će nam trebati nešto poznato kao projektor. A projektor ima logiku projicirati model domene zapisivanja u model domene čitanja.

Postoje mnogo sofisticiraniji načini da se to riješi, ali ostat ćemo relativno jednostavni:

javna klasa UserProjector {UserReadRepository readRepository = new UserReadRepository (); javni UserProjector (UserReadRepository readRepository) {this.readRepository = readRepository; } javni void projekt (Korisnički korisnik) {UserContact userContact = Izborno.ofNullable (readRepository.getUserContact (user.getUserid ())) .orElse (novi UserContact ()); Karta contactByType = novi HashMap (); for (Contact contact: user.getContacts ()) {Set contacts = Optional.ofNullable (contactByType.get (contact.getType ())) .orElse (novi HashSet ()); contacts.add (kontakt); contactByType.put (contact.getType (), kontakti); } userContact.setContactByType (contactByType); readRepository.addUserContact (user.getUserid (), userContact); UserAddress userAddress = Izborno.ofNullable (readRepository.getUserAddress (user.getUserid ())) .orElse (nova UserAddress ()); Karta addressByRegion = novi HashMap (); za (Adresa adresa: user.getAddresses ()) {Postavi adrese = Neobavezno.ofNullable (addressByRegion.get (address.getState ())) .orElse (novi HashSet ()); adrese.add (adresa); addressByRegion.put (address.getState (), adrese); } userAddress.setAddressByRegion (addressByRegion); readRepository.addUserAddress (user.getUserid (), userAddress); }}

Ovo je prije vrlo grub način za to, ali daje nam dovoljno uvida u ono što je potrebno kako bi CQRS funkcionirao. Štoviše, nije potrebno da spremišta za čitanje i pisanje sjede u različitim fizičkim trgovinama. Distribuirani sustav ima svoj dio problema!

Imajte na umu da je nije prikladno projicirati trenutno stanje domene zapisivanja u različite modele domene čitanja. Primjer koji smo ovdje uzeli prilično je jednostavan, stoga ne vidimo problem.

Međutim, kako modeli za pisanje i čitanje postaju sve složeniji, postat će sve teže projicirati. To možemo riješiti projekcijom temeljenom na događajima umjesto projekcijom utemeljenom na državi s izvorom događaja. Vidjet ćemo kako to postići kasnije u vodiču.

4.4. Prednosti i nedostaci CQRS-a

Raspravljali smo o uzorku CQRS i naučili kako ga uvesti u tipičnu aplikaciju. Kategorički smo pokušali riješiti problem koji se odnosi na krutost modela domene u rukovanju i čitanjem i upisivanjem.

Razmotrimo sada neke druge prednosti koje CQRS donosi arhitekturi aplikacije:

  • CQRS nam pruža prikladan način odabira zasebnih modela domena prikladno za operacije pisanja i čitanja; ne moramo stvoriti složeni model domene koji podržava oboje
  • Pomaže nam odaberite spremišta koja odgovaraju pojedinačno za rješavanje složenosti operacija čitanja i pisanja, poput velike propusnosti za pisanje i male latencije za čitanje
  • To prirodno nadopunjuje programske modele utemeljene na događajima u distribuiranoj arhitekturi pružajući razdvajanje problema kao i jednostavnije modele domena

Međutim, ovo ne dolazi besplatno. Kao što je vidljivo iz ovog jednostavnog primjera, CQRS dodaje značajnu složenost arhitekturi. Možda neće biti prikladno ili vrijedno boli u mnogim scenarijima:

  • Samo složeni model domene može imati koristi zbog dodatne složenosti ovog uzorka; jednostavnim modelom domene može se upravljati bez svega toga
  • Prirodno dovodi do dupliciranja koda donekle, što je prihvatljivo zlo u odnosu na dobitak do kojeg nas vodi; međutim savjetuje se individualna prosudba
  • Odvojena spremišta dovesti do problema dosljednosti, i teško je održavati spremišta za pisanje i čitanje uvijek u savršenoj sinkronizaciji; često se moramo zadovoljiti eventualnom dosljednošću

5. Predstavljamo izvor izvora

Dalje ćemo se pozabaviti drugim problemom o kojem smo razgovarali u našoj jednostavnoj aplikaciji. Ako se prisjećamo, to je bilo povezano s našim spremištem postojanosti.

Za rješavanje ovog problema predstavit ćemo izvor događaja. Izvori događaja dramatično mijenja način na koji razmišljamo o pohrani stanja aplikacije.

Pogledajmo kako mijenja naše spremište:

Evo, strukturirali smo naše spremište za pohranu uređenog popisa događaja domene. Svaka promjena objekta domene smatra se događajem. Koliko bi događaj trebao biti grub ili sitan, pitanje je dizajna domene. Ovdje je važno uzeti u obzir sljedeće događaji imaju vremenski poredak i nepromjenjivi su.

5.1. Implementacija događaja i trgovine događaja

Temeljni objekti u aplikacijama vođenim događajima su događaji, a izvor događaja se ne razlikuje. Kao što smo vidjeli ranije, događaji predstavljaju specifičnu promjenu stanja modela domene u određenom trenutku. Dakle, započet ćemo definiranjem osnovnog događaja za našu jednostavnu aplikaciju:

javni sažetak klase Event {javni konačni UUID id = UUID.randomUUID (); javni konačni datum kreiran = novi datum (); }

To samo osigurava da svaki događaj koji generiramo u našoj aplikaciji dobije jedinstvenu identifikaciju i vremensku oznaku stvaranja. Oni su neophodni za njihovu daljnju obradu.

Naravno, može biti nekoliko drugih atributa koji bi nas mogli zanimati, poput atributa za utvrđivanje porijekla događaja.

Dalje, kreirajmo neke događaje specifične za domenu nasljeđujući iz ovog osnovnog događaja:

javna klasa UserCreatedEvent proširuje Event {private String userId; private String firstName; private String lastName; } javna klasa UserContactAddedEvent proširuje Event {private String contactType; private String contactDetails; } javna klasa UserContactRemovedEvent proširuje Event {private String contactType; private String contactDetails; } javna klasa UserAddressAddedEvent proširuje Event {private String city; privatna država gudača; privatni niz postCode; } javna klasa UserAddressRemovedEvent proširuje Event {private String city; privatna država gudača; privatni niz postCode; }

To su jednostavni POJO-ovi u Javi koji sadrže detalje događaja domene. Međutim, ovdje je najvažnija stvar koju treba primijetiti je zrnatost događaja.

Mogli smo stvoriti jedan događaj za korisnička ažuriranja, ali umjesto toga odlučili smo stvoriti zasebne događaje za dodavanje i uklanjanje adrese i kontakta. Izbor se preslikava na ono što čini učinkovitiji rad s modelom domene.

Sada nam je naravno potrebno spremište za održavanje događaja naše domene:

javna klasa EventStore {privatna karta trgovina = nova HashMap (); }

Ovo je jednostavna struktura podataka u memoriji za održavanje događaja naše domene. U stvarnosti, postoji nekoliko rješenja posebno kreiranih za rukovanje podacima o događajima poput Apache Druida. Postoji mnogo distribuiranih spremišta podataka opće namjene sposobnih za upravljanje izvorima događaja, uključujući Kafku i Cassandru.

5.2. Generiranje i potrošnja događaja

Dakle, sada će se promijeniti naša usluga koja je rješavala sve CRUD operacije. Sada će, umjesto ažuriranja pokretnog stanja domene, dodati događaje domene. Također će koristiti iste događaje domene za odgovaranje na upite.

Pogledajmo kako to možemo postići:

javna klasa UserService {privatno spremište EventStore; javna UserService (spremište EventStore) {this.repository = spremište; } javna void createUser (String userId, String firstName, String lastName) {repository.addEvent (userId, new UserCreatedEvent (userId, firstName, lastName)); } javna void updateUser (niz userId, Postavi kontakte, Postavi adrese) {User user = UserUtility.recreateUserState (spremište, userId); user.getContacts (). stream () .filter (c ->! contacts.contens (c)) .forEach (c -> repository.addEvent (userId, new UserContactRemovedEvent (c.getType (), c.getDetail ()) )); contacts.stream () .filter (c ->! user.getContacts (). sadrži (c)) .forEach (c -> repository.addEvent (userId, new UserContactAddedEvent (c.getType (), c.getDetail ()) )); user.getAddresses (). stream () .filter (a ->! adrese. sadrži (a)) .forEach (a -> repository.addEvent (userId, new UserAddressRemovedEvent (a.getCity (), a.getState (), a.getPostcode ()))); adrese.stream () .filter (a ->! user.getAddresses (). sadrži (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 (spremište, userId); vratiti user.getContacts (). stream () .filter (c -> c.getType (). jednako (contactType)) .collect (Collectors.toSet ()); } javni skup getAddressByRegion (niz userId, stanje niza) baca iznimku {User user = UserUtility.recreateUserState (spremište, userId); vratiti user.getAddresses (). stream () .filter (a -> a.getState (). jednako (stanje)) .collect (Collectors.toSet ()); }}

Imajte na umu da generiramo nekoliko događaja kao dio rukovanja korisničkom operacijom ažuriranja ovdje. Također, zanimljivo je primijetiti kako smo generiranje trenutnog stanja modela domene ponovnim prikazivanjem svih do sada generiranih događaja domene.

Naravno, u stvarnoj aplikaciji to nije izvediva strategija, a morat ćemo održavati lokalnu predmemoriju kako bismo izbjegli generiranje stanja svaki put. Postoje i druge strategije poput snimki i sažimanja u spremištu događaja koje mogu ubrzati postupak.

Ovim je završen naš napor da uvodimo izvor događaja u našu jednostavnu aplikaciju.

5.3. Prednosti i nedostaci izvora događaja

Sada smo uspješno usvojili zamjenski način spremanja objekata domene pomoću izvora događaja. Izvor događaja moćan je obrazac i donosi puno prednosti arhitekturi aplikacije ako se koristi na odgovarajući način:

  • Pravi operacije pisanja mnogo brže jer nije potrebno čitanje, ažuriranje i pisanje; write je samo dodavanje događaja u dnevnik
  • Uklanja objektno-relacijsku impedansu i, prema tome, potreba za složenim alatima za mapiranje; naravno, još uvijek moramo ponovno stvoriti predmete
  • Događa se dostaviti dnevnik revizije kao nusproizvod, koji je potpuno pouzdan; možemo ispraviti pogreške kako se promijenilo stanje modela domene
  • Omogućuje podržati vremenske upite i postići putovanje kroz vrijeme (stanje domene u prošlosti)!
  • To je prirodno pogodan za projektiranje labavo spojenih komponenata u arhitekturi mikrousluga koje asinkrono komuniciraju razmjenom poruka

Međutim, kao i uvijek, čak i izvor događaja nije srebrni metak. Prisiljava nas na dramatično drugačiji način pohrane podataka. To se možda neće pokazati korisnim u nekoliko slučajeva:

  • Eto povezana krivulja učenja i potreban pomak u razmišljanju usvojiti izvor događaja; za početak nije intuitivno
  • Čini to prilično je teško obraditi tipične upite jer trebamo ponovno stvoriti državu ako je ne zadržimo u lokalnoj predmemoriji
  • Iako se može primijeniti na bilo koji model domene, to je prikladniji za model zasnovan na događajima u arhitekturi vođenoj događajima

6. CQRS s izvorom događaja

Sad kad smo vidjeli kako pojedinačno uvesti izvor događaja i CQRS u našu jednostavnu aplikaciju, vrijeme je da ih spojimo. Trebalo bi biti prilično intuitivno sada kad ti obrasci mogu imati velike koristi jedni od drugih. Međutim, u ovom ćemo dijelu to učiniti jasnijim.

Pogledajmo prvo kako ih arhitektura aplikacije spaja:

To do sada ne bi trebalo biti iznenađenje. Zamijenili smo stranu za pisanje spremišta za pohranu događaja, dok je strana za čitanje spremišta i dalje ista.

Napominjemo da ovo nije jedini način korištenja izvora događaja i CQRS u arhitekturi aplikacije. Mi mogu biti prilično inovativni i koristiti te uzorke zajedno s drugim uzorcima i smisliti nekoliko mogućnosti arhitekture.

Ovdje je važno osigurati da ih koristimo za upravljanje složenošću, a ne samo za daljnje povećanje složenosti!

6.1. Povezivanje CQRS-a i izvora događaja

Nakon što smo pojedinačno implementirali izvore događaja i CQRS, ne bi trebalo biti toliko teško razumjeti kako ih možemo povezati.

Dobro započnite s aplikacijom u kojoj smo uveli CQRS i samo napravimo relevantne promjene donijeti izvor događaja u nagib. Također ćemo iskoristiti iste događaje i trgovinu događaja koje smo definirali u našoj aplikaciji gdje smo uveli izvor događaja.

Postoji samo nekoliko promjena. Počet ćemo s promjenom agregata u generirati događaje umjesto ažuriranja stanja:

javna klasa UserAggregate {private EventStore writeRepository; javni UserAggregate (spremište EventStore) {this.writeRepository = spremište; } javni popis handleCreateUserCommand (naredba CreateUserCommand) {UserCreatedEvent događaj = novi UserCreatedEvent (command.getUserId (), command.getFirstName (), command.getLastName ()); writeRepository.addEvent (command.getUserId (), događaj); return Arrays.asList (događaj); } javni popis handleUpdateUserCommand (naredba UpdateUserCommand) {User user = UserUtility.recreateUserState (writeRepository, command.getUserId ()); Popis događaja = novi ArrayList (); Popis contactsToRemove = user.getContacts (). Stream () .filter (c ->! Command.getContacts (). Sadrži (c)) .collect (Collectors.toList ()); za (Kontakt za kontakt: contactsToRemove) {UserContactRemovedEvent contactRemovedEvent = new UserContactRemovedEvent (contact.getType (), contact.getDetail ()); events.add (contactRemovedEvent); writeRepository.addEvent (command.getUserId (), contactRemovedEvent); } Popis contactToAdd = command.getContacts (). Stream () .filter (c ->! User.getContacts (). Sadrži (c)) .collect (Collectors.toList ()); za (Kontakt za kontakt: contactsToAdd) {UserContactAddedEvent contactAddedEvent = novi UserContactAddedEvent (contact.getType (), contact.getDetail ()); events.add (contactAddedEvent); writeRepository.addEvent (command.getUserId (), contactAddedEvent); } // slično obrađujemo adreseToRemove // ​​slično obrađujemo adreseToAdd povratne događaje; }}

Jedina druga potrebna promjena je na projektoru, koji sada treba obrađuju događaje umjesto stanja objektnih domena:

javna klasa UserProjector {UserReadRepository readRepository = new UserReadRepository (); javni UserProjector (UserReadRepository readRepository) {this.readRepository = readRepository; } javni void projekt (String userId, Popis događaja) {for (Event event: events) {if (primjerak događaja UserAddressAddedEvent) primjenjuje se (userId, (UserAddressAddedEvent) event); ako se (primjerak događaja UserAddressRemovedEvent) primijeni (userId, (UserAddressRemovedEvent) događaj); ako se (primjerak događaja UserContactAddedEvent) primjenjuje (userId, (UserContactAddedEvent) događaj); ako se (primjerak događaja UserContactRemovedEvent) primijeni (userId, (UserContactRemovedEvent) događaj); }} primjenjuje se javna void (String userId, UserAddressAddedEvent event) {Adresa adresa = nova adresa (event.getCity (), event.getState (), event.getPostCode ()); UserAddress userAddress = Izborno.ofNullable (readRepository.getUserAddress (userId)) .orElse (nova UserAddress ()); Postavi adrese = Neobvezno.ofNullable (userAddress.getAddressByRegion () .get (address.getState ())) .orElse (novi HashSet ()); adrese.add (adresa); userAddress.getAddressByRegion () .put (address.getState (), adrese); readRepository.addUserAddress (userId, userAddress); } primjenjuje se javna praznina (String userId, UserAddressRemovedEvent event) {Address address = nova adresa (event.getCity (), event.getState (), event.getPostCode ()); UserAddress userAddress = readRepository.getUserAddress (userId); if (userAddress! = null) {Postavi adrese = userAddress.getAddressByRegion () .get (address.getState ()); if (adrese! = null) adrese.remove (adresa); readRepository.addUserAddress (userId, userAddress); }} javna praznina se primjenjuje (String userId, UserContactAddedEvent event) {// Slično obrađuje UserContactAddedEvent event} javna praznina primjenjuje se (String userId, UserContactRemovedEvent event) {// Slično obrađuje UserContactRemovedEvent event}}

Ako se prisjetimo problema o kojima smo razgovarali tijekom rukovanja državnom projekcijom, ovo je potencijalno rješenje za to.

The projekcija zasnovana na događajima prilično je prikladna i lakša za provedbu. Sve što moramo učiniti je obraditi sve događaje domene koji se događaju i primijeniti ih na sve modele pročitane domene. Tipično, u aplikaciji koja se temelji na događajima, projektor će slušati događaje domene koja ga zanima i ne bi se oslanjao na to da ga netko izravno zove.

To je gotovo sve što moramo učiniti kako bismo u našoj jednostavnoj aplikaciji spojili izvor događaja i CQRS.

7. Zaključak

U ovom uputstvu raspravljali smo o osnovama izvora izvora i CQRS dizajnerskih obrazaca. Razvili smo jednostavnu aplikaciju i na nju pojedinačno primijenili ove uzorke.

U tom smo procesu shvatili prednosti koje oni donose i nedostatke koje oni predstavljaju. Napokon, shvatili smo zašto i kako oba ova uzorka zajedno ugraditi u našu aplikaciju.

Jednostavna aplikacija o kojoj smo govorili u ovom vodiču ni izbliza ne opravdava potrebu za CQRS-om i izvorom događaja. Cilj nam je bio razumjeti osnovne pojmove, pa je primjer bio trivijalan. No, kao što je već spomenuto, korist od ovih obrazaca može se ostvariti samo u aplikacijama koje imaju relativno složen model domene.

Kao i obično, izvorni kod za ovaj članak može se naći na GitHubu.