DDD ograničeni konteksti i Java moduli

1. Pregled

Domain-Driven Design (DDD) skup je načela i alata koji nam pomažu u dizajniranju učinkovitih softverskih arhitektura za postizanje veće poslovne vrijednosti. Ograničeni kontekst jedan je od središnjih i bitnih uzoraka za spašavanje arhitekture iz Velikog blata blatom razdvajanjem cijele domene aplikacije na više semantički konzistentnih dijelova.

Istodobno, s Java 9 Module Systemom možemo stvoriti jako zatvorene module.

U ovom uputstvu stvorit ćemo jednostavnu aplikaciju za pohranu i vidjeti kako iskoristiti Java 9 module, definirajući eksplicitne granice za ograničeni kontekst.

2. DDD ograničeni konteksti

U današnje vrijeme softverski sustavi nisu jednostavne CRUD aplikacije. Zapravo, tipični monolitni sustav poduzeća sastoji se od neke naslijeđene baze koda i novo dodanih značajki. Međutim, postaje sve teže održavati takve sustave sa svakom promjenom. Na kraju, to može postati potpuno neodrživo.

2.1. Ograničeni kontekst i sveprisutni jezik

Da bi riješio adresirani problem, DDD pruža koncept Ograničeni kontekst. Ograničeni kontekst logična je granica domene gdje se određeni pojmovi i pravila dosljedno primjenjuju. Unutar ove granice, svi pojmovi, definicije i koncepti čine sveprisutni jezik.

Posebno je glavna korist sveprisutnog jezika grupiranje članova projekta iz različitih područja oko određene poslovne domene.

Uz to, više konteksta može raditi s istom stvari. Međutim, ono može imati različita značenja unutar svakog od ovih konteksta.

2.2. Kontekst narudžbe

Počnimo s implementacijom naše aplikacije definiranjem konteksta narudžbe. Ovaj kontekst sadrži dva entiteta: Artikal narudžbe i Nalog kupca.

The Nalog kupca entitet je skupni korijen:

javna klasa CustomerOrder {private int orderId; private String PaymentMethod; privatna string adresa; privatni popis orderItems; javni plutajući izračunatiTotalPrice () {vratiti orderItems.stream (). map (OrderItem :: getTotalPrice) .reduce (0F, Float :: sum); }}

Kao što vidimo, ova klasa sadrži izračunajTotalPrice metoda poslovanja. No, u stvarnom će projektu vjerojatno biti puno složenije - na primjer, uključujući popuste i poreze u konačnoj cijeni.

Dalje, kreirajmo Artikl narudžbe razred:

javna klasa OrderItem {private int productId; privatna int količina; privatna plutajuća jedinicaPrice; privatna plutajuća jedinicaWeight; }

Definirali smo entitete, ali također moramo neke API-je izložiti drugim dijelovima aplikacije. Stvorimo CustomerOrderService razred:

javna klasa CustomerOrderService implementira OrderService {public static final String EVENT_ORDER_READY_FOR_SHIPMENT = "OrderReadyForShipmentEvent"; privatni CustomerOrderRepository orderRepository; privatni EventBus eventBus; @Override public void placeOrder (narudžba kupca) {this.orderRepository.saveCustomerOrder (narudžba); Korisni teret karte = novi HashMap (); payload.put ("order_id", String.valueOf (order.getOrderId ())); ApplicationEvent događaj = novi ApplicationEvent (korisni teret) {@Override public String getType () {return EVENT_ORDER_READY_FOR_SHIPMENT; }}; this.eventBus.publish (događaj); }}

Ovdje moramo istaknuti nekoliko važnih točaka. The naručiti metoda odgovorna je za obradu narudžbi kupaca. Nakon obrade narudžbe, događaj se objavljuje na EventBus. O komunikaciji vođenoj događajima razgovarat ćemo u sljedećim poglavljima. Ova usluga pruža zadanu implementaciju za OrderService sučelje:

javno sučelje OrderService proširuje ApplicationService {void placeOrder (narudžba CustomerOrder); nevažeće setOrderRepository (CustomerOrderRepository orderRepository); }

Nadalje, ova usluga zahtijeva CustomerOrderRepository ustrajati u naredbama:

javno sučelje CustomerOrderRepository {void saveCustomerOrder (narudžba CustomerOrder); }

Ono što je bitno je to ovo sučelje nije implementirano unutar ovog konteksta već će ga osigurati Infrastrukturni modul, kao što ćemo vidjeti kasnije.

2.3. Kontekst otpreme

Sada, definirajmo kontekst otpreme. Također će biti jednostavan i sadržavat će tri cjeline: Parcela, PackageItem, i Nalog za isporuku.

Počnimo s Nalog za isporuku entitet:

javna klasa ShippableOrder {private int orderId; privatna string adresa; privatni popis packageItems; }

U ovom slučaju, entitet ne sadrži način plaćanja polje. To je zato što nas u našem kontekstu dostave ne zanima koji se način plaćanja koristi. Kontekst otpreme samo je odgovoran za obradu pošiljaka narudžbi.

Također, Parcela entitet specifičan je za kontekst otpreme:

javna klasa Parcela {private int orderId; privatna string adresa; private String trackingId; privatni popis packageItems; javni plutajući izračunTotalWeight () {return packageItems.stream (). map (PackageItem :: getWeight) .reduce (0F, Float :: sum); } public boolean isTaxable () {return calcuEstimatedValue ()> 100; } javni plutajući izračunEstimatedValue () {return packageItems.stream (). map (PackageItem :: getWeight) .reduce (0F, Float :: sum); }}

Kao što vidimo, sadrži i specifične poslovne metode i djeluje kao skupni korijen.

Napokon, definirajmo ParcelShippingService:

javna klasa ParcelShippingService implementira ShippingService {public static final String EVENT_ORDER_READY_FOR_SHIPMENT = "OrderReadyForShipmentEvent"; privatni ShippingOrderRepository orderRepository; privatni EventBus eventBus; privatna karta shippedParcels = novi HashMap (); @Override public void shipOrder (int orderId) {Neobvezna narudžba = this.orderRepository.findShippableOrder (orderId); order.ifPresent (completeOrder -> {Parcela parcela = novi Parcel (completeOrder.getOrderId (), completeOrder.getAddress (), completeOrder.getPackageItems ()); if (parcel.isTaxable ()) {// Izračunajte dodatne poreze} // Pošaljite parcelu this.shippedParcels.put (CompleteOrder.getOrderId (), parcela);}); } @Override public void listenToOrderEvents () {this.eventBus.subscribe (EVENT_ORDER_READY_FOR_SHIPMENT, new EventSubscriber () {@Override public void onEvent (E event) {shipOrder (Integer.parseInt (event.getPayloadVal) }); } @Override public Izborni getParcelByOrderId (int orderId) {return Optional.ofNullable (this.shippedParcels.get (orderId)); }}

Ova usluga slično koristi ShippingOrderRepository za dohvaćanje narudžbi po id. Još važnije, pretplaćuje se na OrderReadyForShipmentEvent događaj, koji je objavljen u drugom kontekstu. Kada se dogodi ovaj događaj, usluga primjenjuje neka pravila i šalje narudžbu. Radi jednostavnosti, isporučene narudžbe pohranjujemo u HashMap.

3. Kontekstne karte

Do sada smo definirali dva konteksta. Međutim, nismo postavili nikakve eksplicitne odnose među njima. U tu svrhu DDD ima koncept Mapiranje konteksta. Karta konteksta vizualni je opis odnosa između različitih konteksta sustava. Ova karta pokazuje kako različiti dijelovi zajedno koegzistiraju da bi stvorili domenu.

Pet je glavnih vrsta odnosa između ograničenih konteksta:

  • Partnerstvo - odnos između dva konteksta koji surađuju kako bi se dvije momčadi uskladile s ovisnim ciljevima
  • Dijeljena jezgra - vrsta odnosa kada se zajednički dijelovi nekoliko konteksta izdvoje u drugi kontekst / modul radi smanjenja dupliciranja koda
  • Kupac-dobavljač - veza između dva konteksta, gdje jedan kontekst (uzvodno) stvara podatke, a drugi (nizvodno) ih troši. U ovom su odnosu obje strane zainteresirane za uspostavljanje najbolje moguće komunikacije
  • Konformist - ovaj odnos također ima uzvodno i nizvodno, međutim, nizvodno se uvijek podudara s API-jevima uzlaznog toka
  • Antikorupcijski sloj - ova vrsta odnosa široko se koristi za naslijeđene sustave kako bi ih prilagodili novoj arhitekturi i postupno migrirali iz naslijeđene baze koda. Sloj za borbu protiv korupcije djeluje kao adapter za prevođenje podataka s gornje strane i zaštitu od neželjenih promjena

U našem konkretnom primjeru koristit ćemo vezu Dijeljena jezgra. Nećemo ga definirati u čistom obliku, ali uglavnom će djelovati kao posrednik događaja u sustavu.

Dakle, modul SharedKernel neće sadržavati nikakve konkretne implementacije, već samo sučelja.

Počnimo s EventBus sučelje:

javno sučelje EventBus {void objavljivanje (E događaj); nevažeća pretplata (String eventType, pretplatnik EventSubscriber); poništenje pretplate (String eventType, pretplatnik EventSubscriber); }

Ovo sučelje bit će implementirano kasnije u našem modulu Infrastruktura.

Dalje kreiramo sučelje osnovne usluge sa zadanim metodama za podršku komunikaciji vođenoj događajima:

javno sučelje ApplicationService {zadana void objavitiEvent (E događaj) {EventBus eventBus = getEventBus (); if (eventBus! = null) {eventBus.publish (događaj); }} zadana void pretplata (String eventType, pretplatnik EventSubscriber) {EventBus eventBus = getEventBus (); if (eventBus! = null) {eventBus.subscribe (eventType, pretplatnik); }} zadana void odjava (String eventType, pretplatnik EventSubscriber) {EventBus eventBus = getEventBus (); if (eventBus! = null) {eventBus.unsubscribe (eventType, pretplatnik); }} EventBus getEventBus (); void setEventBus (EventBus eventBus); }

Dakle, servisna sučelja u ograničenom kontekstu proširuju ovo sučelje tako da imaju zajedničku funkcionalnost povezanu s događajima.

4. Java 9 Modularnost

Sada je vrijeme da istražimo kako Java 9 Module System može podržati definiranu strukturu aplikacije.

Sustav modula Java Platform (JPMS) potiče na izgradnju pouzdanijih i snažnije inkapsuliranih modula. Kao rezultat, ove značajke mogu pomoći u izoliranju našeg konteksta i uspostavljanju jasnih granica.

Pogledajmo naš konačni dijagram modula:

4.1. Modul SharedKernel

Počnimo s modulom SharedKernel, koji nema nikakve ovisnosti o drugim modulima. Dakle, module-info.java izgleda kao:

modul com.baeldung.dddmodules.sharedkernel {izvozi com.baeldung.dddmodules.sharedkernel.events; izvozi com.baeldung.dddmodules.sharedkernel.service; }

Izvozimo sučelja modula, tako da su dostupna ostalim modulima.

4.2. Kontekst narudžbe Modul

Dalje, preusmjerimo fokus na modul OrderContext. Potrebna su samo sučelja definirana u modulu SharedKernel:

modul com.baeldung.dddmodules.ordercontext {zahtijeva com.baeldung.dddmodules.sharedkernel; izvozi com.baeldung.dddmodules.ordercontext.service; izvozi com.baeldung.dddmodules.ordercontext.model; izvozi com.baeldung.dddmodules.ordercontext.repository; pruža com.baeldung.dddmodules.ordercontext.service.OrderService s com.baeldung.dddmodules.ordercontext.service.CustomerOrderService; }

Također, možemo vidjeti da ovaj modul izvozi zadanu implementaciju za OrderService sučelje.

4.3. Kontekst otpreme Modul

Slično prethodnom modulu, kreirajmo datoteku definicije modula ShippingContext:

modul com.baeldung.dddmodules.shippingcontext {zahtijeva com.baeldung.dddmodules.sharedkernel; izvozi com.baeldung.dddmodules.shippingcontext.service; izvozi com.baeldung.dddmodules.shippingcontext.model; izvozi com.baeldung.dddmodules.shippingcontext.repository; pruža com.baeldung.dddmodules.shippingcontext.service.ShippingService s com.baeldung.dddmodules.shippingcontext.service.ParcelShippingService; }

Na isti način izvozimo zadanu implementaciju za Usluga dostave sučelje.

4.4. Infrastrukturni modul

Sada je vrijeme da opišemo modul Infrastruktura. Ovaj modul sadrži detalje o implementaciji definiranih sučelja. Počet ćemo izradom jednostavne implementacije za EventBus sučelje:

javna klasa SimpleEventBus implementira EventBus {privatna konačna karta pretplatnici = novi ConcurrentHashMap (); @Override public voidobjavite (E event) {if (subscribers.containsKey (event.getType ())) {subscribers.get (event.getType ()) .forEach (pretplatnik -> pretplatnik.onEvent (događaj)); }} @Override javna void pretplata (String eventType, pretplatnik EventSubscriber) {Set eventSubscribers = subscribers.get (eventType); if (eventSubscribers == null) {eventSubscribers = new CopyOnWriteArraySet (); pretplatnici.put (eventType, eventSubscribers); } eventSubscribers.add (pretplatnik); } @Override javna void odjava (String eventType, EventSubscriber pretplatnik) {if (subscribers.containsKey (eventType)) {subscribers.get (eventType) .remove (pretplatnik); }}}

Dalje, moramo implementirati CustomerOrderRepository i ShippingOrderRepository sučelja. U većini slučajeva Narudžba entitet će biti pohranjen u istoj tablici, ali će se koristiti kao drugi model entiteta u ograničenom kontekstu.

Vrlo je često vidjeti jedan entitet koji sadrži miješani kôd iz različitih područja poslovne domene ili mapiranja baze podataka na niskoj razini. Za našu implementaciju podijelili smo entitete prema ograničenom kontekstu: Nalog kupca i Nalog za isporuku.

Prvo, izradimo klasu koja će predstavljati čitav trajni model:

javna statička klasa PersistenceOrder {public int orderId; public String PaymentMethod; javna gudačka adresa; javni popis orderItems; javna statička klasa OrderItem {public int productId; javna float jedinicaPrice; javni plutajući itemWeight; javna int količina; }}

Vidimo da ova klasa sadrži sva polja iz oba Nalog kupca i Nalog za isporuku entiteta.

Da stvari budu jednostavne, simulirajmo bazu podataka u memoriji:

javna klasa InMemoryOrderStore implementira CustomerOrderRepository, ShippingOrderRepository {private Map ordersDb = new HashMap (); @Override javna void saveCustomerOrder (narudžba kupca) {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 Neobvezna findShippableOrder (int orderId) {if (! This.ordersDb.containsKey (orderId)) return Optional.empty (); PersistenceOrder orderRecord = this.ordersDb.get (orderId); return Optional.of (new ShippableOrder (orderRecord.orderId, orderRecord.orderItems .stream (). map (orderItem -> new PackageItem (orderItem.productId, orderItem.itemWeight, orderItem.quantity * orderItem.unitPrice)). Collect (Collectors. izlistati()))); }}

Ovdje istrajavamo i dohvaćamo različite vrste entiteta pretvarajući trajne modele u odgovarajući tip ili iz njega.

Na kraju, kreirajmo definiciju modula:

modul com.baeldung.dddmodules.infrastructure {zahtijeva prijelazni com.baeldung.dddmodules.sharedkernel; zahtijeva prijelazni com.baeldung.dddmodules.ordercontext; zahtijeva prijelazni com.baeldung.dddmodules.shippingcontext; pruža com.baeldung.dddmodules.sharedkernel.events.EventBus s com.baeldung.dddmodules.infrastructure.events.SimpleEventBus; pruža com.baeldung.dddmodules.ordercontext.repository.CustomerOrderRepository s com.baeldung.dddmodules.infrastructure.db.InMemoryOrderStore; pruža com.baeldung.dddmodules.shippingcontext.repository.ShippingOrderRepository s com.baeldung.dddmodules.infrastructure.db.InMemoryOrderStore; }

Koristiti pruža sa klauzula, pružamo implementaciju nekoliko sučelja koja su definirana u drugim modulima.

Nadalje, ovaj modul djeluje kao agregator ovisnosti, pa koristimo zahtijeva prijelazno ključna riječ. Kao rezultat, modul koji zahtijeva modul Infrastruktura tranzitivno će dobiti sve ove ovisnosti.

4.5. Glavni modul

Za kraj, definirajmo modul koji će biti ulazna točka naše aplikacije:

modul com.baeldung.dddmodules.mainapp {koristi com.baeldung.dddmodules.sharedkernel.events.EventBus; koristi com.baeldung.dddmodules.ordercontext.service.OrderService; koristi com.baeldung.dddmodules.ordercontext.repository.CustomerOrderRepository; koristi com.baeldung.dddmodules.shippingcontext.repository.ShippingOrderRepository; koristi com.baeldung.dddmodules.shippingcontext.service.ShippingService; zahtijeva prijelaznu com.baeldung.dddmodules.infrastructure; }

Kako smo upravo postavili prijelazne ovisnosti na modulu Infrastruktura, ovdje ih ne trebamo izričito zahtijevati.

S druge strane, ove ovisnosti navodimo pomoću koristi ključna riječ. The koristi klauzula upućuje ServiceLoader, što ćemo otkriti u sljedećem poglavlju, da ovaj modul želi koristiti ova sučelja. Međutim, ne zahtijeva da implementacije budu dostupne tijekom vremena kompajliranja.

5. Pokretanje aplikacije

Napokon, gotovo smo spremni za izradu naše aplikacije. Iskoristit ćemo Maven za izgradnju našeg projekta. To znatno olakšava rad s modulima.

5.1. Struktura projekta

Naš projekt sadrži pet modula i roditeljski modul. Pogledajmo strukturu našeg projekta:

ddd-moduli (korijenski direktorij) pom.xml | - infrastruktura | - src | - glavna | - java module-info.java | - com.baeldung.dddmodules.infrastruktura pom.xml | - mainapp | - src | - main | - java module-info.java | - com.baeldung.dddmodules.mainapp pom.xml | - ordercontext | - src | - main | - java module-info.java | --com.baeldung.dddmodules.ordercontext pom.xml | - sharedkernel | - src | - main | - java module-info.java | - com.baeldung.dddmodules.sharedkernel pom.xml | - shippingcontext | - src | - main | - java module-info.java | - com.baeldung.dddmodules.shippingcontext pom.xml

5.2. Glavna primjena

Do sada imamo sve osim glavne aplikacije, pa definirajmo našu glavni metoda:

public static void main (String args []) {Map spremnik = createContainer (); OrderService orderService = (OrderService) container.get (OrderService.class); ShippingService shippingService = (ShippingService) container.get (ShippingService.class); shippingService.listenToOrderEvents (); CustomerOrder customerOrder = novi CustomerOrder (); int orderId = 1; customerOrder.setOrderId (orderId); Lista orderItems = novi ArrayList (); orderItems.add (novi OrderItem (1, 2, 3, 1)); orderItems.add (novi OrderItem (2, 1, 1, 1)); orderItems.add (novi OrderItem (3, 4, 11, 21)); customerOrder.setOrderItems (orderItems); customerOrder.setPaymentMethod ("PayPal"); customerOrder.setAddress ("Puna adresa ovdje"); orderService.placeOrder (customerOrder); if (orderId == shippingService.getParcelByOrderId (orderId) .get (). getOrderId ()) {System.out.println ("Narudžba je uspješno obrađena i isporučena"); }}

Razmotrimo ukratko našu glavnu metodu. Ovom metodom simuliramo jednostavan tijek narudžbi kupaca pomoću prethodno definiranih usluga. Isprva smo stvorili narudžbu s tri predmeta i pružili potrebne podatke o otpremi i plaćanju. Zatim smo predali narudžbu i konačno provjerili je li otpremljena i uspješno obrađena.

Ali kako smo dobili sve ovisnosti i zašto createContainer povratak metode Karta<> Objekt>? Pogledajmo pobliže ovu metodu.

5.3. Injekcija ovisnosti pomoću ServiceLoader-a

U ovom projektu nemamo ovisnosti o proljetnom IoC-u, pa ćemo alternativno koristiti ServiceLoader API za otkrivanje implementacija usluga. Ovo nije nova značajka - ServiceLoader Sam API postoji od Jave 6.

Primjer učitavača možemo dobiti pozivajući se na jedan od statičkih elemenata opterećenje metode ServiceLoader razred. The opterećenje metoda vraća Iterativ tipa kako bismo mogli prelaziti preko otkrivenih implementacija.

Sada, primijenimo loader za rješavanje naših ovisnosti:

javna statička karta 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 spremnik = novi HashMap (); container.put (OrderService.class, orderService); container.put (ShippingService.class, shippingService); povratni spremnik; }

Ovdje, zovemo statički opterećenje metoda za svako sučelje koje nam treba, što svaki put stvara novu instancu učitavača. Kao rezultat, neće predmemorirati već riješene ovisnosti - umjesto toga, svaki put će stvoriti nove instance.

Općenito, instance usluge mogu se stvoriti na jedan od dva načina. Ili klasa implementacije usluge mora imati javni no-arg konstruktor, ili mora koristiti statički davatelja usluga metoda.

Kao posljedica toga, većina naših usluga ima ne-arg konstruktore i metode postavljanja za ovisnosti. Ali, kao što smo već vidjeli, InMemoryOrderStore klasa implementira dva sučelja: CustomerOrderRepository i ShippingOrderRepository.

Međutim, ako zatražimo svako od ovih sučelja pomoću opterećenje metodu, dobit ćemo različite instance InMemoryOrderStore. To nije poželjno ponašanje, pa iskoristimo davatelja usluga metoda tehnike za predmemoriranje instance:

javna klasa InMemoryOrderStore implementira CustomerOrderRepository, ShippingOrderRepository {private volatile static InMemoryOrderStore instance = new InMemoryOrderStore (); javni statički dobavljač InMemoryOrderStore () {instanca return; }}

Primijenili smo uzorak Singleton za predmemoriranje jedne instance datoteke InMemoryOrderStore klase i vratite je iz davatelja usluga metoda.

Ako davatelj usluga izjavi a davatelja usluga metoda, zatim ServiceLoader poziva ovu metodu za dobivanje instance usluge. U suprotnom, pokušat će stvoriti instancu pomoću konstruktora no-argument putem Reflection. Kao rezultat toga, možemo promijeniti mehanizam pružatelja usluga bez utjecaja na naš createContainer metoda.

I na kraju, pružamo riješene ovisnosti uslugama putem postavljača i vraćamo konfigurirane usluge.

Napokon, možemo pokrenuti aplikaciju.

6. Zaključak

U ovom smo članku razgovarali o nekim kritičnim DDD konceptima: Ograničeni kontekst, Sveprisutni jezik i Mapiranje konteksta. Iako podjela sustava na ograničeni kontekst ima puno prednosti, istodobno nema potrebe svugdje primjenjivati ​​ovaj pristup.

Dalje, vidjeli smo kako koristiti Java 9 Module System zajedno s Bounded Contextom za stvaranje snažno inkapsuliranih modula.

Nadalje, pokrili smo zadano ServiceLoader mehanizam za otkrivanje ovisnosti.

Potpuni izvorni kod projekta dostupan je na GitHubu.