CQRS og hendelsessourcing i Java

1. Introduksjon

I denne opplæringen vil vi utforske de grunnleggende konseptene i Command Query Responsibility Segregation (CQRS) og Event Sourcing designmønstre.

Selv om de ofte blir sitert som komplementære mønstre, vil vi prøve å forstå dem hver for seg og til slutt se hvordan de utfyller hverandre. Det er flere verktøy og rammer, for eksempel Axon, for å hjelpe til med å vedta disse mønstrene, men vi lager en enkel applikasjon i Java for å forstå det grunnleggende.

2. Grunnleggende konsepter

Vi vil først forstå disse mønstrene teoretisk før vi prøver å implementere dem. Siden de fremstår som individuelle mønstre ganske bra, vil vi prøve å forstå uten å blande dem.

Vær oppmerksom på at disse mønstrene ofte brukes sammen i en bedriftsapplikasjon. I denne forbindelse har de også nytte av flere andre arkitektoniske mønstre. Vi vil diskutere noen av dem når vi går videre.

2.1. Arrangementssourcing

Arrangementssourcing gir oss en ny måte å vedvare applikasjonstilstanden som en ordnet hendelsesforløp. Vi kan selektivt spørre om disse hendelsene og rekonstruere applikasjonens tilstand når som helst. Selvfølgelig, for å få dette til å fungere, må vi gjenta alle endringer i tilstanden til applikasjonen som hendelser:

Disse hendelsene her er fakta som har skjedd og som ikke kan endres - med andre ord, de må være uforanderlige. Å gjenskape søknadstilstanden er bare et spørsmål om å spille av alle hendelsene på nytt.

Merk at dette også åpner for muligheten til å spille av hendelser selektivt, spille noen hendelser omvendt og mye mer. Som en konsekvens kan vi behandle applikasjonstilstanden som en sekundærborger, med hendelsesloggen som vår primære kilde til sannhet.

2.2. CQRS

Enkelt sagt, CQRS er det om å adskille kommando- og spørresiden til applikasjonsarkitekturen. CQRS er basert på CQS-prinsippet (Command Query Separation) som ble foreslått av Bertrand Meyer. CQS foreslår at vi deler operasjonene på domeneobjekter i to forskjellige kategorier: Spørringer og kommandoer:

Spørringer returnerer et resultat og endrer ikke den observerbare tilstanden av et system. Kommandoer endrer tilstanden til systemet, men returnerer ikke nødvendigvis en verdi.

Vi oppnår dette ved å skille mellom kommandosiden og spørringssiden av domenemodellen. Vi kan selvfølgelig ta et skritt videre ved å dele opp skrive- og lesesiden av datalageret ved å innføre en mekanisme for å holde dem synkronisert.

3. En enkel applikasjon

Vi begynner med å beskrive et enkelt program i Java som bygger en domenemodell.

Applikasjonen vil tilby CRUD-operasjoner på domenemodellen og vil også inneholde en utholdenhet for domenenettene. CRUD står for Create, Read, Update og Delete, som er grunnleggende operasjoner som vi kan utføre på et domeneobjekt.

Vi bruker samme applikasjon til å introdusere Event Sourcing og CQRS i senere seksjoner.

I prosessen vil vi utnytte noen av konseptene fra Domain-Driven Design (DDD) i vårt eksempel.

DDD adresserer analyse og design av programvare som er avhengig av kompleks domenespesifikk kunnskap. Den bygger på ideen om at programvaresystemer må baseres på en velutviklet modell av et domene. DDD ble først foreskrevet av Eric Evans som en mønsterkatalog. Vi bruker noen av disse mønstrene for å bygge eksemplet vårt.

3.1. Søknadsoversikt

Å lage en brukerprofil og administrere den er et typisk krav i mange applikasjoner. Vi definerer en enkel domenemodell som fanger brukerprofilen sammen med en utholdenhet:

Som vi kan se, er domenemodellen vår normalisert og avslører flere CRUD-operasjoner. Disse operasjonene er bare for demonstrasjon og kan være enkel eller kompleks avhengig av kravene. Videre kan utholdenhetsregisteret her være i minnet eller bruke en database i stedet.

3.2. Søknadsimplementering

Først må vi lage Java-klasser som representerer domenemodellen vår. Dette er en ganske enkel domenemodell og krever kanskje ikke engang kompleksiteten i designmønstre som Event Sourcing og CQRS. Imidlertid vil vi holde dette enkelt for å fokusere på å forstå det grunnleggende:

public class User {private String userid; privat streng fornavn; privat streng etternavn; private Sett kontakter; private Sett adresser; // getters and setters} public class Contact {private String type; private strengdetaljer; // getters and setters} public class Address {private String city; privat strengstat; privat String postnummer; // getters og setters}

Vi definerer også et enkelt lager i minnet for vedvarende applikasjonstilstand. Dette gir selvfølgelig ingen verdi, men er nok for demonstrasjonen vår senere:

public class UserRepository {private Map store = new HashMap (); }

Nå vil vi definere en tjeneste for å eksponere typiske CRUD-operasjoner på domenemodellen vår:

offentlig klasse UserService {private UserRepository repository; offentlig 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, user); } public void updateUser (String userId, Set contacts, Set addresses) {User user = repository.getUser (userId); user.setContacts (kontakter); user.setAddresses (adresser); repository.addUser (userId, user); } public Set getContactByType (String userId, String contactType) {User user = repository.getUser (userId); Angi kontakter = user.getContacts (); return contacts.stream () .filter (c -> c.getType (). tilsvarer (contactType)) .collect (Collectors.toSet ()); } public Set getAddressByRegion (String userId, String state) {User user = repository.getUser (userId); Sett adresser = user.getAddresses (); returadresser.stream () .filter (a -> a.getState (). er lik (tilstand)) .collect (Collectors.toSet ()); }}

Det er ganske mye det vi må gjøre for å sette opp vår enkle applikasjon. Dette er langt fra å være produksjonsklar kode, men det avslører noen av de viktige punktene som vi skal diskutere senere i denne opplæringen.

3.3. Problemer i denne applikasjonen

Før vi fortsetter i diskusjonen med Event Sourcing og CQRS, er det verdt å diskutere problemene med den nåværende løsningen. Tross alt vil vi løse de samme problemene ved å bruke disse mønstrene!

Av mange problemer som vi kanskje legger merke til her, vil vi bare fokusere på to av dem:

  • Domenemodell: Les og skriv-operasjonene skjer over den samme domenemodellen. Selv om dette ikke er et problem for en enkel domenemodell som dette, kan det forverres ettersom domenemodellen blir kompleks. Det kan hende at vi må optimalisere domenemodellen vår og den underliggende lagringen for å passe de individuelle behovene for lese- og skriveoperasjonene.
  • Standhaftighet: Den utholdenheten vi har for domeneobjektene våre, lagrer bare den nyeste tilstanden til domenemodellen. Selv om dette er tilstrekkelig i de fleste situasjoner, gjør det noen oppgaver utfordrende. For eksempel, hvis vi må utføre en historisk revisjon av hvordan domeneobjektet har endret tilstand, er det ikke mulig her. Vi må supplere løsningen med noen revisjonslogger for å oppnå dette.

4. Introduksjon av CQRS

Vi begynner å løse det første problemet vi diskuterte i forrige avsnitt ved å introdusere CQRS-mønsteret i applikasjonen vår. Som en del av dette, vi skiller domenemodellen og dens utholdenhet for å håndtere skrive- og leseoperasjoner. La oss se hvordan CQRS-mønsteret restrukturerer applikasjonen vår:

Diagrammet her forklarer hvordan vi har til hensikt å skille applikasjonsarkitekturen vår for å skrive og lese sider. Imidlertid har vi introdusert ganske mange nye komponenter her som vi må forstå bedre. Vær oppmerksom på at disse ikke er strengt relatert til CQRS, men CQRS har stor fordel av dem:

  • Aggregat / Aggregator:

Aggregat er et mønster beskrevet i Domain-Driven Design (DDD) som logisk grupperer forskjellige enheter ved å binde enheter til en samlet rot. Det samlede mønsteret gir transaksjonskonsistens mellom enhetene.

CQRS drar naturlig fordel av det samlede mønsteret, som grupperer skrive-domenemodellen, og gir transaksjonsgarantier. Aggregater har normalt en hurtigbufret tilstand for bedre ytelse, men kan fungere perfekt uten den.

  • Projeksjon / projektor:

Projeksjon er et annet viktig mønster som er til stor fordel for CQRS. Projeksjon betyr egentlig å representere domeneobjekter i forskjellige former og strukturer.

Disse fremskrivningene av originaldata er skrivebeskyttet og svært optimalisert for å gi en forbedret leseopplevelse. Vi kan igjen bestemme oss for å cache anslag for bedre ytelse, men det er ikke en nødvendighet.

4.1. Implementering av skrivesiden av applikasjonen

La oss først implementere skrivesiden av applikasjonen.

Vi begynner med å definere de nødvendige kommandoene. EN kommando er en intensjon om å mutere tilstanden til domenemodellen. Om det lykkes eller ikke, avhenger av forretningsreglene vi konfigurerer.

La oss se kommandoene våre:

offentlig klasse CreateUserCommand {private String userId; privat streng fornavn; privat streng etternavn; } Offentlig klasse UpdateUserCommand {private String userId; private Sett adresser; private Sett kontakter; }

Dette er ganske enkle klasser som inneholder dataene vi har tenkt å mutere.

Deretter definerer vi et aggregat som er ansvarlig for å ta kommandoer og håndtere dem. Aggregater kan godta eller avvise en kommando:

offentlig klasse UserAggregate {private UserWriteRepository writeRepository; public UserAggregate (UserWriteRepository repository) {this.writeRepository = repository; } offentlig bruker handleCreateUserCommand (CreateUserCommand-kommando) {User user = new User (command.getUserId (), command.getFirstName (), command.getLastName ()); writeRepository.addUser (user.getUserid (), bruker); retur bruker; } offentlig bruker handleUpdateUserCommand (kommando UpdateUserCommand) {Bruker bruker = writeRepository.getUser (command.getUserId ()); user.setAddresses (command.getAddresses ()); user.setContacts (command.getContacts ()); writeRepository.addUser (user.getUserid (), bruker); retur bruker; }}

Aggregatet bruker et depot for å hente den nåværende tilstanden og vedvare eventuelle endringer i den. Videre kan den lagre den nåværende tilstanden lokalt for å unngå tur-retur-kostnadene til et depot mens den behandler hver kommando.

Til slutt trenger vi et lager for å holde tilstanden til domenemodellen. Dette vil vanligvis være en database eller annen holdbar butikk, men her vil vi ganske enkelt erstatte dem med en datastruktur i minnet:

public class UserWriteRepository {private Map store = new HashMap (); // aksessorer og mutatorer}

Dette avslutter skrivesiden av søknaden vår.

4.2. Implementering av lesesiden av applikasjonen

La oss bytte til lesesiden av applikasjonen nå. Vi begynner med å definere lesesiden av domenemodellen:

offentlig klasse UserAddress {private Map addressByRegion = ny HashMap (); } UserContact i offentlig klasse {private Map contactByType = ny HashMap (); }

Hvis vi husker våre leseoperasjoner, er det ikke vanskelig å se at disse klassene kartlegger perfekt for å håndtere dem. Det er skjønnheten ved å lage en domenemodell sentrert rundt spørsmål vi har.

Deretter definerer vi leselageret. Igjen, vi bruker bare en datastruktur i minnet, selv om dette vil være en mer holdbar datalager i ekte applikasjoner:

offentlig klasse UserReadRepository {private Map userAddress = new HashMap (); private Map userContact = nye HashMap (); // aksessorer og mutatorer}

Nå vil vi definere de nødvendige spørsmålene vi må støtte. Et spørsmål er en intensjon om å få data - det kan ikke nødvendigvis føre til data.

La oss se spørsmålene våre:

offentlig klasse ContactByTypeQuery {private String userId; private String contactType; } public class AddressByRegionQuery {private String userId; privat strengstat; }

Igjen, dette er enkle Java-klasser som inneholder dataene for å definere et spørsmål.

Det vi trenger nå er en projeksjon som kan håndtere disse spørsmålene:

offentlig klasse UserProjection {private UserReadRepository readRepository; offentlig UserProjection (UserReadRepository readRepository) {this.readRepository = readRepository; } public Set handle (ContactByTypeQuery query) {UserContact userContact = readRepository.getUserContact (query.getUserId ()); returner userContact.getContactByType () .get (query.getContactType ()); } public Set handle (AddressByRegionQuery query) {UserAddress userAddress = readRepository.getUserAddress (query.getUserId ()); returner userAddress.getAddressByRegion () .get (query.getState ()); }}

Projeksjonen her bruker leselageret vi definerte tidligere for å adressere spørsmålene vi har. Dette avslutter ganske mye lesesiden av søknaden vår også.

4.3. Synkronisering av lese- og skrivedata

Ett stykke av dette puslespillet er fortsatt ikke løst: det er ingenting å synkronisere våre skrive- og leseoppbevaringssteder.

Det er her vi trenger noe kjent som en projektor. EN projektoren har logikken til å projisere skrivedomenemodellen i den lest domenemodellen.

Det er mye mer sofistikerte måter å håndtere dette på, men vi holder det relativt enkelt:

offentlig klasse UserProjector {UserReadRepository readRepository = ny UserReadRepository (); offentlig UserProjector (UserReadRepository readRepository) {this.readRepository = readRepository; } public void project (User user) {UserContact userContact = Optional.ofNullable (readRepository.getUserContact (user.getUserid ())) .orElse (new UserContact ()); Kart contactByType = ny HashMap (); for (Contact contact: user.getContacts ()) {Set contacts = Optional.ofNullable (contactByType.get (contact.getType ())) .orElse (new HashSet ()); contacts.add (kontakt); contactByType.put (contact.getType (), kontakter); } userContact.setContactByType (contactByType); readRepository.addUserContact (user.getUserid (), userContact); UserAddress userAddress = Optional.ofNullable (readRepository.getUserAddress (user.getUserid ())). OrElse (new UserAddress ()); Kart addressByRegion = ny HashMap (); for (Adresse-adresse: user.getAddresses ()) {Angi adresser = Valgfri.avNullable (addressByRegion.get (address.getState ())) .ellerElse (ny HashSet ()); adresser.add (adresse); addressByRegion.put (address.getState (), adresser); } userAddress.setAddressByRegion (addressByRegion); readRepository.addUserAddress (user.getUserid (), userAddress); }}

Dette er heller en veldig rå måte å gjøre dette på, men gir oss nok innsikt i hva som trengs for at CQRS skal fungere. Videre er det ikke nødvendig å ha lese- og skriveoppbevaringsstedene i forskjellige fysiske butikker. Et distribuert system har sin egen andel av problemer!

Vær oppmerksom på at det er ikke praktisk å projisere den nåværende tilstanden til skrivedomenet i forskjellige leste domenemodeller. Eksemplet vi har tatt her er ganske enkelt, derfor ser vi ikke problemet.

Etter hvert som skrive- og lesemodellene blir mer komplekse, blir det imidlertid vanskeligere å projisere. Vi kan løse dette gjennom hendelsesbasert projeksjon i stedet for statsbasert projeksjon med hendelsessourcing. Vi får se hvordan du kan oppnå dette senere i opplæringen.

4.4. Fordeler og ulemper ved CQRS

Vi diskuterte CQRS-mønsteret og lærte å introdusere det i en typisk applikasjon. Vi har kategorisk prøvd å løse problemet knyttet til stivheten til domenemodellen i håndtering av både lese og skrive.

La oss nå diskutere noen av de andre fordelene som CQRS gir til en applikasjonsarkitektur:

  • CQRS gir oss en praktisk måte å velge separate domenemodeller på passende for skrive- og leseoperasjoner; vi trenger ikke lage en kompleks domenemodell som støtter begge deler
  • Det hjelper oss å velg arkiver som passer individuelt for å håndtere kompleksiteten i lese- og skriveoperasjonene, som høy gjennomstrømning for skriving og lav latens for lesing
  • Det naturlig utfyller hendelsesbaserte programmeringsmodeller i en distribuert arkitektur ved å gi en separasjon av bekymringer så vel som enklere domenemodeller

Dette kommer imidlertid ikke gratis. Som det fremgår av dette enkle eksempelet, tilfører CQRS arkitekturen betydelig kompleksitet. Det er kanskje ikke passende eller verdt smerten i mange scenarier:

  • Kun en kompleks domenemodell kan være til nytte fra den ekstra kompleksiteten til dette mønsteret; en enkel domenemodell kan håndteres uten alt dette
  • Naturlig fører til kodekopiering til en viss grad, noe som er en akseptabel ondskap i forhold til gevinsten den fører oss til; det anbefales imidlertid individuell vurdering
  • Separate arkiver føre til problemer med konsistens, og det er vanskelig å alltid skrive og lese arkiver i perfekt synkronisering; vi må ofte nøye oss med eventuell konsistens

5. Introduksjon av hendelsessourcing

Deretter skal vi ta opp det andre problemet vi diskuterte i vår enkle applikasjon. Hvis vi husker, var det relatert til vårt utholdenhetsregister.

Vi introduserer hendelsessourcing for å løse dette problemet. Arrangementssourcing dramatisk endrer måten vi tenker på applikasjonsstatuslagringen.

La oss se hvordan det endrer depotet vårt:

Her har vi strukturert vårt lager for å lagre en bestilt liste over domenehendelser. Hver endring av domeneobjektet betraktes som en hendelse. Hvor grov eller finkornet en begivenhet skal være, er et spørsmål om domenedesign. De viktige tingene du bør vurdere her er at hendelser har en tidsmessig orden og er uforanderlige.

5.1. Implementering av arrangementer og eventbutikk

De grunnleggende objektene i hendelsesdrevne applikasjoner er hendelser, og innkjøp av hendelser er ikke annerledes. Som vi har sett tidligere, hendelser representerer en spesifikk endring i domenemodellens tilstand på et bestemt tidspunkt. Så vi begynner med å definere basishendelsen for vår enkle applikasjon:

public abstract class Event {public final UUID id = UUID.randomUUID (); offentlig finale Dato opprettet = ny dato (); }

Dette sikrer bare at hver hendelse vi genererer i applikasjonen vår får en unik identifikasjon og tidsstempelet for opprettelsen. Disse er nødvendige for å behandle dem videre.

Selvfølgelig kan det være flere andre attributter som kan interessere oss, som et attributt for å etablere herkomst til et arrangement.

La oss deretter lage noen domenespesifikke hendelser som arver fra denne basishendelsen:

offentlig klasse UserCreatedEvent utvider hendelsen {privat streng bruker-ID; privat streng fornavn; privat streng etternavn; } offentlig klasse UserContactAddedEvent utvider hendelse {private String contactType; privat streng kontaktdetaljer; } offentlig klasse UserContactRemovedEvent utvider hendelse {private String contactType; privat streng kontaktdetaljer; } offentlig klasse UserAddressAddedEvent utvider hendelse {privat strengby; privat strengstat; private String postCode; } offentlig klasse UserAddressRemovedEvent utvider hendelse {privat strengby; privat strengstat; private String postCode; }

Dette er enkle POJOer i Java som inneholder detaljene for domenhendelsen. Imidlertid er det viktige å merke seg her granulariteten til hendelsene.

Vi kunne ha opprettet en enkelt hendelse for brukeroppdateringer, men i stedet bestemte vi oss for å lage separate hendelser for tillegg og fjerning av adresse og kontakt. Valget er kartlagt til det som gjør det mer effektivt å jobbe med domenemodellen.

Nå trenger vi naturlig nok et depot for å holde domenhendelsene våre:

offentlig klasse EventStore {privat kart butikk = ny HashMap (); }

Dette er en enkel datastruktur i minnet for å holde domenehendelsene våre. I virkeligheten, det er flere løsninger spesielt laget for å håndtere hendelsesdata som Apache Druid. Det er mange distribuerte datalagre for allmenn bruk som kan håndtere innkjøp av arrangementer, inkludert Kafka og Cassandra.

5.2. Generere og forbruke hendelser

Så, nå vil tjenesten vår som håndterer alle CRUD-operasjoner endres. Nå, i stedet for å oppdatere en domenetilstand i bevegelse, vil den legge til domenehendelser. Den vil også bruke de samme domenehendelsene for å svare på spørsmål.

La oss se hvordan vi kan oppnå dette:

offentlig klasse UserService {private EventStore repository; public UserService (EventStore repository) {this.repository = repository; } public void createUser (String userId, String firstName, String lastName) {repository.addEvent (userId, new UserCreatedEvent (userId, firstName, lastName)); } public void updateUser (String userId, Angi kontakter, Angi adresser) {User user = UserUtility.recreateUserState (repository, 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 (). inneholder (c)) .forEach (c -> repository.addEvent (userId, new UserContactAddedEvent (c.getType (), c.getDetail ()) )); user.getAddresses (). stream () .filter (a ->! adresser. inneholder (a)). forEach (a -> repository.addEvent (userId, new UserAddressRemovedEvent (a.getCity (), a.getState (), a.getPostcode ()))); adresser.stream () .filter (a ->! user.getAddresses (). inneholder (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 (repository, userId); returner user.getContacts (). stream () .filter (c -> c.getType (). er lik (contactType)) .collect (Collectors.toSet ()); } public Set getAddressByRegion (String userId, String state) kaster Unntak {User user = UserUtility.recreateUserState (repository, userId); returner user.getAddresses (). stream () .filter (a -> a.getState (). er lik (tilstand)) .collect (Collectors.toSet ()); }}

Vær oppmerksom på at vi genererer flere hendelser som en del av håndteringen av oppdateringsbrukeroperasjonen her. Det er også interessant å merke seg hvordan vi har det generere den nåværende tilstanden til domenemodellen ved å spille av alle domenehendelsene som er generert så langt.

Selvfølgelig, i en reell applikasjon, er dette ikke en gjennomførbar strategi, og vi må opprettholde en lokal cache for å unngå å generere staten hver gang. Det er andre strategier som øyeblikksbilder og opprulling i hendelsesregisteret som kan øke hastigheten på prosessen.

Dette avslutter vårt forsøk på å innføre sourcing av hendelser i vår enkle applikasjon.

5.3. Fordeler og ulemper ved hendelseskjøp

Nå har vi vellykket tatt i bruk en alternativ måte å lagre domeneobjekter på ved hjelp av hendelsessourcing. Arrangementssourcing er et kraftig mønster og gir mange fordeler til en applikasjonsarkitektur hvis den brukes riktig:

  • Gjør at skrive operasjoner mye raskere ettersom det ikke kreves lese, oppdatere og skrive; skriv er bare å legge til en hendelse i en logg
  • Fjerner objektrelasjonsimpedansen og følgelig behovet for komplekse kartleggingsverktøy; selvfølgelig trenger vi fortsatt å gjenskape gjenstandene
  • Skjer med gi en revisjonslogg som et biprodukt, som er helt pålitelig; vi kan feilsøke nøyaktig hvordan tilstanden til en domenemodell har endret seg
  • Det gjør det mulig å støtte timelige spørsmål og oppnå tidsreiser (domenetilstanden på et tidspunkt i fortiden)!
  • Det er naturlig passer for design av løst koblede komponenter i en mikrotjenestearkitektur som kommuniserer asynkront ved å utveksle meldinger

Imidlertid, som alltid, er til og med sourcing ikke en sølvkule. Det tvinger oss til å vedta en dramatisk annen måte å lagre data på. Dette kan ikke vise seg å være nyttig i flere tilfeller:

  • Det er en tilhørende læringskurve og et tankeskift kreves å vedta sourcing; til å begynne med er det ikke intuitivt
  • Det gjør det ganske vanskelig å håndtere typiske spørsmål ettersom vi trenger å gjenskape staten med mindre vi holder staten i den lokale hurtigbufferen
  • Selv om den kan brukes på alle domenemodeller, er den mer passende for den hendelsesbaserte modellen i en hendelsesdrevet arkitektur

6. CQRS med hendelsessourcing

Nå som vi har sett hvordan vi enkelt kan introdusere Event Sourcing og CQRS til vår enkle applikasjon, er det på tide å bringe dem sammen. Det bør være ganske intuitivt nå som disse mønstrene kan ha stor nytte av hverandre. Imidlertid vil vi gjøre det mer eksplisitt i denne delen.

La oss først se hvordan applikasjonsarkitekturen bringer dem sammen:

Dette burde ikke være noen overraskelse nå. Vi har erstattet skrivesiden av depotet til å være en hendelsesbutikk, mens lesesiden av depotet fortsetter å være den samme.

Vær oppmerksom på at dette ikke er den eneste måten å bruke Event Sourcing og CQRS i applikasjonsarkitekturen. Vi kan være ganske nyskapende og bruke disse mønstrene sammen med andre mønstre og komme opp med flere arkitekturalternativer.

Det som er viktig her er å sikre at vi bruker dem til å håndtere kompleksiteten, ikke bare for å øke kompleksiteten ytterligere!

6.1. Å bringe CQRS og hendelsessourcing sammen

Etter å ha implementert Event Sourcing og CQRS hver for seg, bør det ikke være så vanskelig å forstå hvordan vi kan bringe dem sammen.

Vi vil begynn med applikasjonen der vi introduserte CQRS og bare gjør relevante endringer for å bringe innkjøp av arrangementer i folden. Vi vil også utnytte de samme hendelsene og hendelsesbutikken som vi definerte i applikasjonen vår der vi introduserte hendelsessalg.

Det er bare noen få endringer. Vi begynner med å endre aggregatet til generere hendelser i stedet for å oppdatere tilstanden:

offentlig klasse UserAggregate {private EventStore writeRepository; public UserAggregate (EventStore repository) {this.writeRepository = repository; } public List handleCreateUserCommand (CreateUserCommand command) {UserCreatedEvent event = new UserCreatedEvent (command.getUserId (), command.getFirstName (), command.getLastName ()); writeRepository.addEvent (command.getUserId (), hendelse); returnere Arrays.asList (hendelse); } offentlig liste handleUpdateUserCommand (UpdateUserCommand-kommando) {User user = UserUtility.recreateUserState (writeRepository, command.getUserId ()); Listehendelser = ny ArrayList (); Liste contactsToRemove = user.getContacts (). Stream () .filter (c ->! Command.getContacts (). Inneholder (c)) .collect (Collectors.toList ()); for (Kontaktkontakt: contactsToRemove) {UserContactRemovedEvent contactRemovedEvent = ny UserContactRemovedEvent (contact.getType (), contact.getDetail ()); events.add (contactRemovedEvent); writeRepository.addEvent (command.getUserId (), contactRemovedEvent); } Liste over contactToAdd = command.getContacts (). Stream () .filter (c ->! User.getContacts (). Inneholder (c)) .collect (Collectors.toList ()); for (Kontaktkontakt: contactsToAdd) {UserContactAddedEvent contactAddedEvent = ny UserContactAddedEvent (contact.getType (), contact.getDetail ()); events.add (contactAddedEvent); writeRepository.addEvent (command.getUserId (), contactAddedEvent); } // behandle adresser på samme måteToRemove // ​​behandle adresser på samme måteToAdd return events; }}

Den eneste andre endringen som kreves, er i projektoren, som nå må behandle hendelser i stedet for domeneobjektstatus:

offentlig klasse UserProjector {UserReadRepository readRepository = ny UserReadRepository (); offentlig 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); hvis (hendelsesinstans av UserAddressRemovedEvent) gjelder (userId, (UserAddressRemovedEvent) hendelse); hvis (hendelsesforekomst av UserContactAddedEvent) gjelder (userId, (UserContactAddedEvent) hendelse); hvis (hendelsesinstans av UserContactRemovedEvent) gjelder (userId, (UserContactRemovedEvent) hendelse); }} 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 (new UserAddress ()); Sett adresser = Valgfritt.ofNullable (userAddress.getAddressByRegion () .get (address.getState ())). Eller Else (ny HashSet ()); adresser.add (adresse); userAddress.getAddressByRegion () .put (address.getState (), adresser); readRepository.addUserAddress (userId, userAddress); } public void apply (String userId, UserAddressRemovedEvent event) {Address address = new Address (event.getCity (), event.getState (), event.getPostCode ()); UserAddress userAddress = readRepository.getUserAddress (userId); hvis (userAddress! = null) {Angi adresser = userAddress.getAddressByRegion () .get (address.getState ()); hvis (adresser! = null) adresser. fjern (adresse); readRepository.addUserAddress (userId, userAddress); }} public void apply (String userId, UserContactAddedEvent event) {// Handle på samme måte UserContactAddedEvent event} public void apply (String userId, UserContactRemovedEvent event) {// Behandler på samme måte UserContactRemovedEvent-hendelse}}

Hvis vi husker problemene vi diskuterte under håndtering av statsbasert projeksjon, er dette en potensiell løsning på det.

De hendelsesbasert projeksjon er ganske praktisk og enklere å implementere. Alt vi trenger å gjøre er å behandle alle forekommende domenehendelser og bruke dem på alle leste domenemodeller. Vanligvis, i et hendelsesbasert program, ville projektoren lytte til domenearrangementer den er interessert i, og vil ikke stole på at noen kaller det direkte.

Dette er stort sett alt vi trenger å gjøre for å bringe Event Sourcing og CQRS sammen i vår enkle applikasjon.

7. Konklusjon

I denne veiledningen diskuterte vi det grunnleggende om designmønstre for hendelsessourcing og CQRS. Vi utviklet en enkel applikasjon og brukte disse mønstrene individuelt på den.

I prosessen forsto vi fordelene de gir og ulempene de gir. Til slutt forsto vi hvorfor og hvordan vi kan innlemme begge disse mønstrene sammen i applikasjonen vår.

Den enkle applikasjonen vi har diskutert i denne opplæringen, kommer ikke en gang i nærheten av å rettferdiggjøre behovet for CQRS og Event Sourcing. Vårt fokus var å forstå de grunnleggende begrepene, og eksemplet var trivielt. Men som nevnt tidligere, kan fordelen med disse mønstrene bare realiseres i applikasjoner som har en rimelig kompleks domenemodell.

Som vanlig kan kildekoden for denne artikkelen finnes på GitHub.


$config[zx-auto] not found$config[zx-overlay] not found