Vedvarende DDD-aggregater

1. Oversigt

I denne vejledning undersøger vi mulighederne for vedvarende DDD-aggregater ved hjælp af forskellige teknologier.

2. Introduktion til aggregater

Et samlet er en gruppe af forretningsobjekter, som altid skal være ensartede. Derfor gemmer og opdaterer vi aggregater som helhed i en transaktion.

Aggregat er et vigtigt taktisk mønster i DDD, som hjælper med at opretholde konsistensen af ​​vores forretningsobjekter. Imidlertid er ideen om aggregat også nyttig uden for DDD-sammenhængen.

Der er mange forretningssager, hvor dette mønster kan være nyttigt. Som en tommelfingerregel skal vi overveje at bruge aggregater, når der er flere objekter ændret som en del af den samme transaktion.

Lad os se på, hvordan vi kan anvende dette, når vi modellerer et ordrekøb.

2.1. Eksempel på indkøbsordre

Så lad os antage, at vi vil modellere en indkøbsordre:

class Order {private Collection orderLines; private penge totalCost; // ...}
klasse OrderLine {privat produktprodukt; privat int mængde; // ...}
klasse Produkt {privat pengepris; // ...}

Disse klasser udgør et simpelt aggregat. Begge ordrelinjer og Udgifter i alt felter i Bestille skal altid være konsistent, altså Udgifter i alt skal altid have værdien lig med summen af ​​alle ordrelinjer.

Nu kan vi alle blive fristet til at gøre alle disse til fuldt udbyggede Java Beans. Men bemærk at introducere enkle getters og settere i Bestille let kunne bryde indkapslingen af ​​vores model og overtræde forretningsbegrænsninger.

Lad os se, hvad der kunne gå galt.

2.2. Naivt samlet design

Lad os forestille os, hvad der kunne ske, hvis vi besluttede at naivt tilføje getters og settere til alle ejendomme på Bestille klasse, inklusive setOrderTotal.

Der er intet, der forbyder os at udføre følgende kode:

Ordreordre = ny ordre (); order.setOrderLines (Arrays.asList (orderLine0, orderLine1)); order.setTotalCost (Money.zero (CurrencyUnit.USD)); // dette ser ikke godt ud ...

I denne kode indstiller vi manuelt Udgifter i alt ejendom til nul, hvilket overtræder en vigtig forretningsregel. Absolut bør de samlede omkostninger ikke være nul dollars!

Vi har brug for en måde at beskytte vores forretningsregler på. Lad os se på, hvordan samlede rødder kan hjælpe.

2.3. Samlet rod

En samlet rod er en klasse, der fungerer som et indgangspunkt til vores aggregat. Al forretningsdrift skal gå gennem roden. På denne måde kan den samlede rod tage sig af at holde aggregatet i en konsistent tilstand.

Roden er, hvad der tager sig af alle vores forretningsinvariere.

Og i vores eksempel, den Bestille klasse er den rigtige kandidat til den samlede rod. Vi skal bare foretage nogle ændringer for at sikre, at aggregatet altid er konsistent:

klasse Bestil {privat endelig Liste ordrelinjer; private penge totalCost; Order (List orderLines) {checkNotNull (orderLines); hvis (orderLines.isEmpty ()) {kast nyt IllegalArgumentException ("Ordren skal have mindst en ordrelinjepost"); } this.orderLines = ny ArrayList (orderLines); totalCost = calcTotalCost (); } ugyldigt addLineItem (OrderLine orderLine) {checkNotNull (orderLine); orderLines.add (orderLine); totalCost = totalCost.plus (orderLine.cost ()); } ugyldig removeLineItem (int line) {OrderLine removedLine = orderLines.remove (line); totalCost = totalCost.minus (fjernetLine.cost ()); } Penge totalCost () {return totalCost; } // ...}

Brug af en samlet rod giver os nu lettere at vende Produkt og OrderLine i uforanderlige objekter, hvor alle egenskaberne er endelige.

Som vi kan se, er dette et ret simpelt aggregat.

Og vi kunne simpelthen have beregnet de samlede omkostninger hver gang uden at bruge et felt.

Men lige nu taler vi bare om samlet vedholdenhed, ikke samlet design. Hold dig opdateret, da dette specifikke domæne kommer til nytte om et øjeblik.

Hvor godt spiller dette med persistensteknologier? Lad os se. I sidste ende vil dette hjælpe os med at vælge det rigtige vedholdenhedsværktøj til vores næste projekt.

3. JPA og dvale

Lad os i dette afsnit forsøge at fortsætte vores Bestille samlet ved hjælp af JPA og Dvaletilstand. Vi bruger Spring Boot og JPA starter:

 org.springframework.boot spring-boot-starter-data-jpa 

For de fleste af os synes dette at være det mest naturlige valg. Når alt kommer til alt har vi brugt år på at arbejde med relationelle systemer, og vi kender alle populære ORM-rammer.

Sandsynligvis det største problem, når vi arbejder med ORM-rammer, er forenklingen af ​​vores model design. Det kaldes også undertiden som objektrelationel impedans mismatch. Lad os tænke over, hvad der ville ske, hvis vi ville fortsætte vores Bestille samlet:

@DisplayName ("givet ordre med to linjeposter, når vedvarende, så ordre gemmes") @Test offentlig ugyldig test () kaster undtagelse {// givet JpaOrder-rækkefølge = preparer TestOrderWithTwoLineItems () // når JpaOrder gemteOrder = repository.save (rækkefølge); // derefter JpaOrder fundetOrder = repository.findById (gemteOrder.getId ()) .get (); assertThat (foundOrder.getOrderLines ()). har størrelse (2); }

På dette tidspunkt kaster denne test en undtagelse: java.lang.IllegalArgumentException: Ukendt enhed: com.baeldung.ddd.order.Order. Vi mangler åbenbart nogle af JPA-kravene:

  1. Tilføj kortkommentarer
  2. OrderLine og Produkt klasser skal være enheder eller @Embeddable klasser, ikke enkle værdiobjekter
  3. Tilføj en tom konstruktør for hver enhed eller @Embeddable klasse
  4. Erstatte Penge ejendomme med enkle typer

Hmm, vi er nødt til at ændre designet af Bestille samlet for at kunne bruge JPA. Selvom tilføjelse af annoteringer ikke er en big deal, kan de andre krav medføre mange problemer.

3.1. Ændringer i værdiobjekterne

Det første spørgsmål ved at forsøge at tilpasse et aggregat til JPA er, at vi har brug for at bryde designet af vores værdiobjekter: Deres egenskaber kan ikke længere være endelige, og vi skal bryde indkapslingen.

Vi er nødt til at tilføje kunstige id'er til OrderLine og Produkt, selvom disse klasser aldrig var designet til at have identifikatorer. Vi ønskede, at de skulle være enkle værdiobjekter.

Det er muligt at bruge @Embedded og @ElementCollection annoteringer i stedet, men denne tilgang kan komplicere ting meget, når du bruger en kompleks objektgraf (for eksempel @Embeddable objekt, der har en anden @Embedded ejendom osv.).

Ved brug af @Embedded annotation tilføjer simpelthen flade egenskaber til overordnet tabel. Bortset fra det, grundlæggende egenskaber (f.eks. Af Snor type) kræver stadig en settermetode, der overtræder den ønskede værdi af objektdesignet.

Tom konstruktorkrav tvinger værdien af ​​objektegenskaber til ikke længere at være endelige, hvilket bryder et vigtigt aspekt af vores originale design. Sandheden bliver fortalt, at dvale kan bruge den private no-args-konstruktør, hvilket mildner problemet lidt, men det er stadig langt fra at være perfekt.

Selv når vi bruger en privat standardkonstruktør, kan vi enten ikke markere vores egenskaber som endelige, eller vi skal initialisere dem med standardværdier (ofte null) inde i standardkonstruktøren.

Men hvis vi ønsker at være fuldt JPA-kompatible, skal vi i det mindste bruge beskyttet synlighed til standardkonstruktøren, hvilket betyder, at andre klasser i den samme pakke kan oprette værdiobjekter uden at angive værdier for deres egenskaber.

3.2. Komplekse typer

Desværre kan vi ikke forvente, at JPA automatisk kortlægger tredjeparts komplekse typer i tabeller. Se bare, hvor mange ændringer vi skulle introducere i det foregående afsnit!

For eksempel når du arbejder med vores Bestille samlet vil vi støde på vedvarende vanskeligheder Joda Money felter.

I et sådant tilfælde ender vi muligvis med at skrive tilpasset type @Konverter tilgængelig fra JPA 2.1. Det kan dog kræve noget ekstra arbejde.

Alternativt kan vi også opdele Penge ejendom i to grundlæggende egenskaber. For eksempel Snor for valutaenhed og BigDecimal for den faktiske værdi.

Mens vi kan skjule implementeringsoplysningerne og stadig bruge Penge klasse gennem API'en for offentlige metoder, viser praksis, at de fleste udviklere ikke kan retfærdiggøre det ekstra arbejde, og degenererede simpelthen modellen til at være i overensstemmelse med JPA-specifikationen i stedet.

3.3. Konklusion

Mens JPA er en af ​​de mest vedtagne specifikationer i verden, er det måske ikke den bedste mulighed for at fortsætte vores Bestille samlet.

Hvis vi ønsker, at vores model skal afspejle de sande forretningsregler, skal vi designe den til ikke at være en simpel 1: 1-gengivelse af de underliggende tabeller.

Grundlæggende har vi tre muligheder her:

  1. Opret et sæt enkle dataklasser og brug dem til at fortsætte og genskabe den rige forretningsmodel. Desværre kan dette kræve en masse ekstra arbejde.
  2. Accepter begrænsningerne ved JPA, og vælg det rigtige kompromis.
  3. Overvej en anden teknologi.

Den første mulighed har det største potentiale. I praksis udvikles de fleste projekter ved hjælp af den anden mulighed.

Lad os nu overveje en anden teknologi til at vedblive aggregater.

4. Dokumentforretning

Et dokumentlager er en alternativ måde at lagre data på. I stedet for at bruge relationer og tabeller gemmer vi hele objekter. Dette gør et dokumentlager til en potentielt perfekt kandidat til vedvarende aggregater.

Til behovene i denne vejledning fokuserer vi på JSON-lignende dokumenter.

Lad os se nærmere på, hvordan vores ordreudholdelsesproblem ser ud i en dokumentbutik som MongoDB.

4.1. Vedvarende samlet ved hjælp af MongoDB

Nu er der en hel del databaser, der kan gemme JSON-data, en af ​​de mest populære er MongoDB. MongoDB gemmer faktisk BSON eller JSON i binær form.

Takket være MongoDB kan vi gemme Bestille eksempel samlet som det er.

Før vi går videre, lad os tilføje Spring Boot MongoDB starter:

 org.springframework.boot spring-boot-starter-data-mongodb 

Nu kan vi køre en lignende testsag som i JPA-eksemplet, men denne gang ved hjælp af MongoDB:

@DisplayName ("givet ordre med to linjeposter, når vedvarende bruger mongo repository, så gemmes ordre") @Test ugyldig test () kaster undtagelse {// givet ordre rækkefølge = preparTestOrderWithTwoLineItems (); // når repo.save (ordre); // derefter Liste fundet Bestillinger = repo.findAll (); hævder, at (fundet bestillinger) .har størrelse (1); Liste fundetOrderLines = fundetOrders.iterator () .næste () .getOrderLines (); assertThat (foundOrderLines) .hasSize (2); assertThat (foundOrderLines) .containsOnlyElementsOf (order.getOrderLines ()); }

Hvad der er vigtigt - vi ændrede ikke originalen Bestille samlede klasser overhovedet ikke nødvendigt at oprette standardkonstruktører, settere eller brugerdefineret konverter til Penge klasse.

Og her er hvad vores Bestille aggregat vises i butikken:

{"_id": ObjectId ("5bd8535c81c04529f54acd14"), "orderLines": [{"product": {"price": {"money": {"currency": {"code": "USD", "numericCode": 840, "decimalPlaces": 2}, "beløb": "10,00"}}}, "antal": 2}, {"produkt": {"pris": {"penge": {"valuta": {"kode ":" USD "," numericCode ": 840," decimalPlaces ": 2}," beløb ":" 5,00 "}}}," antal ": 10}]," totalCost ": {" penge ": {" valuta ": {" code ":" USD "," numericCode ": 840," decimalPlaces ": 2}," amount ":" 70.00 "}}," _class ":" com.baeldung.ddd.order.mongo.Order "}

Dette enkle BSON-dokument indeholder helheden Bestille samlet i et stykke, der matcher pænt med vores oprindelige forestilling om, at alt dette skal være i overensstemmelse med hinanden.

Bemærk, at komplekse objekter i BSON-dokumentet simpelthen serieliseres som et sæt regelmæssige JSON-egenskaber. Takket være dette, selv tredjepartsklasser (som f.eks Joda Money) kan let serieliseres uden behov for at forenkle modellen.

4.2. Konklusion

Vedvarende aggregater ved hjælp af MongoDB er enklere end at bruge JPA.

Dette betyder absolut ikke, at MongoDB er bedre end traditionelle databaser. Der er masser af legitime tilfælde, hvor vi ikke engang skal prøve at modellere vores klasser som aggregater og i stedet bruge en SQL-database.

Når vi stadig har identificeret en gruppe objekter, der altid skal være konsistente i henhold til de komplekse krav, kan brug af et dokumentlager være en meget tiltalende mulighed.

5. Konklusion

I DDD indeholder aggregater normalt de mest komplekse objekter i systemet. At arbejde med dem har brug for en meget anden tilgang end i de fleste CRUD-applikationer.

Brug af populære ORM-løsninger kan føre til en forenklet eller overeksponeret domænemodel, som ofte ikke er i stand til at udtrykke eller håndhæve indviklede forretningsregler.

Dokumentforretninger kan gøre det lettere at opretholde aggregater uden at ofre modelens kompleksitet.

Den fulde kildekode for alle eksemplerne er tilgængelig på GitHub.


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