Pretrvávajúce agregáty DDD

1. Prehľad

V tomto tutoriáli preskúmame možnosti pretrvávania agregátov DDD pomocou rôznych technológií.

2. Úvod do kameniva

Agregát je skupina obchodných objektov, ktoré musia byť vždy konzistentné. Preto v rámci transakcie ukladáme a aktualizujeme agregáty ako celok.

Agregát je dôležitým taktickým vzorom v DDD, ktorý pomáha udržiavať konzistenciu našich obchodných objektov. Myšlienka agregácie je však užitočná aj mimo kontextu DDD.

Existuje mnoho obchodných prípadov, keď sa tento vzor môže hodiť. Ako základné pravidlo by sme mali zvážiť použitie agregátov, keď je v rámci tej istej transakcie zmenených viac objektov.

Poďme sa pozrieť na to, ako by sme to mohli použiť pri modelovaní nákupu objednávky.

2.1. Príklad nákupnej objednávky

Predpokladajme teda, že chceme modelovať objednávku:

trieda Objednávka {private Collection orderLines; celkové peniaze zo súkromných peňazí; // ...}
trieda OrderLine {produkt súkromného produktu; súkromné ​​množstvo; // ...}
trieda Produkt {cena súkromných peňazí; // ...}

Tieto triedy tvoria jednoduchý agregát. Oboje orderLines a Celkové náklady polia objednať musí byť vždy konzistentný, to je Celkové náklady by mala mať vždy hodnotu rovnajúcu sa súčtu všetkých orderLines.

Všetci by sme teraz mohli byť v pokušení zmeniť všetky z nich na plnohodnotné Java Beans. Ale nezabudnite, že zavádzanie jednoduchých getrov a setterov je v objednať môže ľahko prelomiť zapuzdrenie nášho modelu a porušiť obchodné obmedzenia.

Pozrime sa, čo by sa mohlo pokaziť.

2.2. Naivný agregátny dizajn

Poďme si predstaviť, čo by sa mohlo stať, keby sme sa rozhodli naivne pridať getry a setre ku všetkým vlastnostiam na serveri objednať trieda vrátane setOrderTotal.

Vykonanie nasledujúceho kódu nám nebráni nič:

Objednávka objednávky = nová objednávka (); order.setOrderLines (Arrays.asList (orderLine0, orderLine1)); order.setTotalCost (Money.zero (CurrencyUnit.USD)); // toto nevyzerá dobre ...

V tomto kóde sme manuálne nastavili Celkové náklady majetok na nulu, čím sa porušuje dôležité obchodné pravidlo. Celkové náklady by rozhodne nemali byť nula dolárov!

Potrebujeme spôsob ochrany našich obchodných pravidiel. Pozrime sa, ako môžu agregované korene pomôcť.

2.3. Súhrnný koreň

An agregátny koreň je trieda, ktorá funguje ako vstupný bod do nášho agregátu. Všetky obchodné operácie by mali prechádzať koreňom. Týmto spôsobom sa koreňový agregát môže postarať o udržanie agregátu v konzistentnom stave.

Koreň je to, čo sa stará o všetky naše obchodné invarianty.

A v našom príklade objednať trieda je správnym kandidátom na agregovaný koreň. Potrebujeme urobiť niekoľko úprav, aby sme zaistili, že agregát bude vždy konzistentný:

trieda Objednávka {private final List orderLines; celkové peniaze zo súkromných peňazí; Order (List orderLines) {checkNotNull (orderLines); if (orderLines.isEmpty ()) {throw new IllegalArgumentException ("Objednávka musí mať aspoň jednu riadkovú položku objednávky"); } this.orderLines = new ArrayList (orderLines); totalCost = countTotalCost (); } void addLineItem (OrderLine orderLine) {checkNotNull (orderLine); orderLines.add (orderLine); totalCost = totalCost.plus (orderLine.cost ()); } void removeLineItem (int riadok) {OrderLine odstránenýLine = orderLines.remove (riadok); totalCost = totalCost.minus (odstránenýLine.cost ()); } Peniaze totalCost () {return totalCost; } // ...}

Používanie agregovaného koreňa nám teraz umožňuje ľahšie sa otočiť Výrobok a OrderLine do nemenných objektov, kde sú všetky vlastnosti konečné.

Ako vidíme, jedná sa o celkom jednoduchý agregát.

Mohli sme jednoducho kedykoľvek vypočítať celkové náklady bez použitia poľa.

Momentálne však hovoríme iba o vytrvalosti agregátu, nie o agregovanom dizajne. Zostaňte naladení, pretože táto konkrétna doména vám príde o chvíľu vhod.

Ako dobre sa to hrá s technológiami vytrvalosti? Pozrime sa. To nám v konečnom dôsledku pomôže vybrať ten správny nástroj na vytrvalosť pre náš ďalší projekt.

3. JPA a hibernácia

V tejto časti skúsme vytrvať objednať agregovať pomocou JPA a Hibernate. Použijeme Spring Boot a štartér JPA:

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

Pre väčšinu z nás sa to javí ako najprirodzenejšia voľba. Nakoniec sme strávili roky prácou s relačnými systémami a všetci poznáme populárne rámce ORM.

Pravdepodobne najväčším problémom pri práci s rámcami ORM je zjednodušenie návrhu nášho modelu. Tiež sa niekedy označuje ako nesúlad objektovo-relačných impedancií. Zamyslime sa nad tým, čo by sa stalo, keby sme chceli vytrvať v našom objednať agregát:

@DisplayName („zadaná objednávka s dvoma riadkovými položkami, ak bude pretrvávať, potom sa objednávka uloží“) @Test public void test () vyvolá výnimku {// daný JpaOrder order = prepareTestOrderWithTwoLineItems (); // keď JpaOrder savedOrder = repository.save (objednávka); // potom JpaOrder foundOrder = repository.findById (savedOrder.getId ()) .get (); assertThat (foundOrder.getOrderLines ()). hasSize (2); }

V tomto okamihu by tento test vyvolal výnimku: java.lang.IllegalArgumentException: Neznáma entita: com.baeldung.ddd.order.Order. Je zrejmé, že nám chýbajú niektoré z požiadaviek JPA:

  1. Pridajte mapovacie anotácie
  2. OrderLine a Výrobok triedy musia byť entity alebo @Embeddable triedy, nie jednoduché hodnotové objekty
  3. Pridajte prázdny konštruktor pre každú entitu alebo @Embeddable trieda
  4. Vymeniť Peniaze vlastnosti jednoduchými typmi

Hmm, musíme upraviť dizajn objednať agregát, aby bolo možné používať JPA. Aj keď pridávanie anotácií nie je veľký problém, ďalšie požiadavky môžu spôsobiť veľa problémov.

3.1. Zmeny v hodnotových objektoch

Prvým problémom pri pokuse vložiť agregát do JPA je, že musíme prelomiť dizajn našich hodnotových objektov: Ich vlastnosti už nemôžu byť konečné a musíme prelomiť zapuzdrenie.

Musíme pridať umelé ID do OrderLine a Výrobok, aj keď tieto triedy nikdy neboli navrhnuté tak, aby mali identifikátory. Chceli sme, aby to boli jednoduché hodnotné objekty.

Je možné použiť @ Zabudované a @ElementCollection namiesto toho anotácie, ale tento prístup môže pri použití grafu zložitých objektov (napr @Embeddable objekt majúci iný @ Zabudované majetok atď.).

Použitím @ Zabudované anotácia jednoducho pridá ploché vlastnosti do nadradenej tabuľky. Okrem toho základné vlastnosti (napr String typu) stále vyžadujú metódu setter, ktorá porušuje požadovanú hodnotu objektu design.

Prázdna požiadavka na konštruktora núti, aby vlastnosti hodnotových objektov už neboli konečné, čo porušuje dôležitý aspekt nášho pôvodného návrhu. Pravdupovediac, Hibernate môže používať súkromný konštruktor bez argov, ktorý problém trochu zmierňuje, ale stále nie je ani zďaleka dokonalý.

Aj keď používame súkromný predvolený konštruktor, buď nemôžeme označiť naše vlastnosti ako konečné, alebo ich musíme inicializovať predvolenými (často nulovými) hodnotami vo vnútri predvoleného konštruktora.

Ak však chceme, aby boli plne kompatibilné s JPA, musíme pre predvolený konštruktor použiť aspoň chránenú viditeľnosť, čo znamená, že iné triedy v rovnakom balíku môžu vytvárať hodnotové objekty bez určenia hodnôt ich vlastností.

3.2. Komplexné typy

Nanešťastie nemôžeme očakávať, že SPS automaticky mapuje zložité typy tretích strán do tabuliek. Stačí si pozrieť, koľko zmien sme museli zaviesť v predchádzajúcej časti!

Napríklad pri práci s našimi objednať agregátne, stretneme sa s pretrvávajúcimi ťažkosťami Joda Peniaze polia.

V takom prípade by sme mohli skončiť písaním vlastného typu @Converter dostupné od JPA 2.1. To by si však mohlo vyžadovať ďalšiu prácu.

Prípadne môžeme tiež rozdeliť Peniaze majetku na dve základné vlastnosti. Napríklad String pre menovú jednotku a BigDecimal pre skutočnú hodnotu.

Aj keď môžeme skryť podrobnosti implementácie a stále používať Peniaze triedy prostredníctvom verejných metód API, prax ukazuje, že väčšina vývojárov nemôže ospravedlniť prácu navyše a jednoducho by zdegenerovala model tak, aby vyhovoval špecifikácii JPA.

3.3. Záver

Aj keď je JPA jednou z najprijímanejších špecifikácií na svete, nemusí to byť najlepšia voľba na zotrvanie na našej objednať agregát.

Ak chceme, aby náš model odrážal skutočné obchodné pravidlá, mali by sme ho navrhnúť tak, aby nešlo o jednoduché znázornenie podkladových tabuliek v pomere 1: 1.

V zásade tu máme tri možnosti:

  1. Vytvorte sadu jednoduchých tried údajov a použite ich na udržanie a opätovné vytvorenie bohatého obchodného modelu. To si, bohužiaľ, môže vyžadovať veľa práce navyše.
  2. Prijmite obmedzenia JPA a vyberte správny kompromis.
  3. Zvážte inú technológiu.

Prvý variant má najväčší potenciál. V praxi sa väčšina projektov vypracúva pomocou druhej možnosti.

Teraz zvážme inú technológiu na pretrvávanie agregátov.

4. Sklad dokumentov

Sklad dokumentov je alternatívny spôsob ukladania údajov. Namiesto použitia vzťahov a tabuliek ukladáme celé objekty. Toto robí z obchodu s dokumentmi potenciálne dokonalého kandidáta na pretrvávajúce agregáty.

Pre potreby tohto tutoriálu sa zameriame na dokumenty podobné formátu JSON.

Pozrime sa podrobnejšie, ako vyzerá náš problém s pretrvávaním objednávok v obchode s dokumentmi, ako je MongoDB.

4.1. Trvalé agregovanie pomocou MongoDB

Teraz existuje pomerne veľa databáz, ktoré môžu ukladať údaje JSON, jednou z populárnych je MongoDB. MongoDB skutočne ukladá BSON alebo JSON v binárnej forme.

Vďaka MongoDB môžeme ukladať objednať príklad agregát ako je.

Predtým, ako sa posunieme ďalej, pridajme štartovací program Spring Boot MongoDB:

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

Teraz môžeme spustiť podobný testovací prípad ako v príklade JPA, ale tentokrát s použitím MongoDB:

@DisplayName („zadaná objednávka s dvoma riadkovými položkami, ak bude pretrvávať použitie mongo úložiska, potom sa objednávka uloží“) @Test void test () vyvolá výnimku {// daný príkaz Order = prepareTestOrderWithTwoLineItems (); // keď repo.save (objednávka); // potom List foundOrders = repo.findAll (); assertThat (foundOrders) .hasSize (1); Zoznam foundOrderLines = foundOrders.iterator () .next () .getOrderLines (); assertThat (foundOrderLines) .hasSize (2); assertThat (foundOrderLines) .containsOnlyElementsOf (order.getOrderLines ()); }

Čo je dôležité - originál sme nezmenili objednať agregované triedy vôbec; nie je potrebné vytvárať predvolené konštruktory, nastavovače alebo vlastný prevodník pre Peniaze trieda.

A tu je to, čo je naše objednať agregát sa objaví v obchode:

{"_id": ObjectId ("5bd8535c81c04529f54acd14"), "orderLines": [{"produkt": {"cena": {"peniaze": {"mena": {"kód": "USD", "numericCode": 840, "decimalPlaces": 2}, "čiastka": "10,00"}}}, "množstvo": 2}, {"produkt": {"cena": {"peniaze": {"mena": {"kód ":" USD "," numericCode ": 840," decimalPlaces ": 2}," suma ":" 5.00 "}}}," množstvo ": 10}]," totalCost ": {" peniaze ": {" mena ": {" code ":" USD "," numericCode ": 840," decimalPlaces ": 2}," amount ":" 70.00 "}}," _class ":" com.baeldung.ddd.order.mongo.Order „}

Tento jednoduchý dokument BSON obsahuje celok objednať agregované v jednom kuse, čo sa pekne zhoduje s našou pôvodnou predstavou, že toto všetko by malo byť spoločne konzistentné.

Upozorňujeme, že zložité objekty v dokumente BSON sa jednoducho serializujú ako sada bežných vlastností JSON. Vďaka tomu dokonca aj hodiny tretích strán (ako napr Joda Peniaze) je možné ľahko serializovať bez potreby zjednodušenia modelu.

4.2. Záver

Pretrvávajúce agregáty pomocou MongoDB sú jednoduchšie ako použitie JPA.

To absolútne neznamená, že MongoDB je lepší ako tradičné databázy. Existuje veľa legitímnych prípadov, v ktorých by sme sa nemali ani snažiť modelovať naše triedy ako agregáty a namiesto nich používať databázu SQL.

Napriek tomu, keď sme identifikovali skupinu objektov, ktoré by mali byť vždy konzistentné podľa komplexných požiadaviek, potom môže byť použitie úložiska dokumentov veľmi atraktívnou voľbou.

5. Záver

V DDD agregáty zvyčajne obsahujú najkomplexnejšie objekty v systéme. Práca s nimi si vyžaduje veľmi odlišný prístup ako vo väčšine aplikácií CRUD.

Používanie populárnych riešení ORM môže viesť k zjednodušenému alebo príliš exponovanému modelu domény, ktorý často nedokáže vyjadriť alebo vynútiť zložité obchodné pravidlá.

Obchody s dokumentmi môžu uľahčiť pretrvávanie agregácií bez obetovania zložitosti modelu.

Celý zdrojový kód všetkých príkladov je k dispozícii na GitHub.