Trajni DDD agregati

1. Pregled

U ovom uputstvu istražit ćemo mogućnosti postojanih DDD agregata pomoću različitih tehnologija.

2. Uvod u agregate

Agregat je skupina poslovnih objekata koji uvijek trebaju biti dosljedni. Stoga u transakciji spremamo i ažuriramo agregate u cjelini.

Agregat je važan taktički obrazac u DDD-u, koji pomaže u održavanju dosljednosti naših poslovnih objekata. Međutim, ideja agregata korisna je i izvan konteksta DDD-a.

Brojni su poslovni slučajevi u kojima ovaj obrazac može dobro doći. Kao pravilo, trebali bismo razmotriti upotrebu agregata kada se više objekata mijenja kao dio iste transakcije.

Pogledajmo kako bismo to mogli primijeniti prilikom modeliranja narudžbe.

2.1. Primjer narudžbenice

Dakle, pretpostavimo da želimo oblikovati narudžbenicu:

class Order {privatna kolekcija orderLines; privatni novac totalCost; // ...}
klasa OrderLine {proizvod privatnog proizvoda; privatna int količina; // ...}
klasa Proizvod {cijena privatnog novca; // ...}

Te klase čine jednostavan agregat. Oba Redovi narudžbe i Ukupni trošak polja Narudžba mora biti uvijek dosljedan, tj Ukupni trošak treba uvijek imati vrijednost jednaku zbroju svih Redovi narudžbe.

Sad bismo svi mogli doći u iskušenje da sve to pretvorimo u punopravni Java Beans. No, imajte na umu da je uvođenje jednostavnih getera i postavljača u Narudžba mogao lako razbiti enkapsulaciju našeg modela i prekršiti poslovna ograničenja.

Da vidimo što bi moglo poći po zlu.

2.2. Naivni agregatni dizajn

Zamislimo što bi se moglo dogoditi ako bismo odlučili naivno dodati gettere i settere svim svojstvima na Narudžba razred, uključujući setOrderTotal.

Ne postoji ništa što nam zabranjuje izvršavanje sljedećeg koda:

Narudžba narudžbe = nova narudžba (); order.setOrderLines (Arrays.asList (orderLine0, orderLine1)); order.setTotalCost (Money.zero (CurrencyUnit.USD)); // ovo ne izgleda dobro ...

U ovom kodu ručno postavljamo Ukupni trošak imovine na nulu, kršeći važno poslovno pravilo. Definitivno, ukupni trošak ne bi trebao biti nula dolara!

Trebamo način da zaštitimo naša poslovna pravila. Pogledajmo kako skupni korijeni mogu pomoći.

2.3. Zbirni korijen

An agregatni korijen je klasa koja djeluje kao ulazna točka našeg agregata. Sve poslovne operacije trebale bi proći kroz korijen. Na ovaj način, agregatni korijen može se pobrinuti za održavanje agregata u dosljednom stanju.

Korijen je ono što brine o svim našim poslovnim invarijantima.

I u našem primjeru, Narudžba klasa je pravi kandidat za agregatni korijen. Samo trebamo izvršiti neke izmjene kako bismo osigurali da je agregat uvijek dosljedan:

class Order {privatni konačni Popis orderLines; privatni novac totalCost; Narudžba (Popis redoslijeda) {checkNotNull (red narudžbi); if (orderLines.isEmpty ()) {throw new IllegalArgumentException ("Narudžba mora imati barem jednu stavku retka narudžbe"); } this.orderLines = novi ArrayList (orderLines); totalCost = izračunajTotalCost (); } void addLineItem (OrderLine orderLine) {checkNotNull (orderLine); orderLines.add (orderLine); totalCost = totalCost.plus (orderLine.cost ()); } void removeLineItem (int line) {OrderLine removedLine = orderLines.remove (line); totalCost = totalCost.minus (removedLine.cost ()); } Novac totalCost () {return totalCost; } // ...}

Korištenje agregatnog korijena sada nam omogućuje lakše okretanje Proizvod i Linija narudžbe u nepromjenjive predmete, gdje su sva svojstva konačna.

Kao što vidimo, ovo je prilično jednostavan agregat.

A, mogli smo jednostavno izračunati ukupni trošak svaki put bez korištenja polja.

Međutim, trenutno govorimo samo o postojanosti agregata, a ne o dizajnu agregata. Pratite nas jer će ova specifična domena za trenutak dobro doći.

Koliko se ovo dobro poklapa s tehnologijama postojanosti? Pogledajmo. U konačnici, ovo će nam pomoći da odaberemo pravi alat za ustrajnost za sljedeći projekt.

3. JPA i hibernacija

U ovom odjeljku pokušajmo ustrajati na našim Narudžba agregat pomoću JPA i Hibernate. Koristit ćemo Spring Boot i JPA starter:

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

Za većinu nas čini se da je ovo najprirodniji izbor. Napokon, proveli smo godine radeći s relacijskim sustavima i svi znamo popularne ORM okvire.

Vjerojatno najveći problem u radu s ORM okvirima je pojednostavljenje dizajna našeg modela. Također se ponekad naziva i nepodudaranje objektno-relacijske impedancije. Razmislimo o tome što bi se dogodilo da želimo ustrajati na svom Narudžba agregat:

@DisplayName ("zadana narudžba s dvije stavke retka, kada se nastavi, tada se narudžba sprema") @Test javni void test () baca iznimku {// zadana JpaOrder narudžba = pripremaTestOrderWithTwoLineItems (); // kada je JpaOrder savedOrder = repository.save (poredak); // tada JpaOrder foundOrder = repository.findById (savedOrder.getId ()) .get (); assertThat (foundOrder.getOrderLines ()). hasSize (2); }

U ovom bi trenutku ovaj test donio iznimku: java.lang.IllegalArgumentException: Nepoznata cjelina: com.baeldung.ddd.order.Order. Očito nam nedostaju neki od zahtjeva JPA:

  1. Dodajte bilješke mapiranja
  2. Linija narudžbe i Proizvod klase moraju biti entiteti ili @Embeddable klase, a ne jednostavni vrijednosni objekti
  3. Dodajte prazan konstruktor za svaki entitet ili @Embeddable razred
  4. Zamijeniti Novac svojstva s jednostavnim vrstama

Hmm, moramo izmijeniti dizajn Narudžba agregat kako bi mogao koristiti JPA. Iako dodavanje napomena nije velika stvar, ostali zahtjevi mogu uvesti puno problema.

3.1. Promjene na objektima vrijednosti

Prvo pitanje pokušaja uklapanja agregata u JPA jest da moramo razbiti dizajn naših vrijednosnih objekata: Njihova svojstva više ne mogu biti konačna i moramo razbiti enkapsulaciju.

Moramo dodati umjetne ID-ove u Linija narudžbe i Proizvod, čak i ako ove klase nikada nisu dizajnirane da imaju identifikatore. Željeli smo da to budu jednostavni vrijednosni objekti.

Moguće je koristiti @Ugrađen i @ElementCollection umjesto toga, ali ovaj pristup može puno zakomplicirati kada se koristi složeni objektni graf (na primjer @Embeddable objekt koji ima drugog @Ugrađen imovine itd.).

Koristeći @Ugrađen napomena jednostavno dodaje ravna svojstva nadređenoj tablici. Osim toga, osnovna svojstva (npr. Niz type) i dalje zahtijeva metodu postavljanja, koja krši dizajn željenog objekta vrijednosti.

Zahtjev za praznim konstruktorom prisiljava svojstva objekta vrijednosti da više nisu konačna, čime se razbija važan aspekt našeg izvornog dizajna. Istini za volju, Hibernate može koristiti privatni no-args konstruktor, koji malo ublažava problem, ali još uvijek nije savršen.

Čak i kada koristimo privatni zadani konstruktor, ili ne možemo označiti svoja svojstva kao konačna ili ih moramo inicijalizirati zadanim (često null) vrijednostima unutar zadanog konstruktora.

Međutim, ako želimo biti u potpunosti JPA-usklađeni, moramo koristiti barem zaštićenu vidljivost za zadani konstruktor, što znači da druge klase u istom paketu mogu stvarati vrijednosne objekte bez navođenja vrijednosti njihovih svojstava.

3.2. Kompleksne vrste

Nažalost, ne možemo očekivati ​​da JPA automatski preslikava složene vrste nezavisnih proizvođača u tablice. Pogledajte samo koliko smo promjena morali uvesti u prethodnom odjeljku!

Na primjer, kada radite s našim Narudžba zajedno, naići ćemo na poteškoće koje će i dalje trajati Joda novac polja.

U tom bismo slučaju mogli završiti s pisanjem prilagođenog tipa @Konverter dostupno od JPA 2.1. To bi, međutim, moglo zahtijevati dodatni rad.

Alternativno, možemo podijeliti i Novac svojstvo u dva osnovna svojstva. Na primjer Niz za novčanu jedinicu i BigDecimal za stvarnu vrijednost.

Iako možemo sakriti detalje implementacije i još uvijek ih koristiti Novac klase putem API-ja javnih metoda, praksa pokazuje da većina programera ne može opravdati dodatni rad i jednostavno bi degenerirala model kako bi odgovarao JPA specifikaciji.

3.3. Zaključak

Iako je JPA jedna od najčešće prihvaćenih specifikacija na svijetu, možda nije najbolja opcija za ustrajavanje na našim Narudžba agregat.

Ako želimo da naš model odražava istinska poslovna pravila, trebali bismo ga dizajnirati da ne bude jednostavni prikaz temeljnih tablica u omjeru 1: 1.

U osnovi, ovdje imamo tri mogućnosti:

  1. Stvorite skup jednostavnih klasa podataka i upotrijebite ih za ustrajanje i ponovno stvaranje bogatog poslovnog modela. Nažalost, ovo bi moglo zahtijevati puno dodatnog rada.
  2. Prihvatite ograničenja JPA i odaberite pravi kompromis.
  3. Razmotrimo drugu tehnologiju.

Prva opcija ima najveći potencijal. U praksi se većina projekata razvija pomoću druge mogućnosti.

Sada, razmotrimo još jednu tehnologiju za zadržavanje agregata.

4. Pohrana dokumenata

Pohrana dokumenata alternativni je način pohrane podataka. Umjesto da koristimo relacije i tablice, spremamo cijele objekte. To trgovinu dokumentima čini potencijalno savršenim kandidatom za trajne agregate.

Za potrebe ovog vodiča usredotočit ćemo se na JSON-slične dokumente.

Pogledajmo izbliza kako izgleda naš problem postojanosti narudžbe u spremištu dokumenata poput MongoDB-a.

4.1. Trajno agregiranje pomoću MongoDB-a

Sada postoji podosta baza podataka u koje se mogu pohraniti JSON podaci, a jedna od popularnih je MongoDB. MongoDB zapravo pohranjuje BSON ili JSON u binarnom obliku.

Zahvaljujući MongoDB-u možemo pohraniti Narudžba primjer agregat kao što je.

Prije nego što krenemo dalje, dodajmo Spring Boot MongoDB starter:

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

Sada možemo pokrenuti sličan testni slučaj kao u primjeru JPA, ali ovaj put koristeći MongoDB:

@DisplayName ("zadana narudžba s dvije stavke retka, kada se nastavi upotrebljavati mongo spremište, tada se narudžba sprema") @Test void test () baca iznimku {// zadata narudžba narudžbe = pripremaTestOrderWithTwoLineItems (); // kada repo.save (poredak); // zatim popisa foundOrders = repo.findAll (); assertThat (foundOrders) .hasSize (1); Popis foundOrderLines = foundOrders.iterator () .next () .getOrderLines (); assertThat (foundOrderLines) .hasSize (2); assertThat (foundOrderLines) .containsOnlyElementsOf (order.getOrderLines ()); }

Ono što je važno - nismo promijenili original Narudžba agregatne klase uopće; nema potrebe za izradom zadanih konstruktora, postavljača ili prilagođenog pretvarača za Novac razred.

A ovdje je ono što je naše Narudžba agregat se pojavljuje u trgovini:

{"_id": ObjectId ("5bd8535c81c04529f54acd14"), "orderLines": [{"product": {"price": {"money": {"currency": {"code": "USD", "numericCode": 840, "decimalPlaces": 2}, "iznos": "10,00"}}}, "količina": 2}, {"product": {"price": {"money": {"currency": {"code ":" USD "," numericCode ": 840," decimalPlaces ": 2}," iznos ":" 5,00 "}}}," količina ": 10}]," totalCost ": {" money ": {" valuta ": {" code ":" USD "," numericCode ": 840," decimalPlaces ": 2}," amount ":" 70.00 "}}," _class ":" com.baeldung.ddd.order.mongo.Order "}

Ovaj jednostavni BSON dokument sadrži cjelinu Narudžba objediniti u jednom komadu, lijepo se podudarajući s našim izvornim shvaćanjem da bi sve to trebalo biti zajednički dosljedno.

Imajte na umu da se složeni objekti u BSON dokumentu jednostavno serializiraju kao skup regularnih JSON svojstava. Zahvaljujući tome, čak i treće strane (poput Joda novac) može se lako serijalizirati bez potrebe za pojednostavljivanjem modela.

4.2. Zaključak

Postojanje agregata pomoću MongoDB-a jednostavnije je od korištenja JPA.

To apsolutno ne znači da je MongoDB superiorniji od tradicionalnih baza podataka. Puno je legitimnih slučajeva u kojima ne bismo trebali pokušati modelirati svoje klase kao agregate i umjesto toga koristiti SQL bazu podataka.

Ipak, kada smo identificirali skupinu objekata koji bi uvijek trebali biti dosljedni u skladu sa složenim zahtjevima, tada upotreba spremišta dokumenata može biti vrlo privlačna opcija.

5. Zaključak

U DDD-u agregati obično sadrže najsloženije objekte u sustavu. Rad s njima treba sasvim drugačiji pristup nego u većini CRUD aplikacija.

Korištenje popularnih ORM rješenja može dovesti do pojednostavljenog ili previše eksponiranog modela domene koji često nije u stanju izraziti ili provesti zamršena poslovna pravila.

Pohrane dokumenata mogu olakšati ustrajavanje agregata bez žrtvovanja složenosti modela.

Potpuni izvorni kod svih primjera dostupan je na GitHubu.