DDD ohraničené kontexty a moduly Java

1. Prehľad

Domain-Driven Design (DDD) je sada princípov a nástrojov, ktoré nám pomáhajú navrhovať efektívne softvérové ​​architektúry tak, aby poskytovali vyššiu obchodnú hodnotu. Bounded Context je jedným z ústredných a základných vzorov na záchranu architektúry z Big Ball Of Mud segregáciou celej aplikačnej domény na niekoľko sémanticky konzistentných častí.

Zároveň s Java 9 Module System môžeme vytvárať silne zapuzdrené moduly.

V tomto tutoriáli vytvoríme jednoduchú aplikáciu pre ukladanie a uvidíme, ako využiť moduly Java 9 pri definovaní explicitných hraníc pre ohraničené kontexty.

2. DDD ohraničené kontexty

V dnešnej dobe nie sú softvérové ​​systémy jednoduchými aplikáciami CRUD. Typický monolitický podnikový systém v skutočnosti pozostáva z niekoľkých starších kódových databáz a novo pridaných funkcií. Je však čoraz ťažšie udržiavať tieto systémy pri každej vykonanej zmene. Nakoniec sa to môže stať úplne neudržateľným.

2.1. Ohraničený kontext a všadeprítomný jazyk

Na vyriešenie riešeného problému poskytuje DDD koncept ohraničeného kontextu. Ohraničený kontext je logická hranica domény, kde konzistentne platia určité podmienky a pravidlá. Vo vnútri tejto hranice všetky pojmy, definície a pojmy tvoria všadeprítomný jazyk.

Hlavnou výhodou všadeprítomného jazyka je najmä zoskupovanie členov projektu z rôznych oblastí okolo konkrétnej obchodnej oblasti.

Viaceré kontexty môžu navyše fungovať s rovnakou vecou. Vo vnútri každého z týchto kontextov však môže mať rôzne významy.

2.2. Kontext objednávky

Začnime implementovať našu aplikáciu definovaním kontextu objednávky. Tento kontext obsahuje dve entity: OrderItem a Zákaznícka objednávka.

The Zákaznícka objednávka entita je agregovaný koreň:

verejná trieda CustomerOrder {private int orderId; súkromný reťazec PaymentMethod; súkromná adresa reťazca; zoznam súkromných položiek; public float countTotalPrice () {return orderItems.stream (). map (OrderItem :: getTotalPrice) .reduce (0F, Float :: sum); }}

Ako vidíme, táto trieda obsahuje vypočítaťCelkovú cenu obchodná metóda. Ale v projekte z reálneho sveta to bude pravdepodobne oveľa komplikovanejšie - napríklad vrátane zliav a daní v konečnej cene.

Ďalej vytvoríme OrderItem trieda:

public class OrderItem {private int productId; súkromné ​​množstvo; jednotka súkromného plavákaCena; súkromná plaváková jednotka Hmotnosť; }

Definovali sme entity, ale tiež musíme niektoré API vystaviť iným častiam aplikácie. Poďme vytvoriť CustomerOrderService trieda:

public class CustomerOrderService implementuje OrderService {public static final String EVENT_ORDER_READY_FOR_SHIPMENT = "OrderReadyForShipmentEvent"; private CustomerOrderRepository orderRepository; súkromný EventBus eventBus; @Override public void placeOrder (objednávka CustomerOrder) {this.orderRepository.saveCustomerOrder (objednávka); Užitočné zaťaženie mapy = nová HashMap (); payload.put ("order_id", String.valueOf (order.getOrderId ())); Udalosť ApplicationEvent = nová ApplicationEvent (užitočné zaťaženie) {@Override public String getType () {return EVENT_ORDER_READY_FOR_SHIPMENT; }}; this.eventBus.publish (udalosť); }}

Tu musíme zdôrazniť niekoľko dôležitých bodov. The miestoObjednávka metóda je zodpovedná za spracovanie objednávok zákazníkov. Po spracovaní objednávky je udalosť zverejnená na serveri EventBus. V ďalších kapitolách si prediskutujeme komunikáciu riadenú udalosťami. Táto služba poskytuje predvolenú implementáciu pre server OrderService rozhranie:

verejné rozhranie OrderService rozširuje ApplicationService {void placeOrder (objednávka CustomerOrder); void setOrderRepository (CustomerOrderRepository orderRepository); }

Ďalej táto služba vyžaduje CustomerOrderRepository pretrvávať objednávky:

verejné rozhranie CustomerOrderRepository {void saveCustomerOrder (objednávka CustomerOrder); }

Podstatné je to toto rozhranie nie je implementované v tomto kontexte, ale bude poskytované modulom infraštruktúry, ako uvidíme neskôr.

2.3. Prepravný kontext

Teraz definujme kontext prepravy. Bude tiež priamy a bude obsahovať tri entity: Pozemok, PackageItema ShippableOrder.

Začnime s ShippableOrder subjekt:

verejná trieda ShippableOrder {private int orderId; súkromná adresa reťazca; private List packageItems; }

V tomto prípade entita neobsahuje spôsob platby lúka. Je to preto, že v našom kontexte prepravy nás nezaujíma, aký spôsob platby sa použije. Kontext prepravy je zodpovedný iba za spracovanie zásielok objednávok.

Tiež Pozemok subjekt je špecifický pre kontext prepravy:

verejná trieda Parcel {private int orderId; súkromná adresa reťazca; private String trackingId; private List packageItems; public float countTotalWeight () {return packageItems.stream (). map (PackageItem :: getWeight) .reduce (0F, Float :: sum); } public boolean isTaxable () {return CalcEstimatedValue ()> 100; } verejný plavák CalculateEstimatedValue () {return packageItems.stream (). map (PackageItem :: getWeight) .reduce (0F, Float :: sum); }}

Ako vidíme, obsahuje aj konkrétne obchodné metódy a funguje ako agregovaný koreň.

Nakoniec definujme ParcelShippingService:

verejná trieda ParcelShippingService implementuje ShippingService {public static final String EVENT_ORDER_READY_FOR_SHIPMENT = "OrderReadyForShipmentEvent"; private ShippingOrderRepository orderRepository; súkromný EventBus eventBus; súkromná mapa shippedParcels = nová HashMap (); @Override public void shipOrder (int orderId) {Voliteľné poradie = this.orderRepository.findShippableOrder (orderId); order.ifPresent (completedOrder -> {Parcel parcel = nový Parcel (vyplnenýOrder.getOrderId (), vyplnenýOrder.getAddress (), vyplnenýOrder.getPackageItems ()); if (parcel.isTaxable ()) {// Vypočítať ďalšie dane} // Prepraviť balík this.shippedParcels.put (CompleteOrder.getOrderId (), balík);}); } @Override public void listenToOrderEvents () {this.eventBus.subscribe (EVENT_ORDER_READY_FOR_SHIPMENT, new EventSubscriber () {@Override public void onEvent (E event) {shipOrder (Integer.parseInt (event.getPayload_al ") }); } @Override public Voliteľné getParcelByOrderId (int orderId) {return Optional.ofNullable (this.shippedParcels.get (orderId)); }}

Táto služba podobne využíva ShippingOrderRepository na načítanie objednávok podľa id. Ešte dôležitejšie je, že sa hlási k OrderReadyForShipmentEvent udalosť, ktorá je zverejnená v inom kontexte. Ak nastane táto udalosť, služba použije určité pravidlá a doručí objednávku. Pre jednoduchosť ukladáme odoslané objednávky do a HashMap.

3. Kontextové mapy

Doteraz sme definovali dva kontexty. Nestanovili sme však medzi nimi žiadne výslovné vzťahy. Na tento účel má DDD koncept kontextového mapovania. Kontextová mapa je vizuálny popis vzťahov medzi rôznymi kontextmi systému. Táto mapa ukazuje, ako rôzne časti koexistujú spolu a vytvárajú doménu.

Medzi ohraničenými kontextmi existuje päť hlavných typov vzťahov:

  • Partnerstvo - vzťah medzi dvoma kontextmi, ktoré spolupracujú na zosúladení oboch tímov so závislými cieľmi
  • Zdieľané jadro - druh vzťahu, keď sa spoločné časti viacerých kontextov extrahujú do iného kontextu / modulu, aby sa znížila duplikácia kódu
  • Zákazník-dodávateľ - spojenie medzi dvoma kontextmi, kde jeden kontext (upstream) produkuje údaje a druhý (downstream) ich konzumuje. V tomto vzťahu majú obe strany záujem nadviazať najlepšiu možnú komunikáciu
  • Konformný - tento vzťah má tiež upstream a downstream, avšak downstream vždy zodpovedá API API proti prúdu
  • Protikorupčná vrstva - tento typ vzťahov sa často používa v prípade starších systémov na ich prispôsobenie novej architektúre a postupnú migráciu zo staršej kódovej základne. Antikorupčná vrstva slúži ako adaptér na preklad údajov z protiprúdového toku a na ochranu pred nežiaducimi zmenami

V našom konkrétnom príklade použijeme vzťah zdieľaného jadra. Nebudeme ho definovať v čistej podobe, ale bude väčšinou pôsobiť ako sprostredkovateľ udalostí v systéme.

Modul SharedKernel teda nebude obsahovať žiadne konkrétne implementácie, iba rozhrania.

Začnime s EventBus rozhranie:

verejné rozhranie EventBus {void publish (udalosť E); void subscribe (String eventType, EventSubscriber subscriber); void unsubscribe (String eventType, EventSubscriber subscriber); }

Toto rozhranie bude implementované neskôr v našom module Infraštruktúra.

Ďalej vytvoríme rozhranie základnej služby s predvolenými metódami na podporu komunikácie riadenej udalosťami:

verejné rozhranie ApplicationService {default void publishEvent (E event) {EventBus eventBus = getEventBus (); if (eventBus! = null) {eventBus.publish (event); }} default void subscribe (String eventType, EventSubscriber subscriber) {EventBus eventBus = getEventBus (); if (eventBus! = null) {eventBus.subscribe (eventType, subscriber); }} default void unsubscribe (String eventType, EventSubscriber subscriber) {EventBus eventBus = getEventBus (); if (eventBus! = null) {eventBus.unsubscribe (eventType, predplatiteľ); }} EventBus getEventBus (); void setEventBus (EventBus eventBus); }

Servisné rozhrania v ohraničených kontextoch teda rozširujú toto rozhranie o spoločné funkcie súvisiace s udalosťami.

4. Modulárnosť Java 9

Teraz je čas preskúmať, ako môže Java 9 Module System podporovať definovanú štruktúru aplikácie.

Systém Java Platform Module System (JPMS) nabáda na vytvorenie spoľahlivejších a silne zapuzdrených modulov. Výsledkom je, že tieto funkcie môžu pomôcť izolovať naše kontexty a stanoviť jasné hranice.

Pozrime sa na náš konečný diagram modulu:

4.1. Modul SharedKernel

Začnime s modulom SharedKernel, ktorý nezávisí od iných modulov. Takže modul-info.java vyzerá ako:

modul com.baeldung.dddmodules.sharedkernel {exportuje com.baeldung.dddmodules.sharedkernel.events; vývoz com.baeldung.dddmodules.sharedkernel.service; }

Exportujeme rozhrania modulov, aby boli dostupné pre ďalšie moduly.

4.2. OrderContext Modul

Ďalej sa zamerajme na modul OrderContext. Vyžaduje iba rozhrania definované v module SharedKernel:

modul com.baeldung.dddmodules.ordercontext {vyžaduje com.baeldung.dddmodules.sharedkernel; exportuje com.baeldung.dddmodules.ordercontext.service; export com.baeldung.dddmodules.ordercontext.model; exportuje com.baeldung.dddmodules.ordercontext.repository; poskytuje com.baeldung.dddmodules.ordercontext.service.OrderService s com.baeldung.dddmodules.ordercontext.service.CustomerOrderService; }

Vidíme tiež, že tento modul exportuje predvolenú implementáciu súboru OrderService rozhranie.

4.3. ShippingContext Modul

Podobne ako v predchádzajúcom module vytvorme definičný súbor modulu ShippingContext:

modul com.baeldung.dddmodules.shippingcontext {vyžaduje com.baeldung.dddmodules.sharedkernel; exportuje com.baeldung.dddmodules.shippingcontext.service; exportuje com.baeldung.dddmodules.shippingcontext.model; exportuje com.baeldung.dddmodules.shippingcontext.repository; poskytuje com.baeldung.dddmodules.shippingcontext.service.ShippingService s com.baeldung.dddmodules.shippingcontext.service.ParcelShippingService; }

Rovnakým spôsobom exportujeme predvolenú implementáciu pre Prepravná služba rozhranie.

4.4. Modul infraštruktúry

Teraz je čas popísať modul Infraštruktúra. Tento modul obsahuje podrobnosti implementácie pre definované rozhrania. Začneme vytvorením jednoduchej implementácie pre EventBus rozhranie:

verejná trieda SimpleEventBus implementuje EventBus {súkromná konečná mapa predplatitelia = new ConcurrentHashMap (); @Override public void publish (udalosť E) {if (subscribers.containsKey (event.getType ())) {subscribers.get (event.getType ()) .forEach (subscriber -> subscriber.onEvent (event)); }} @Override public void subscribe (String eventType, EventSubscriber subscriber) {Set eventSubscribers = subscribers.get (eventType); if (eventSubscribers == null) {eventSubscribers = new CopyOnWriteArraySet (); subscribers.put (eventType, eventSubscribers); } eventSubscribers.add (predplatiteľ); } @Override public void unsubscribe (String eventType, EventSubscriber subscriber) {if (subscribers.containsKey (eventType)) {subscribers.get (eventType) .remove (subscriber); }}}

Ďalej musíme implementovať CustomerOrderRepository a ShippingOrderRepository rozhrania. Vo väčšine prípadov objednať entita bude uložená v tej istej tabuľke, ale použije sa ako iný model entity v ohraničených kontextoch.

Je veľmi bežné vidieť jednu entitu obsahujúcu zmiešaný kód z rôznych oblastí obchodnej domény alebo nízkoúrovňových mapovaní databáz. Pre našu implementáciu sme naše entity rozdelili podľa ohraničených kontextov: Zákaznícka objednávka a ShippableOrder.

Najprv si vytvorme triedu, ktorá bude predstavovať celý pretrvávajúci model:

verejná statická trieda PersistenceOrder {public int orderId; verejný reťazec PaymentMethod; verejná adresa reťazca; verejný zoznam položiek objednávky; verejná statická trieda OrderItem {public int productId; verejná plaváková jednotkaCena; public float itemWeight; public int množstvo; }}

Vidíme, že táto trieda obsahuje všetky polia z oboch Zákaznícka objednávka a ShippableOrder subjekty.

Aby sme to zjednodušili, simulujme databázu v pamäti:

verejná trieda InMemoryOrderStore implementuje CustomerOrderRepository, ShippingOrderRepository {private Map commandsDb = new HashMap (); @Override public void saveCustomerOrder (CustomerOrder order) {this.ordersDb.put (order.getOrderId (), new PersistenceOrder (order.getOrderId (), order.getPaymentMethod (), order.getAddress (), order .getOrderItems () .stream () .map (orderItem -> new PersistenceOrder.OrderItem (orderItem.getProductId (), orderItem.getQuantity (), orderItem.getUnitWeight (), orderItem.getUnitPrice ())) .collect (Collectors.toList ())));; } @Override public Optional findShippableOrder (int orderId) {if (! This.ordersDb.containsKey (orderId)) return Optional.empty (); PersistenceOrder orderRecord = this.ordersDb.get (orderId); vrátiť Optional.of (new ShippableOrder (orderRecord.orderId, orderRecord.orderItems .stream (). map (orderItem -> new PackageItem (orderItem.productId, orderItem.itemWeight, orderItem.quantity * orderItem.unitPrice)) .collect (Collectors. listovať()))); }}

Tu pretrvávame a načítame rôzne typy entít prevedením pretrvávajúcich modelov na alebo z vhodného typu.

Na záver si vytvoríme definíciu modulu:

modul com.baeldung.dddmodules.infrastructure {vyžaduje tranzitívny com.baeldung.dddmodules.sharedkernel; vyžaduje tranzitívny com.baeldung.dddmodules.ordercontext; vyžaduje tranzitívny com.baeldung.dddmodules.shippingcontext; poskytuje com.baeldung.dddmodules.sharedkernel.events.EventBus s com.baeldung.dddmodules.infrastructure.events.SimpleEventBus; poskytuje com.baeldung.dddmodules.ordercontext.repository.CustomerOrderRepository s com.baeldung.dddmodules.infrastructure.db.InMemoryOrderStore; poskytuje com.baeldung.dddmodules.shippingcontext.repository.ShippingOrderRepository s com.baeldung.dddmodules.infrastructure.db.InMemoryOrderStore; }

Pomocou poskytuje s klauzula, poskytujeme implementáciu niekoľkých rozhraní, ktoré boli definované v iných moduloch.

Ďalej tento modul funguje ako agregátor závislostí, takže používame vyžaduje tranzitívne kľúčové slovo. Výsledkom je, že modul, ktorý vyžaduje modul Infraštruktúra, prechodne získa všetky tieto závislosti.

4.5. Hlavný modul

Na záver definujme modul, ktorý bude vstupným bodom do našej aplikácie:

modul com.baeldung.dddmodules.mainapp {používa com.baeldung.dddmodules.sharedkernel.events.EventBus; používa com.baeldung.dddmodules.ordercontext.service.OrderService; používa com.baeldung.dddmodules.ordercontext.repository.CustomerOrderRepository; používa com.baeldung.dddmodules.shippingcontext.repository.ShippingOrderRepository; používa com.baeldung.dddmodules.shippingcontext.service.ShippingService; vyžaduje tranzitívnu kom.baeldung.dddmodules.infraštruktúru; }

Pretože sme práve nastavili prechodné závislosti na module Infrastructure, nemusíme ich tu výslovne vyžadovať.

Na druhej strane uvádzame tieto závislosti pomocou používa kľúčové slovo. The používa klauzula poučí ServiceLoader, o ktorom v nasledujúcej kapitole zistíme, že tento modul chce tieto rozhrania používať. Avšak nevyžaduje, aby boli implementácie dostupné počas kompilácie.

5. Spustenie aplikácie

Nakoniec sme takmer pripravení na zostavenie našej aplikácie. Využijeme Maven na vytvorenie nášho projektu. To výrazne uľahčuje prácu s modulmi.

5.1. Štruktúra projektu

Náš projekt obsahuje päť modulov a nadradený modul. Pozrime sa na našu štruktúru projektu:

ddd-modules (koreňový adresár) pom.xml | - infraštruktúra | - src | - hlavný | - java module-info.java | - com.baeldung.dddmodules.infrastructure pom.xml | - mainapp | - src | - main | - java module-info.java | - com.baeldung.dddmodules.mainapp pom.xml | - ordercontext | - src | - hlavný | - java module-info.java | --com.baeldung.dddmodules.ordercontext pom.xml | - sharedkernel | - src | - hlavný | - java module-info.java | - com.baeldung.dddmodules.sharedkernel pom.xml | - shippingcontext | - src | - hlavný | - java modul-info.java | - com.baeldung.dddmodules.shippingcontext pom.xml

5.2. Hlavná aplikácia

Teraz už máme všetko okrem hlavnej aplikácie, takže si zadefinujme našu hlavný metóda:

public static void main (String args []) {Mapa container = createContainer (); OrderService orderService = (OrderService) container.get (OrderService.class); ShippingService shippingService = (ShippingService) container.get (ShippingService.class); shippingService.listenToOrderEvents (); CustomerOrder customerOrder = nový CustomerOrder (); int orderId = 1; customerOrder.setOrderId (orderId); Zoznam orderItems = nový ArrayList (); orderItems.add (nový OrderItem (1, 2, 3, 1)); orderItems.add (nový OrderItem (2, 1, 1, 1)); orderItems.add (nový OrderItem (3, 4, 11, 21)); customerOrder.setOrderItems (orderItems); customerOrder.setPaymentMethod ("PayPal"); customerOrder.setAddress ("Celá adresa tu"); orderService.placeOrder (customerOrder); if (orderId == shippingService.getParcelByOrderId (orderId) .get (). getOrderId ()) {System.out.println ("Objednávka bola úspešne spracovaná a odoslaná"); }}

Poďme v krátkosti prediskutovať našu hlavnú metódu. V tejto metóde simulujeme jednoduchý tok objednávok zákazníkov pomocou predtým definovaných služieb. Spočiatku sme vytvorili objednávku s tromi položkami a poskytli sme potrebné informácie o doprave a platbe. Ďalej sme zadali objednávku a nakoniec skontrolovali, či bola odoslaná a úspešne spracovaná.

Ako sme však dostali všetky závislosti a prečo createContainer návrat metódy Mapa<> Objekt>? Pozrime sa podrobnejšie na túto metódu.

5.3. Vkladanie závislostí pomocou ServiceLoader

V tomto projekte nemáme žiadne jarné IoC závislosti, takže alternatívne použijeme ServiceLoader API na objavovanie implementácií služieb. Toto nie je nová funkcia - ServiceLoader Samotné API existuje už od Javy 6.

Inštanciu načítavača môžeme získať vyvolaním jednej zo statických naložiť metódy ServiceLoader trieda. The naložiť metóda vracia Iterable typ, aby sme mohli iterovať nad objavenými implementáciami.

Teraz použijeme načítač na vyriešenie našich závislostí:

verejná statická mapa createContainer () {EventBus eventBus = ServiceLoader.load (EventBus.class) .findFirst (). get (); CustomerOrderRepository customerOrderRepository = ServiceLoader.load (CustomerOrderRepository.class) .findFirst (). Get (); ShippingOrderRepository shippingOrderRepository = ServiceLoader.load (ShippingOrderRepository.class) .findFirst (). Get (); ShippingService shippingService = ServiceLoader.load (ShippingService.class) .findFirst (). Get (); shippingService.setEventBus (eventBus); shippingService.setOrderRepository (shippingOrderRepository); OrderService orderService = ServiceLoader.load (OrderService.class) .findFirst (). Get (); orderService.setEventBus (eventBus); orderService.setOrderRepository (customerOrderRepository); HashMap kontajner = nový HashMap (); container.put (OrderService.class, orderService); container.put (ShippingService.class, shippingService); spätný kontajner; }

Tu, voláme statický naložiť metóda pre každé rozhranie, ktoré potrebujeme, a ktorá zakaždým vytvorí novú inštanciu zavádzača. Vo výsledku nebude ukladať do medzipamäte už vyriešené závislosti - namiesto toho zakaždým vytvorí nové inštancie.

Inštancie služieb možno vo všeobecnosti vytvoriť jedným z dvoch spôsobov. Buď trieda implementácie služby musí mať verejný konštruktor no-arg, alebo musí používať statický poskytovateľ metóda.

V dôsledku toho má väčšina našich služieb konštruktéry no-arg a metódy setter pre závislosti. Ale, ako sme už videli, InMemoryOrderStore trieda implementuje dve rozhrania: CustomerOrderRepository a ShippingOrderRepository.

Ak však požadujeme každé z týchto rozhraní pomocou naložiť metóda, dostaneme rôzne inštancie InMemoryOrderStore. To nie je žiaduce správanie, takže použijeme poskytovateľ metóda na uloženie inštancie do medzipamäte:

public class InMemoryOrderStore implementuje CustomerOrderRepository, ShippingOrderRepository {private volatile static InMemoryOrderStore instance = new InMemoryOrderStore (); public static InMemoryOrderStore provider () {návratová inštancia; }}

Použili sme vzor Singleton na medzipamäť jednej inštancie súboru InMemoryOrderStore triedy a vrátiť ju z poskytovateľ metóda.

Ak poskytovateľ služieb deklaruje a poskytovateľ metóda, potom ServiceLoader vyvolá túto metódu na získanie inštancie služby. V opačnom prípade sa pokúsi vytvoriť inštanciu pomocou konštruktora bez argumentov pomocou funkcie Reflection. Vo výsledku môžeme zmeniť mechanizmus poskytovateľa služieb bez toho, aby sme ovplyvnili náš createContainer metóda.

A nakoniec poskytujeme vyriešené závislosti službám prostredníctvom nastavovačov a vrátime nakonfigurované služby.

Nakoniec môžeme aplikáciu spustiť.

6. Záver

V tomto článku sme diskutovali o niekoľkých kritických konceptoch DDD: ohraničený kontext, všadeprítomný jazyk a mapovanie kontextu. Aj keď rozdelenie systému na ohraničené kontexty má veľa výhod, zároveň nie je potrebné tento prístup uplatňovať všade.

Ďalej sme videli, ako používať Java 9 Module System spolu s Bounded Context na vytváranie silne zapuzdrených modulov.

Ďalej sme pokryli predvolené nastavenie ServiceLoader mechanizmus na zisťovanie závislostí.

Celý zdrojový kód projektu je k dispozícii na GitHub.


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