Aanhoudende DDD-aggregaten

1. Overzicht

In deze tutorial onderzoeken we de mogelijkheden van persisterende DDD-aggregaten met behulp van verschillende technologieën.

2. Inleiding tot aggregaten

Een aggregaat is een groep bedrijfsobjecten die altijd consistent moeten zijn. Daarom bewaren en actualiseren we aggregaten als geheel binnen een transactie.

Aggregate is een belangrijk tactisch patroon in DDD, dat helpt om de consistentie van onze bedrijfsobjecten te behouden. Het idee van aggregaat is echter ook nuttig buiten de DDD-context.

Er zijn tal van businesscases waarin dit patroon van pas kan komen. Als vuistregel moeten we overwegen om aggregaten te gebruiken als er meerdere objecten zijn gewijzigd als onderdeel van dezelfde transactie.

Laten we eens kijken hoe we dit kunnen toepassen bij het modelleren van een orderaankoop.

2.1. Voorbeeld van een inkooporder

Laten we dus aannemen dat we een inkooporder willen modelleren:

klasse Order {privé Collectie orderLines; privégeld totaalkosten; // ...}
klasse OrderLine {privéproductproduct; private int hoeveelheid; // ...}
klasse Product {privé Geldprijs; // ...}

Deze klassen vormen een eenvoudig geheel. Beide orderLines en totale prijs velden van de Bestellen moet altijd consistent zijn, dat wil zeggen totale prijs moet altijd de waarde hebben die gelijk is aan de som van alle orderLines.

Nu kunnen we allemaal in de verleiding komen om al deze in volwaardige Javabonen te veranderen. Maar merk op dat het introduceren van eenvoudige getters en setters in Bestellen zou gemakkelijk de inkapseling van ons model kunnen doorbreken en zakelijke beperkingen kunnen schenden.

Laten we eens kijken wat er mis kan gaan.

2.2. Naïef Aggregate Design

Laten we ons eens voorstellen wat er zou kunnen gebeuren als we besloten om naïef getters en setters toe te voegen aan alle eigendommen op het Bestellen klasse, inclusief setOrderTotal.

Niets belet ons om de volgende code uit te voeren:

Order order = nieuwe order (); order.setOrderLines (Arrays.asList (orderLine0, orderLine1)); order.setTotalCost (Money.zero (CurrencyUnit.USD)); // dit ziet er niet goed uit ...

In deze code stellen we handmatig de totale prijs eigendom op nul zetten, wat in strijd is met een belangrijke zakelijke regel. Absoluut, de totale kosten mogen niet nul dollar zijn!

We hebben een manier nodig om onze bedrijfsregels te beschermen. Laten we eens kijken hoe Aggregate Roots kan helpen.

2.3. Aggregate Root

Een geaggregeerde root is een klasse die werkt als een toegangspunt tot ons aggregaat. Alle bedrijfsactiviteiten moeten via de root gaan. Op deze manier kan de geaggregeerde root ervoor zorgen dat het aggregaat in een consistente staat blijft.

De wortel is wat zorgt voor al onze zakelijke invarianten.

En in ons voorbeeld is de Bestellen class is de juiste kandidaat voor de geaggregeerde root. We hoeven alleen maar enkele wijzigingen aan te brengen om ervoor te zorgen dat het aggregaat altijd consistent is:

class Order {private final List orderLines; privégeld totaalkosten; Order (lijst orderLines) {checkNotNull (orderLines); if (orderLines.isEmpty ()) {throw new IllegalArgumentException ("Order moet ten minste één orderregelitem hebben"); } this.orderLines = nieuwe ArrayList (orderLines); totalCost = berekenTotalCost (); } void addLineItem (OrderLine orderLine) {checkNotNull (orderLine); orderLines.add (orderLine); totalCost = totalCost.plus (orderLine.cost ()); } void removeLineItem (int regel) {OrderLine removeLine = orderLines.remove (regel); totalCost = totalCost.minus (removeLine.cost ()); } Money totalCost () {retourneer totalCost; } // ...}

Door een geaggregeerde wortel te gebruiken, kunnen we nu gemakkelijker draaien Product en OrderLine in onveranderlijke objecten, waar alle eigenschappen definitief zijn.

Zoals we kunnen zien, is dit een vrij eenvoudig aggregaat.

En we hadden eenvoudig elke keer de totale kosten kunnen berekenen zonder een veld te gebruiken.

Op dit moment hebben we het echter alleen over geaggregeerde persistentie, niet over geaggregeerd ontwerp. Blijf op de hoogte, want dit specifieke domein zal zo goed van pas komen.

Hoe goed speelt dit met persistentietechnologieën? Laten we kijken. Uiteindelijk zal dit ons helpen om de juiste persistentietool te kiezen voor ons volgende project.

3. JPA en slaapstand

Laten we in dit gedeelte proberen onze Bestellen aggregeren met behulp van JPA en Hibernate. We gebruiken Spring Boot en JPA-starter:

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

Voor de meesten van ons lijkt dit de meest natuurlijke keuze te zijn. We hebben tenslotte jarenlang met relationele systemen gewerkt en we kennen allemaal populaire ORM-frameworks.

Waarschijnlijk het grootste probleem bij het werken met ORM-frameworks is de vereenvoudiging van ons modelontwerp. Het wordt ook wel Object-relationele impedantie-mismatch genoemd. Laten we eens nadenken over wat er zou gebeuren als we ons wilden voortzetten Bestellen aggregaat:

@DisplayName ("gegeven order met twee regelitems, indien persistent, dan order wordt opgeslagen") @Test public void test () gooit uitzondering {// gegeven JpaOrder order = preparTestOrderWithTwoLineItems (); // wanneer JpaOrder opgeslagenOrder = repository.save (bestelling); // dan JpaOrder foundOrder = repository.findById (opgeslagenOrder.getId ()) .get (); assertThat (foundOrder.getOrderLines ()). hasSize (2); }

Op dit punt zou deze test een uitzondering opleveren: java.lang.IllegalArgumentException: onbekende entiteit: com.baeldung.ddd.order.Order. Het is duidelijk dat we enkele van de JPA-vereisten missen:

  1. Voeg kaartannotaties toe
  2. OrderLine en Product klassen moeten entiteiten zijn of @Embeddable klassen, geen eenvoudige waardeobjecten
  3. Voeg een lege constructor toe voor elke entiteit of @Embeddable klasse
  4. Vervangen Geld eigenschappen met eenvoudige typen

Hmm, we moeten het ontwerp van Bestellen aggregeren om JPA te kunnen gebruiken. Hoewel het toevoegen van annotaties geen probleem is, kunnen de andere vereisten voor veel problemen zorgen.

3.1. Wijzigingen aan de waardeobjecten

De eerste kwestie van het proberen om een ​​aggregaat in de PPV in te passen, is dat we het ontwerp van onze waardevoorwerpen moeten doorbreken: hun eigenschappen kunnen niet langer definitief zijn en we moeten de inkapseling doorbreken.

We moeten kunstmatige ID's toevoegen aan het OrderLine en Product, zelfs als deze klassen nooit zijn ontworpen om ID's te hebben. We wilden dat het eenvoudige waardeobjecten waren.

Het is mogelijk om @Embedded en @ElementCollection annotaties, maar deze benadering kan de zaken erg ingewikkeld maken bij het gebruik van een complexe objectgrafiek (bijvoorbeeld @Embeddable object met een ander @Embedded eigendom etc.).

Gebruik makend van @Embedded annotatie voegt eenvoudig platte eigenschappen toe aan de bovenliggende tabel. Behalve dat, basiseigenschappen (bijv. Van Draad type) vereisen nog steeds een setter-methode, die in strijd is met het gewenste waardeobjectontwerp.

Een lege constructorvereiste dwingt de waarde van de objecteigenschappen om niet meer definitief te zijn, waardoor een belangrijk aspect van ons oorspronkelijke ontwerp wordt verbroken. Eerlijk gezegd kan Hibernate de private no-args-constructor gebruiken, wat het probleem een ​​beetje verlicht, maar het is nog lang niet perfect.

Zelfs als we een privé-standaardconstructor gebruiken, kunnen we onze eigenschappen niet als definitief markeren of we moeten ze initialiseren met standaardwaarden (vaak null) in de standaardconstructor.

Als we echter volledig JPA-compatibel willen zijn, moeten we op zijn minst beschermde zichtbaarheid gebruiken voor de standaardconstructor, wat betekent dat andere klassen in hetzelfde pakket waardeobjecten kunnen creëren zonder waarden van hun eigenschappen op te geven.

3.2. Complexe typen

Helaas kunnen we niet verwachten dat JPA automatisch complexe typen van derden in tabellen omzet. Kijk maar eens hoeveel wijzigingen we in de vorige sectie moesten aanbrengen!

Als u bijvoorbeeld samenwerkt met onze Bestellen geaggregeerd, zullen we moeilijkheden tegenkomen die aanhouden Joda Money velden.

In dat geval kunnen we eindigen met het schrijven van aangepast type @Converter beschikbaar vanaf JPA 2.1. Dat vereist misschien wat extra werk.

Als alternatief kunnen we ook de Geld onroerend goed in twee basiseigenschappen. Bijvoorbeeld Draad voor munteenheid en BigDecimal voor de werkelijke waarde.

Hoewel we de implementatiedetails kunnen verbergen en toch kunnen gebruiken Geld class via de API voor openbare methoden, leert de praktijk dat de meeste ontwikkelaars het extra werk niet kunnen rechtvaardigen en in plaats daarvan het model zouden degenereren om te voldoen aan de JPA-specificatie.

3.3. Conclusie

Hoewel JPA een van de meest geaccepteerde specificaties ter wereld is, is het misschien niet de beste optie om onze Bestellen aggregaat.

Als we willen dat ons model de echte bedrijfsregels weerspiegelt, moeten we het zo ontwerpen dat het geen simpele 1: 1 weergave van de onderliggende tabellen is.

In principe hebben we hier drie opties:

  1. Maak een set eenvoudige gegevensklassen en gebruik deze om het rijke bedrijfsmodel te behouden en opnieuw te creëren. Helaas kan dit veel extra werk vergen.
  2. Accepteer de beperkingen van JPA en kies het juiste compromis.
  3. Overweeg een andere technologie.

De eerste optie heeft het grootste potentieel. In de praktijk worden de meeste projecten ontwikkeld met behulp van de tweede optie.

Laten we nu eens kijken naar een andere technologie om aggregaten te behouden.

4. Documentopslag

Een documentopslag is een alternatieve manier om gegevens op te slaan. In plaats van relaties en tabellen te gebruiken, slaan we hele objecten op. Dit maakt een documentopslag een potentieel perfecte kandidaat voor blijvende aggregaten.

Voor de behoeften van deze tutorial zullen we ons concentreren op JSON-achtige documenten.

Laten we eens nader bekijken hoe ons probleem met de persistentie van bestellingen eruitziet in een documentenwinkel als MongoDB.

4.1. Aanhoudende aggregatie met MongoDB

Nu zijn er nogal wat databases die JSON-gegevens kunnen opslaan, een van de populaire is MongoDB. MongoDB slaat BSON of JSON in feite op in binaire vorm.

Dankzij MongoDB kunnen we de Bestellen voorbeeld aggregaat zoals het is.

Voordat we verder gaan, voegen we de Spring Boot MongoDB-starter toe:

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

Nu kunnen we een vergelijkbare testcase uitvoeren zoals in het JPA-voorbeeld, maar deze keer met MongoDB:

@DisplayName ("gegeven order met twee regelitems, wanneer aanhouden met mongo repository, dan order wordt opgeslagen") @Test void test () gooit uitzondering {// gegeven order order = preparTestOrderWithTwoLineItems (); // when repo.save (volgorde); // dan List foundOrders = repo.findAll (); assertThat (foundOrders) .hasSize (1); Lijst gevondenOrderLines = foundOrders.iterator () .next () .getOrderLines (); assertThat (foundOrderLines) .hasSize (2); assertThat (foundOrderLines) .containsOnlyElementsOf (order.getOrderLines ()); }

Wat belangrijk is - we hebben het origineel niet gewijzigd Bestellen geaggregeerde klassen helemaal; het is niet nodig om standaard constructors, setters of aangepaste converters voor te maken Geld klasse.

En hier is wat onze Bestellen aggregaat verschijnt in de winkel:

{"_id": ObjectId ("5bd8535c81c04529f54acd14"), "orderLines": [{"product": {"price": {"money": {"currency": {"code": "USD", "numericCode": 840, "decimalPlaces": 2}, "amount": "10.00"}}}, "aantal": 2}, {"product": {"prijs": {"geld": {"valuta": {"code ":" USD "," numericCode ": 840," decimalPlaces ": 2}," amount ":" 5.00 "}}}," aantal ": 10}]," totalCost ": {" geld ": {" valuta ": {" code ":" USD "," numericCode ": 840," decimalPlaces ": 2}," amount ":" 70.00 "}}," _class ":" com.baeldung.ddd.order.mongo.Order "}

Dit eenvoudige BSON-document bevat het geheel Bestellen aggregeren uit één stuk, wat mooi aansluit bij ons oorspronkelijke idee dat dit allemaal samen consistent moet zijn.

Merk op dat complexe objecten in het BSON-document eenvoudigweg worden geserialiseerd als een set reguliere JSON-eigenschappen. Dankzij dit kunnen zelfs klassen van derden (zoals Joda Money) kunnen eenvoudig worden geserialiseerd zonder dat het model hoeft te worden vereenvoudigd.

4.2. Conclusie

Aanhoudende aggregaten met MongoDB is eenvoudiger dan met JPA.

Dit betekent absoluut niet dat MongoDB superieur is aan traditionele databases. Er zijn tal van legitieme gevallen waarin we niet eens moeten proberen onze klassen als aggregaten te modelleren en in plaats daarvan een SQL-database te gebruiken.

Maar als we een groep objecten hebben geïdentificeerd die altijd consistent moeten zijn volgens de complexe vereisten, kan het gebruik van een documentarchief een zeer aantrekkelijke optie zijn.

5. Conclusie

In DDD bevatten aggregaten meestal de meest complexe objecten in het systeem. Om met hen samen te werken, is een heel andere aanpak nodig dan bij de meeste CRUD-toepassingen.

Het gebruik van populaire ORM-oplossingen kan leiden tot een simplistisch of overbelicht domeinmodel, dat vaak niet in staat is om ingewikkelde bedrijfsregels uit te drukken of af te dwingen.

Documentopslag kan het gemakkelijker maken om aggregaten te behouden zonder de complexiteit van het model op te offeren.

De volledige broncode van alle voorbeelden is beschikbaar op GitHub.