CQRS og hændelsessourcing i Java

1. Introduktion

I denne vejledning udforsker vi de grundlæggende begreber i Command Query Responsibility Segregation (CQRS) og Event Sourcing designmønstre.

Selvom de ofte citeres som komplementære mønstre, forsøger vi at forstå dem separat og endelig se, hvordan de supplerer hinanden. Der er flere værktøjer og rammer, såsom Axon, til at hjælpe med at vedtage disse mønstre, men vi opretter en simpel applikation i Java for at forstå det grundlæggende.

2. Grundlæggende begreber

Vi forstår først disse mønstre teoretisk, inden vi forsøger at implementere dem. Da de fremstår som individuelle mønstre ganske godt, forsøger vi at forstå uden at blande dem.

Bemærk, at disse mønstre ofte bruges sammen i en virksomhedsapplikation. I denne henseende drager de også fordel af adskillige andre enterprise arkitekturmønstre. Vi diskuterer nogle af dem, når vi går videre.

2.1. Begivenhedskilde

Arrangementssourcing giver os en ny måde at vedvare applikationstilstand på som en ordnet rækkefølge af begivenheder. Vi kan selektivt spørge om disse begivenheder og rekonstruere applikationens tilstand til enhver tid. For at få dette til at fungere er vi selvfølgelig nødt til at gentage enhver ændring i applikationens tilstand som begivenheder:

Disse begivenheder her er fakta, der er sket og ikke kan ændres - med andre ord, de skal være uforanderlige. Genskabelse af applikationstilstanden er bare et spørgsmål om at afspille alle begivenheder igen.

Bemærk, at dette også åbner muligheden for at afspille begivenheder selektivt, afspille nogle begivenheder i omvendt retning og meget mere. Som en konsekvens kan vi behandle selve ansøgningstilstanden som en sekundær borger med hændelsesloggen som vores primære kilde til sandhed.

2.2. CQRS

Enkelt sagt er CQRS det om at adskille kommando- og forespørgselssiden af ​​applikationsarkitekturen. CQRS er baseret på Command Query Separation (CQS) -princippet, som blev foreslået af Bertrand Meyer. CQS foreslår, at vi deler operationerne på domæneobjekter i to forskellige kategorier: Forespørgsler og kommandoer:

Forespørgsler returnerer et resultat og ændrer ikke den observerbare tilstand af et system. Kommandoer ændrer systemets tilstand, men returnerer ikke nødvendigvis en værdi.

Vi opnår dette ved rent at adskille kommando- og forespørgselssiden af ​​domænemodellen. Vi kan selvfølgelig tage et skridt videre ved også at opdele skrive- og læsesiden af ​​datalageret ved at indføre en mekanisme, der holder dem synkroniserede.

3. En enkel applikation

Vi begynder med at beskrive en simpel applikation i Java, der bygger en domænemodel.

Applikationen tilbyder CRUD-operationer på domænemodellen og vil også have en vedholdenhed for domæneobjekterne. CRUD står for Create, Read, Update og Delete, som er grundlæggende operationer, som vi kan udføre på et domæneobjekt.

Vi bruger den samme applikation til at introducere Event Sourcing og CQRS i senere sektioner.

I processen udnytter vi nogle af koncepterne fra Domain-Driven Design (DDD) i vores eksempel.

DDD adresserer analyse og design af software, der er afhængig af kompleks domænespecifik viden. Det bygger på ideen om, at softwaresystemer skal baseres på en veludviklet model af et domæne. DDD blev først ordineret af Eric Evans som et katalog over mønstre. Vi bruger nogle af disse mønstre til at bygge vores eksempel.

3.1. Applikationsoversigt

Oprettelse af en brugerprofil og administration af den er et typisk krav i mange applikationer. Vi definerer en simpel domænemodel, der fanger brugerprofilen sammen med en vedholdenhed:

Som vi kan se, er vores domænemodel normaliseret og udsætter flere CRUD-operationer. Disse operationer er kun til demonstration og kan være enkel eller kompleks afhængigt af kravene. Desuden kan persistensopbevaringsstedet her være i hukommelsen eller bruge en database i stedet.

3.2. Applikationsimplementering

Først bliver vi nødt til at oprette Java-klasser, der repræsenterer vores domænemodel. Dette er en ret simpel domænemodel og kræver muligvis ikke engang kompleksiteten i designmønstre ligesom Event Sourcing og CQRS. Vi holder dog dette simpelt for at fokusere på at forstå det grundlæggende:

offentlig klasse bruger {privat streng brugerid; privat streng fornavn; privat streng efternavn; private Sæt kontakter; private sæt 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 simpelt lager i hukommelsen til vedholdenhed i vores applikationstilstand. Selvfølgelig tilføjer dette ikke nogen værdi, men det er tilstrækkeligt til vores demonstration senere:

offentlig klasse UserRepository {privat kortbutik = ny HashMap (); }

Nu definerer vi en tjeneste, der eksponerer typiske CRUD-operationer på vores domænemodel:

offentlig klasse UserService {privat 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 kontakter, Set adresser) {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); Indstil kontakter = user.getContacts (); returnere contacts.stream () .filter (c -> c.getType (). er lig med (contactType)) .collect (Collectors.toSet ()); } public Set getAddressByRegion (String userId, String state) {User user = repository.getUser (userId); Indstil adresser = user.getAddresses (); returadresser.stream () .filter (a -> a.getState (). er lig med (tilstand)) .collect (Collectors.toSet ()); }}

Det er stort set hvad vi skal gøre for at opsætte vores enkle applikation. Dette er langt fra at være produktionsklar kode, men det afslører nogle af de vigtige punkter at vi vil drøfte senere i denne vejledning.

3.3. Problemer i denne applikation

Før vi fortsætter videre i vores diskussion med Event Sourcing og CQRS, er det umagen værd at diskutere problemerne med den nuværende løsning. Når alt kommer til alt vil vi løse de samme problemer ved at anvende disse mønstre!

Ud af mange problemer, som vi måske bemærker her, vil vi bare fokusere på to af dem:

  • Domæne Model: Læse- og skrivehandlingerne foregår over den samme domænemodel. Selvom dette ikke er et problem for en simpel domænemodel som denne, kan det forværres, da domænemodellen bliver kompleks. Vi skal muligvis optimere vores domænemodel og den underliggende lagring for dem, så de passer til de individuelle behov i læse- og skriveoperationerne.
  • Udholdenhed: Den vedholdenhed, vi har for vores domæneobjekter, gemmer kun den seneste status for domænemodellen. Selvom dette er tilstrækkeligt i de fleste situationer, gør det nogle opgaver udfordrende. For eksempel, hvis vi skal udføre en historisk revision af, hvordan domæneobjektet har ændret tilstand, er det ikke muligt her. Vi er nødt til at supplere vores løsning med nogle revisionslogfiler for at opnå dette.

4. Introduktion til CQRS

Vi begynder at løse det første problem, vi diskuterede i sidste afsnit ved at introducere CQRS-mønsteret i vores ansøgning. Som en del af dette vi adskiller domænemodellen og dens vedholdenhed til at håndtere skrive- og læseoperationer. Lad os se, hvordan CQRS-mønsteret omstrukturerer vores applikation:

Diagrammet her forklarer, hvordan vi planlægger at adskille vores applikationsarkitektur for at skrive og læse sider. Vi har imidlertid introduceret en hel del nye komponenter her, som vi skal forstå bedre. Bemærk, at disse ikke er strengt relateret til CQRS, men CQRS har stor fordel af dem:

  • Aggregat / Aggregator:

Samlet er et mønster beskrevet i Domain-Driven Design (DDD), der logisk grupperer forskellige enheder ved at binde enheder til en samlet rod. Det samlede mønster giver transaktionel konsistens mellem enhederne.

CQRS drager naturligvis fordel af det samlede mønster, der grupperer skrivedomænemodellen og giver transaktionsgarantier. Aggregater har normalt en cachelagret tilstand for bedre ydeevne, men kan fungere perfekt uden den.

  • Projektion / projektor:

Projektion er et andet vigtigt mønster, som i høj grad gavner CQRS. Fremskrivning betyder i det væsentlige at repræsentere domæneobjekter i forskellige former og strukturer.

Disse fremskrivninger af originaldata er skrivebeskyttet og yderst optimeret til at give en forbedret læseoplevelse. Vi beslutter måske igen at cache fremskrivninger for bedre ydeevne, men det er ikke en nødvendighed.

4.1. Implementering af skrivesiden af ​​applikationen

Lad os først implementere skrivesiden af ​​applikationen.

Vi begynder med at definere de krævede kommandoer. EN kommando er en hensigt om at mutere domænemodellens tilstand. Om det lykkes eller ej, afhænger af de forretningsregler, vi konfigurerer.

Lad os se vores kommandoer:

offentlig klasse CreateUserCommand {private String userId; privat streng fornavn; privat streng efternavn; } Offentlig klasse UpdateUserCommand {private String userId; private sæt adresser; private Sæt kontakter; }

Dette er ret enkle klasser, der indeholder de data, vi har til hensigt at mutere.

Dernæst definerer vi et aggregat, der er ansvarligt for at tage kommandoer og håndtere dem. Aggregater kan acceptere eller afvise en kommando:

offentlig klasse UserAggregate {private UserWriteRepository writeRepository; offentlig UserAggregate (UserWriteRepository repository) {this.writeRepository = repository; } offentlig bruger handleCreateUserCommand (CreateUserCommand kommando) {Bruger bruger = ny bruger (command.getUserId (), command.getFirstName (), command.getLastName ()); writeRepository.addUser (user.getUserid (), bruger); tilbagevendende bruger } offentlig bruger handleUpdateUserCommand (kommando UpdateUserCommand) {Bruger bruger = writeRepository.getUser (command.getUserId ()); user.setAddresses (command.getAddresses ()); user.setContacts (command.getContacts ()); writeRepository.addUser (user.getUserid (), bruger); tilbagevendende bruger }}

Aggregatet bruger et arkiv til at hente den aktuelle tilstand og vedligeholde ændringer i det. Desuden kan det gemme den aktuelle tilstand lokalt for at undgå returflyvning til et lager, mens hver kommando behandles.

Endelig har vi brug for et lager for at indeholde tilstanden for domænemodellen. Dette vil typisk være en database eller anden holdbar butik, men her erstatter vi dem simpelthen med en datastruktur i hukommelsen:

offentlig klasse UserWriteRepository {privat kortbutik = ny HashMap (); // accessors og mutatorer}

Dette afslutter skrivesiden af ​​vores ansøgning.

4.2. Implementering af den læste side af applikationen

Lad os skifte til den læste side af applikationen nu. Vi begynder med at definere den læste side af domænemodellen:

offentlig klasse UserAddress {privat kort addressByRegion = ny HashMap (); } offentlig klasse UserContact {privat kort contactByType = ny HashMap (); }

Hvis vi husker vores læste operationer, er det ikke svært at se, at disse klasser kortlægger perfekt til at håndtere dem. Det er skønheden ved at oprette en domænemodel centreret omkring forespørgsler, vi har.

Dernæst definerer vi læselageret. Igen bruger vi bare en datastruktur i hukommelsen, selvom dette vil være en mere holdbar datalager i ægte applikationer:

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

Nu definerer vi de krævede forespørgsler, vi skal støtte. En forespørgsel er en hensigt om at få data - det kan ikke nødvendigvis resultere i data.

Lad os se vores forespørgsler:

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

Igen er dette enkle Java-klasser, der indeholder dataene til at definere en forespørgsel.

Hvad vi har brug for nu er en projektion, der kan håndtere disse forespørgsler:

offentlig klasse UserProjection {private UserReadRepository readRepository; offentlig UserProjection (UserReadRepository readRepository) {this.readRepository = readRepository; } offentlig sæthåndtag (ContactByTypeQuery-forespørgsel) {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 ()); }}

Projektionen her bruger det læse arkiv, vi definerede tidligere til at adressere de forespørgsler, vi har. Dette afslutter stort set også den læste side af vores ansøgning.

4.3. Synkronisering af læse- og skrivedata

Et stykke af dette puslespil er stadig uløst: der er intet at synkronisere vores skrive- og læseopbevaringssteder.

Det er her, vi har brug for noget kendt som en projektor. EN projektor har logikken til at projicere skrivedomænemodellen i den læste domænemodel.

Der er meget mere sofistikerede måder at håndtere dette på, men vi holder det relativt simpelt:

offentlig klasse UserProjector {UserReadRepository readRepository = nyt UserReadRepository (); offentlig UserProjector (UserReadRepository readRepository) {this.readRepository = readRepository; } offentligt ugyldigt projekt (brugerbruger) {UserContact userContact = Valgfrit.ofNullable (readRepository.getUserContact (user.getUserid ())). ellerElse (ny UserContact ()); Kort 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 ())). EllerElse (ny UserAddress ()); Kort addressByRegion = ny HashMap (); for (Adresse-adresse: user.getAddresses ()) {Indstil adresser = Valgfri.ofNullable (addressByRegion.get (address.getState ())) .ellerElse (ny HashSet ()); adresser. tilføj (adresse); addressByRegion.put (address.getState (), adresser); } userAddress.setAddressByRegion (addressByRegion); readRepository.addUserAddress (user.getUserid (), userAddress); }}

Dette er snarere en meget rå måde at gøre dette på, men giver os tilstrækkelig indsigt i, hvad der er behov for for at CQRS skal fungere. Desuden er det ikke nødvendigt at have læse- og skriveopbevaringsstederne i forskellige fysiske butikker. Et distribueret system har sin egen andel af problemer!

Bemærk, at det er ikke praktisk at projicere den aktuelle tilstand for skrivedomænet i forskellige læste domænemodeller. Det eksempel, vi har taget her, er ret simpelt, derfor ser vi ikke problemet.

Efterhånden som skrive- og læsemodellerne bliver mere komplekse, bliver det stadig sværere at projicere. Vi kan tackle dette gennem begivenhedsbaseret projektion i stedet for statsbaseret projektion med Sourcing af begivenheder. Vi får se, hvordan du opnår dette senere i vejledningen.

4.4. Fordele og ulemper ved CQRS

Vi diskuterede CQRS-mønsteret og lærte at introducere det i en typisk applikation. Vi har kategorisk forsøgt at løse problemet relateret til domænemodelens stivhed i håndtering af både læsning og skrivning.

Lad os nu diskutere nogle af de andre fordele, som CQRS bringer til en applikationsarkitektur:

  • CQRS giver os en bekvem måde at vælge separate domænemodeller på passende til skrive- og læseoperationer vi behøver ikke oprette en kompleks domænemodel, der understøtter begge dele
  • Det hjælper os med at vælg arkiver, der er individuelt egnede til håndtering af kompleksiteten i læse- og skriveoperationerne, som f.eks. høj kapacitet til skrivning og lav latens til læsning
  • Det naturligt supplerer begivenhedsbaserede programmeringsmodeller i en distribueret arkitektur ved at give en adskillelse af bekymringer såvel som enklere domænemodeller

Dette kommer dog ikke gratis. Som det fremgår af dette enkle eksempel tilføjer CQRS arkitekturen betydelig kompleksitet. Det er muligvis ikke passende eller værd at have smerter i mange scenarier:

  • Kun en kompleks domænemodel kan være til gavn fra den ekstra kompleksitet af dette mønster; en simpel domænemodel kan styres uden alt dette
  • Naturligt fører til kodekopiering til en vis grad, hvilket er et acceptabelt onde sammenlignet med den gevinst, det fører os til; dog anbefales individuel vurdering
  • Separate opbevaringssteder føre til problemer med konsistens, og det er svært at altid skrive og læse arkiver i perfekt synkronisering altid; vi er ofte nødt til at nøjes med en eventuel konsistens

5. Introduktion til begivenhedssourcing

Dernæst behandler vi det andet problem, vi diskuterede i vores enkle applikation. Hvis vi husker, var det relateret til vores persistensopbevaringssted.

Vi introducerer Event Sourcing for at løse dette problem. Begivenhedssourcing dramatisk ændrer den måde, vi tænker på applikationsstatuslageret.

Lad os se, hvordan det ændrer vores lager:

Her har vi struktureret vores lager for at gemme en ordnet liste over domænehændelser. Enhver ændring af domæneobjektet betragtes som en begivenhed. Hvor grov eller finkornet en begivenhed skal være, er et spørgsmål om domæne design. De vigtige ting at overveje her er det begivenheder har en tidsmæssig orden og er uforanderlige.

5.1. Implementering af begivenheder og eventbutik

De grundlæggende objekter i begivenhedsdrevne applikationer er begivenheder, og hændelsessourcing er ikke anderledes. Som vi har set tidligere, begivenheder repræsenterer en bestemt ændring i domænemodellens tilstand på et bestemt tidspunkt. Så vi begynder med at definere basishændelsen til vores enkle anvendelse:

offentlig abstrakt klasse Begivenhed {offentlig endelig UUID id = UUID.randomUUID (); offentlig endelig Dato oprettet = ny dato (); }

Dette sikrer bare, at hver begivenhed, vi genererer i vores applikation, får en unik identifikation og tidspunktet for oprettelsen. Disse er nødvendige for at behandle dem yderligere.

Selvfølgelig kan der være flere andre attributter, der kan interessere os, som en attribut til at fastslå oprindelsen til en begivenhed.

Lad os derefter oprette nogle domænespecifikke begivenheder, der arver fra denne basishændelse:

offentlig klasse UserCreatedEvent udvider begivenhed {privat streng brugerId; privat streng fornavn; privat streng efternavn; } offentlig klasse UserContactAddedEvent udvider begivenhed {private String contactType; privat streng kontaktdetaljer; } offentlig klasse UserContactRemovedEvent udvider begivenhed {private String contactType; privat streng kontaktdetaljer; } offentlig klasse UserAddressAddedEvent udvider begivenhed {privat strengby; privat strengstat; private String postCode; } Public class UserAddressRemovedEvent udvider begivenhed {privat strengby; privat strengstat; private String postCode; }

Dette er enkle POJO'er i Java, der indeholder detaljerne om domænehændelsen. Den vigtige ting at bemærke her er dog begivenhedernes granularitet.

Vi kunne have oprettet en enkelt begivenhed til brugeropdateringer, men i stedet besluttede vi at oprette separate begivenheder til tilføjelse og fjernelse af adresse og kontakt. Valget kortlægges til, hvad der gør det mere effektivt at arbejde med domænemodellen.

Nu har vi naturligvis brug for et lager for at afholde vores domænehændelser:

offentlig klasse EventStore {privat kort butik = ny HashMap (); }

Dette er en simpel datastruktur i hukommelsen, der holder vores domænehændelser. I virkeligheden, der er flere løsninger specielt oprettet til at håndtere begivenhedsdata som Apache Druid. Der er mange distribuerede datalagre til generelle formål, der er i stand til at håndtere hændelsessourcing, herunder Kafka og Cassandra.

5.2. Generering og forbrug af begivenheder

Så nu vil vores service, der håndterede alle CRUD-operationer, ændre sig. Nu, i stedet for at opdatere en bevægelig domænetilstand, tilføjer den domænehændelser. Det bruger også de samme domænehændelser til at svare på forespørgsler.

Lad os se, hvordan vi kan opnå dette:

offentlig klasse UserService {privat EventStore-lager; offentlig 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, Set contacts, Set adresses) {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 (). indeholder (c)) .forEach (c -> repository.addEvent (userId, new UserContactAddedEvent (c.getType (), c.getDetail ()) )); user.getAddresses (). stream () .filter (a ->! adresser. indeholder (a)) .forEach (a -> repository.addEvent (userId, new UserAddressRemovedEvent (a.getCity (), a.getState (), a.getPostcode ()))); adresser.stream () .filter (a ->! user.getAddresses (). indeholder (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 lig med (contactType)) .collect (Collectors.toSet ()); } public Set getAddressByRegion (String userId, String state) kaster Undtagelse {User user = UserUtility.recreateUserState (repository, userId); returner user.getAddresses (). stream () .filter (a -> a.getState (). er lig med (tilstand)) .collect (Collectors.toSet ()); }}

Bemærk, at vi genererer flere begivenheder som en del af håndteringen af ​​opdateringsbrugerfunktionen her. Det er også interessant at bemærke, hvordan vi er generere den aktuelle tilstand af domænemodellen ved at afspille alle domænehændelser, der hidtil er genereret.

I en rigtig applikation er dette selvfølgelig ikke en gennemførlig strategi, og vi bliver nødt til at opretholde en lokal cache for at undgå at generere staten hver gang. Der er andre strategier som snapshots og roll-up i event repository, der kan fremskynde processen.

Dette afslutter vores indsats for at indføre sourcing af begivenheder i vores enkle applikation.

5.3. Fordele og ulemper ved sourcing af begivenheder

Nu har vi med succes vedtaget en alternativ måde at gemme domæneobjekter på ved hjælp af hændelsessourcing. Begivenhedssourcing er et stærkt mønster og bringer en masse fordele til en applikationsarkitektur, hvis det anvendes korrekt:

  • Gør skrive operationer meget hurtigere da der ikke kræves læsning, opdatering og skrivning; skriv er blot at føje en begivenhed til en log
  • Fjerner den objektrelationelle impedans og dermed behovet for komplekse kortlægningsværktøjer; selvfølgelig er vi stadig nødt til at genskabe objekterne tilbage
  • Sker med give en revisionslog som et biprodukt, som er helt pålidelig; vi kan fejle nøjagtigt, hvordan en domænemodels tilstand har ændret sig
  • Det gør det muligt at støtte timelige forespørgsler og opnå tidsrejser (domænetilstanden på et tidspunkt i fortiden)!
  • Det er naturligt egnet til design af løst koblede komponenter i en mikroservicearkitektur, der kommunikerer asynkront ved at udveksle meddelelser

Men som altid er selv indkøb af begivenheder ikke en sølvkugle. Det tvinger os til at vedtage en dramatisk anden måde at lagre data på. Dette viser sig muligvis ikke at være nyttigt i flere tilfælde:

  • Der er en tilknyttet læringskurve og et tankeskift krævet at vedtage sourcing af begivenheder til at begynde med er det ikke intuitivt
  • Det gør det ret vanskeligt at håndtere typiske forespørgsler da vi har brug for at genskabe staten, medmindre vi holder staten i den lokale cache
  • Selvom det kan anvendes på enhver domænemodel, er det mere passende til den hændelsesbaserede model i en hændelsesdrevet arkitektur

6. CQRS med hændelsessourcing

Nu hvor vi har set, hvordan vi individuelt introducerer Event Sourcing og CQRS til vores enkle applikation, er det tid til at bringe dem sammen. Det bør være ret intuitivt nu hvor disse mønstre kan have stor gavn af hinanden. Dog gør vi det mere eksplicit i dette afsnit.

Lad os først se, hvordan applikationsarkitekturen bringer dem sammen:

Dette burde ikke være nogen overraskelse nu. Vi har udskiftet skrive-siden af ​​arkivet til at være en begivenhedsbutik, mens den læste side af arkivet fortsat er den samme.

Bemærk, at dette ikke er den eneste måde at bruge Event Sourcing og CQRS på i applikationsarkitekturen. Vi kan være ret innovative og bruge disse mønstre sammen med andre mønstre og komme med flere arkitekturmuligheder.

Hvad der er vigtigt her er at sikre, at vi bruger dem til at styre kompleksiteten, ikke blot for at øge kompleksiteten yderligere!

6.1. At bringe CQRS og hændelsessourcing sammen

Efter at have implementeret Event Sourcing og CQRS individuelt, bør det ikke være så svært at forstå, hvordan vi kan bringe dem sammen.

Godt begynde med applikationen, hvor vi introducerede CQRS og bare foretage relevante ændringer for at bringe sourcing af begivenheder i folden. Vi vil også udnytte de samme begivenheder og begivenhedsbutik, som vi definerede i vores applikation, hvor vi introducerede begivenhedssourcing.

Der er kun et par ændringer. Vi begynder med at ændre aggregatet til generere begivenheder i stedet for opdateringstilstand:

offentlig klasse UserAggregate {private EventStore writeRepository; offentlig UserAggregate (EventStore repository) {this.writeRepository = repository; } offentlig liste handleCreateUserCommand (CreateUserCommand command) {UserCreatedEvent event = new UserCreatedEvent (command.getUserId (), command.getFirstName (), command.getLastName ()); writeRepository.addEvent (command.getUserId (), begivenhed); returnere Arrays.asList (begivenhed); } offentlig liste handleUpdateUserCommand (UpdateUserCommand-kommando) {User user = UserUtility.recreateUserState (writeRepository, command.getUserId ()); Listehændelser = ny ArrayList (); Liste kontakterTil fjernelse = user.getContacts (). Stream () .filter (c ->! Command.getContacts (). Indeholder (c)) .collect (Collectors.toList ()); for (Kontaktkontakt: contactsToRemove) {UserContactRemovedEvent contactRemovedEvent = ny UserContactRemovedEvent (contact.getType (), contact.getDetail ()); events.add (contactRemovedEvent); writeRepository.addEvent (command.getUserId (), contactRemovedEvent); } Liste kontakterTilføj = command.getContacts (). Stream () .filter (c ->! User.getContacts (). Indeholder (c)) .collect (Collectors.toList ()); for (Kontaktkontakt: contactsToAdd) {UserContactAddedEvent contactAddedEvent = ny UserContactAddedEvent (contact.getType (), contact.getDetail ()); events.add (contactAddedEvent); writeRepository.addEvent (command.getUserId (), contactAddedEvent); } // behandler adresser på samme mådeToRemove // ​​behandler adresser på samme mådeTilføj returhændelser; }}

Den eneste anden nødvendige ændring er i projektoren, som nu skal behandle begivenheder i stedet for domæneobjekttilstande:

offentlig klasse UserProjector {UserReadRepository readRepository = nyt UserReadRepository (); offentlig UserProjector (UserReadRepository readRepository) {this.readRepository = readRepository; } offentligt ugyldigt projekt (String userId, List events) {for (Event event: events) {if (event instanceof UserAddressAddedEvent) apply (userId, (UserAddressAddedEvent) event); hvis (begivenhedsinstans af UserAddressRemovedEvent) gælder (userId, (UserAddressRemovedEvent) begivenhed); hvis (begivenhedsinstans af UserContactAddedEvent) gælder (userId, (UserContactAddedEvent) begivenhed); hvis (begivenhedsinstans af UserContactRemovedEvent) gælder (userId, (UserContactRemovedEvent) begivenhed); }} public void apply (String userId, UserAddressAddedEvent event) {Address address = new Address (event.getCity (), event.getState (), event.getPostCode ()); UserAddress userAddress = Optional.ofNullable (readRepository.getUserAddress (userId)). EllerElse (ny UserAddress ()); Indstil adresser = Valgfri.ofNullable (userAddress.getAddressByRegion () .get (address.getState ())). Eller Else (ny HashSet ()); adresser. tilføj (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) {Indstil adresser = userAddress.getAddressByRegion () .get (address.getState ()); hvis (adresser! = null) adresser. fjern (adresse); readRepository.addUserAddress (userId, userAddress); }} offentlig ugyldighed gælder (String userId, UserContactAddedEvent event) {// Håndter på samme måde UserContactAddedEvent event} public void apply (String userId, UserContactRemovedEvent event) {// Tilsvarende håndterer UserContactRemovedEvent begivenhed}}

Hvis vi husker de problemer, vi diskuterede under håndtering af statsbaseret projektion, er dette en mulig løsning på det.

Det begivenhedsbaseret projektion er ret praktisk og lettere at implementere. Alt, hvad vi skal gøre, er at behandle alle forekommende domænehændelser og anvende dem på alle læste domænemodeller. I en begivenhedsbaseret applikation ville projektoren typisk lytte til domænehændelser, den er interesseret i, og ville ikke stole på, at nogen kalder det direkte.

Dette er stort set alt, hvad vi skal gøre for at bringe Event Sourcing og CQRS sammen i vores enkle applikation.

7. Konklusion

I denne vejledning diskuterede vi det grundlæggende i Design Sourcing og CQRS designmønstre. Vi udviklede en enkel applikation og anvendte disse mønstre individuelt på den.

I processen forstod vi fordelene, de bringer, og de ulemper, de har. Endelig forstod vi, hvorfor og hvordan man indarbejder begge disse mønstre sammen i vores applikation.

Den enkle applikation, vi har diskuteret i denne vejledning, kommer ikke engang tæt på at retfærdiggøre behovet for CQRS og Event Sourcing. Vores fokus var at forstå de grundlæggende begreber, hvorfor eksemplet var trivielt. Men som nævnt før kan fordelen ved disse mønstre kun realiseres i applikationer, der har en forholdsvis kompleks domænemodel.

Som normalt kan kildekoden til denne artikel findes på GitHub.


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