DDD-avgrensede kontekster og Java-moduler

1. Oversikt

Domain-Driven Design (DDD) er et sett med prinsipper og verktøy som hjelper oss med å designe effektive programvarearkitekturer for å levere høyere forretningsverdi. Bounded Context er et av de sentrale og viktige mønstrene for å redde arkitektur fra Big Ball Of Mud ved å adskille hele applikasjonsdomenet i flere semantisk konsistente deler.

Samtidig, med Java 9 Module System, kan vi lage sterkt innkapslede moduler.

I denne opplæringen oppretter vi en enkel butikkapplikasjon og ser hvordan du kan utnytte Java 9-moduler mens vi definerer eksplisitte grenser for avgrensede sammenhenger.

2. DDD-avgrensede kontekster

I dag er programvaresystemer ikke enkle CRUD-applikasjoner. Egentlig består det typiske monolitiske enterprise-systemet av noen eldre kodebaser og nylig lagt til funksjoner. Imidlertid blir det vanskeligere og vanskeligere å vedlikeholde slike systemer med hver endring. Til slutt kan det bli fullstendig uvedlikeholdelig.

2.1. Begrenset kontekst og allestedsnærværende språk

For å løse det adresserte problemet, gir DDD konseptet Bounded Context. En avgrenset kontekst er en logisk grense for et domene der bestemte vilkår og regler gjelder konsekvent. Inne i denne grensen, alle termer, definisjoner og begreper danner det allestedsnærværende språket.

Spesielt er den største fordelen med allestedsnærværende språk å gruppere prosjektmedlemmer fra forskjellige områder rundt et bestemt forretningsdomene.

I tillegg kan flere sammenhenger fungere med det samme. Imidlertid kan det ha forskjellige betydninger i hver av disse sammenhengene.

2.2. Bestill kontekst

La oss begynne å implementere søknaden vår ved å definere ordrekonteksten. Denne konteksten inneholder to enheter: Bestillingsvare og Kundebestilling.

De Kundebestilling enhet er en samlet rot:

offentlig klasse kundeordre {private int orderId; privat streng betalingMetode; privat strengadresse; private listeordreelementer; public float calcTotalPrice () {return orderItems.stream (). map (OrderItem :: getTotalPrice) .reduce (0F, Float :: sum); }}

Som vi kan se, inneholder denne klassen calcTotalPrice forretningsmetode. Men i et virkelig prosjekt vil det sannsynligvis være mye mer komplisert - for eksempel inkludert rabatter og avgifter i den endelige prisen.

La oss deretter lage Bestillingsvare klasse:

offentlig klasse OrderItem {private int productId; privat int mengde; privat flyte enhet Pris; privat flottør enhet Vekt; }

Vi har definert enheter, men vi må også eksponere noe API for andre deler av applikasjonen. La oss lage CustomerOrderService klasse:

public class CustomerOrderService implementerer OrderService {public static final String EVENT_ORDER_READY_FOR_SHIPMENT = "OrderReadyForShipmentEvent"; privat kundeOrdre Depot ordre Depot; private EventBus eventBus; @Override public void placeOrder (CustomerOrder order) {this.orderRepository.saveCustomerOrder (order); Kartnyttelast = ny HashMap (); payload.put ("order_id", String.valueOf (order.getOrderId ())); ApplicationEvent-hendelse = ny ApplicationEvent (nyttelast) {@Override public String getType () {return EVENT_ORDER_READY_FOR_SHIPMENT; }}; this.eventBus.publish (event); }}

Her har vi noen viktige punkter å trekke frem. De Legg inn bestilling metoden er ansvarlig for behandling av kundeordrer. Etter at en ordre er behandlet, blir hendelsen publisert til EventBus. Vi vil diskutere den hendelsesdrevne kommunikasjonen i de neste kapitlene. Denne tjenesten gir standard implementering for OrderService grensesnitt:

offentlig grensesnitt OrderService utvider ApplicationService {void placeOrder (CustomerOrder order); ugyldig setOrderRepository (CustomerOrderRepository orderRepository); }

Videre krever denne tjenesten CustomerOrderRepository å vedvare bestillinger:

offentlig grensesnitt CustomerOrderRepository {void saveCustomerOrder (CustomerOrder order); }

Det som er viktig er det dette grensesnittet er ikke implementert i denne sammenhengen, men vil bli levert av infrastrukturmodulen, som vi får se senere.

2.3. Fraktkontekst

La oss nå definere Shipping Context. Det vil også være greit og inneholde tre enheter: Pakke, PackageItem, og Kan bestilles.

La oss starte med Kan bestilles enhet:

offentlig klasse ShippableOrder {private int orderId; privat strengadresse; privat listepakkeItems; }

I dette tilfellet inneholder ikke enheten betalingsmetode felt. Det er fordi vi i vår fraktsammenheng ikke bryr oss hvilken betalingsmåte som brukes. Shipping Context er bare ansvarlig for behandling av forsendelser av ordrer.

Også, den Pakke enhet er spesifikk for forsendelseskonteksten:

offentlig klasse Pakke {privat int orderId; privat strengadresse; privat streng trackingId; privat listepakkeItems; public float calcTotalWeight () {return packageItems.stream (). map (PackageItem :: getWeight) .reduce (0F, Float :: sum); } offentlig boolsk isTaxable () {retur calculateEstimatedValue ()> 100; } public float calculateEstimatedValue () {return packageItems.stream (). map (PackageItem :: getWeight) .reduce (0F, Float :: sum); }}

Som vi kan se, inneholder den også spesifikke forretningsmetoder og fungerer som en samlet rot.

Til slutt, la oss definere ParcelShippingService:

public class ParcelShippingService implementerer ShippingService {public static final String EVENT_ORDER_READY_FOR_SHIPMENT = "OrderReadyForShipmentEvent"; private ShippingOrderRepository orderRepository; private EventBus eventBus; private Map shippedParcels = nye HashMap (); @Override public void shipOrder (int orderId) {Optional order = this.orderRepository.findShippableOrder (orderId); order.ifPresent (completeOrder -> {Parcel parcel = new Parcel (completeOrder.getOrderId (), completedOrder.getAddress (), completedOrder.getPackageItems ()); if (parcel.isTaxable ()) {// Beregn tilleggsavgift} // Send pakke this.shippedParcels.put (completeOrder.getOrderId (), pakke);}); } @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 Valgfritt getParcelByOrderId (int orderId) {retur Optional.ofNullable (this.shippedParcels.get (orderId)); }}

Denne tjenesten bruker på samme måte ShippingOrderRepository for å hente ordrer etter id. Enda viktigere, det abonnerer på OrderReadyForShipmentEvent hendelse, som er publisert i en annen kontekst. Når denne hendelsen inntreffer, bruker tjenesten noen regler og sender bestillingen. For enkelhets skyld lagrer vi sendte ordrer i en HashMap.

3. Kontekstkart

Så langt har vi definert to sammenhenger. Vi satte imidlertid ikke noen eksplisitte forhold mellom dem. For dette formålet har DDD konseptet Context Mapping. Et kontekstkart er en visuell beskrivelse av forholdet mellom forskjellige sammenhenger i systemet. Dette kartet viser hvordan forskjellige deler eksisterer sammen for å danne domenet.

Det er fem hovedtyper av forhold mellom avgrensede sammenhenger:

  • Samarbeid - et forhold mellom to sammenhenger som samarbeider om å tilpasse de to lagene til avhengige mål
  • Delt kjerne - et slags forhold når vanlige deler av flere sammenhenger blir hentet ut til en annen kontekst / modul for å redusere duplisering av kode
  • Kunde-leverandør - en forbindelse mellom to sammenhenger, der den ene konteksten (oppstrøms) produserer data, og den andre (nedstrøms) forbruker den. I dette forholdet er begge sider interessert i å etablere en best mulig kommunikasjon
  • Konformist - dette forholdet har også oppstrøms og nedstrøms, men nedstrøms samsvarer alltid med oppstrøms APIer
  • Antikorrupsjonslag - denne typen forhold brukes mye for eldre systemer for å tilpasse dem til en ny arkitektur og gradvis migrere fra den eldre kodebasen. Anticorruption-laget fungerer som et adapter for å oversette data fra oppstrøms og beskytte mot uønskede endringer

I vårt spesielle eksempel bruker vi Shared Kernel-forholdet. Vi vil ikke definere det i sin rene form, men det vil for det meste fungere som en formidler av hendelser i systemet.

Dermed inneholder SharedKernel-modulen ingen konkrete implementeringer, bare grensesnitt.

La oss starte med EventBus grensesnitt:

offentlig grensesnitt EventBus {void publish (E event); ugyldig abonnement (String eventType, EventSubscriber subscriber); ugyldig avmelding (String eventType, EventSubscriber subscriber); }

Dette grensesnittet vil bli implementert senere i vår infrastrukturmodul.

Deretter lager vi et basetjenestegrensesnitt med standardmetoder for å støtte hendelsesdrevet kommunikasjon:

offentlig grensesnitt ApplicationService {standard ugyldig publishEvent (E hendelse) {EventBus eventBus = getEventBus (); hvis (eventBus! = null) {eventBus.publish (event); }} standard ugyldig abonnement (String eventType, EventSubscriber subscriber) {EventBus eventBus = getEventBus (); hvis (eventBus! = null) {eventBus.subscribe (eventType, abonnent); }} standard ugyldig avmelding (String eventType, EventSubscriber subscriber) {EventBus eventBus = getEventBus (); hvis (eventBus! = null) {eventBus.unsubscribe (eventType, abonnent); }} EventBus getEventBus (); ugyldig setEventBus (EventBus eventBus); }

Så tjenestegrensesnitt i avgrensede sammenhenger utvider dette grensesnittet til å ha vanlig hendelsesrelatert funksjonalitet.

4. Java 9-modularitet

Nå er det på tide å utforske hvordan Java 9 Module System kan støtte den definerte applikasjonsstrukturen.

Java Platform Module System (JPMS) oppfordrer til å bygge mer pålitelige og sterkt innkapslede moduler. Som et resultat kan disse funksjonene bidra til å isolere våre sammenhenger og etablere klare grenser.

La oss se vårt siste moduldiagram:

4.1. SharedKernel Module

La oss starte med SharedKernel-modulen, som ikke har noen avhengighet av andre moduler. Så module-info.java ser ut som:

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

Vi eksporterer modulgrensesnitt, slik at de er tilgjengelige for andre moduler.

4.2. OrderContext Modul

Deretter flytter vi fokuset til OrderContext-modulen. Det krever bare grensesnitt som er definert i SharedKernel-modulen:

modul com.baeldung.dddmodules.ordercontext {krever com.baeldung.dddmodules.sharedkernel; eksporterer com.baeldung.dddmodules.ordercontext.service; eksporterer com.baeldung.dddmodules.ordercontext.model; eksport com.baeldung.dddmodules.ordercontext.repository; gir com.baeldung.dddmodules.ordercontext.service.OrderService med com.baeldung.dddmodules.ordercontext.service.CustomerOrderService; }

Vi kan også se at denne modulen eksporterer standardimplementeringen for OrderService grensesnitt.

4.3. ShippingContext Modul

På samme måte som den forrige modulen, la oss lage definisjonsfilen for ShippingContext-modul:

modul com.baeldung.dddmodules.shippingcontext {krever com.baeldung.dddmodules.sharedkernel; eksporterer com.baeldung.dddmodules.shippingcontext.service; eksporterer com.baeldung.dddmodules.shippingcontext.model; eksport com.baeldung.dddmodules.shippingcontext.repository; gir com.baeldung.dddmodules.shippingcontext.service.ShippingService med com.baeldung.dddmodules.shippingcontext.service.ParcelShippingService; }

På samme måte eksporterer vi standardimplementeringen for ShippingService grensesnitt.

4.4. Infrastrukturmodul

Nå er det på tide å beskrive infrastrukturmodulen. Denne modulen inneholder implementeringsdetaljer for de definerte grensesnittene. Vi begynner med å lage en enkel implementering for EventBus grensesnitt:

offentlig klasse SimpleEventBus implementerer EventBus {private final Map abonnenter = ny ConcurrentHashMap (); @Override public void publish (E event) {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); hvis (eventSubscribers == null) {eventSubscribers = ny CopyOnWriteArraySet (); subscribers.put (eventType, eventSubscribers); } eventSubscribers.add (abonnent); } @ Override public void unsubscribe (String eventType, EventSubscriber subscriber) {if (subscribers.containsKey (eventType)) {subscribers.get (eventType) .remove (subscriber); }}}

Deretter må vi implementere CustomerOrderRepository og ShippingOrderRepository grensesnitt. I de fleste tilfeller er Rekkefølge enhet vil bli lagret i samme tabell, men brukes som en annen enhetsmodell i avgrensede sammenhenger.

Det er veldig vanlig å se en enhet som inneholder blandet kode fra forskjellige områder av virksomhetsdomenet eller databasekartlegginger på lavt nivå. For implementeringen har vi delt enhetene våre i henhold til de avgrensede kontekstene: Kundebestilling og Kan bestilles.

La oss først lage en klasse som vil representere en hel vedvarende modell:

offentlig statisk klasse PersistenceOrder {public int orderId; offentlig streng betalingMetode; offentlig strengadresse; offentlig listeordreItemer; offentlig statisk klasse OrderItem {public int productId; offentlig flyte enhet Pris; offentlig flytevare Vekt; offentlig int mengde; }}

Vi kan se at denne klassen inneholder alle felt fra begge Kundebestilling og Kan bestilles enheter.

For å gjøre ting enkelt, la oss simulere en database i minnet:

offentlig klasse InMemoryOrderStore implementerer CustomerOrderRepository, ShippingOrderRepository {private Map ordersDb = 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 Valgfritt findShippableOrder (int orderId) {if (! This.ordersDb.containsKey (orderId)) returner 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)). å liste opp()))); }}

Her vedvarer vi og henter forskjellige typer enheter ved å konvertere vedvarende modeller til eller fra en passende type.

Til slutt, la oss lage moduldefinisjonen:

modul com.baeldung.dddmodules.infrastructure {krever transitiv com.baeldung.dddmodules.sharedkernel; krever transitiv com.baeldung.dddmodules.ordercontext; krever transitiv com.baeldung.dddmodules.shippingcontext; gir com.baeldung.dddmodules.sharedkernel.events.EventBus med com.baeldung.dddmodules.infrastructure.events.SimpleEventBus; gir com.baeldung.dddmodules.ordercontext.repository.CustomerOrderRepository med com.baeldung.dddmodules.infrastructure.db.InMemoryOrderStore; gir com.baeldung.dddmodules.shippingcontext.repository.ShippingOrderRepository med com.baeldung.dddmodules.infrastructure.db.InMemoryOrderStore; }

Bruker gir med klausul, gir vi implementeringen av noen få grensesnitt som ble definert i andre moduler.

Videre fungerer denne modulen som en samler av avhengigheter, så vi bruker krever transitive nøkkelord. Som et resultat vil en modul som krever infrastrukturmodulen transitt få alle disse avhengighetene.

4.5. Hovedmodul

For å avslutte, la oss definere en modul som vil være inngangspunktet for søknaden vår:

modul com.baeldung.dddmodules.mainapp {bruker com.baeldung.dddmodules.sharedkernel.events.EventBus; bruker com.baeldung.dddmodules.ordercontext.service.OrderService; bruker com.baeldung.dddmodules.ordercontext.repository.CustomerOrderRepository; bruker com.baeldung.dddmodules.shippingcontext.repository.ShippingOrderRepository; bruker com.baeldung.dddmodules.shippingcontext.service.ShippingService; krever transitiv com.baeldung.dddmodules.infrastructure; }

Ettersom vi nettopp har satt transitive avhengigheter av infrastrukturmodulen, trenger vi ikke å kreve dem eksplisitt her.

På den annen side lister vi opp disse avhengighetene med bruker nøkkelord. De bruker klausul instruerer ServiceLoader, som vi vil oppdage i neste kapittel, at denne modulen ønsker å bruke disse grensesnittene. Derimot, det krever ikke at implementeringer er tilgjengelige under kompileringstiden.

5. Kjøre applikasjonen

Endelig er vi nesten klare til å bygge applikasjonen vår. Vi vil utnytte Maven for å bygge prosjektet vårt. Dette gjør det mye lettere å jobbe med moduler.

5.1. Prosjektstruktur

Prosjektet vårt inneholder fem moduler og foreldremodulen. La oss ta en titt på prosjektstrukturen vår:

ddd-moduler (rotkatalogen) pom.xml | - infrastruktur | - src | - hoved | - 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 | - 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. Hovedapplikasjon

Nå har vi alt unntatt hovedapplikasjonen, så la oss definere vår hoved- metode:

public static void main (String args []) {Map container = createContainer (); OrderService orderService = (OrderService) container.get (OrderService.class); ShippingService shippingService = (ShippingService) container.get (ShippingService.class); shippingService.listenToOrderEvents (); CustomerOrder customerOrder = ny kundeordre (); int orderId = 1; customerOrder.setOrderId (orderId); Liste orderItems = ny ArrayList (); orderItems.add (nytt OrderItem (1, 2, 3, 1)); orderItems.add (nytt OrderItem (2, 1, 1, 1)); orderItems.add (nytt OrderItem (3, 4, 11, 21)); customerOrder.setOrderItems (orderItems); customerOrder.setPaymentMethod ("PayPal"); customerOrder.setAddress ("Full adresse her"); orderService.placeOrder (kundeOrder); if (orderId == shippingService.getParcelByOrderId (orderId) .get (). getOrderId ()) {System.out.println ("Bestillingen er behandlet og sendt vellykket"); }}

La oss kort diskutere vår hovedmetode. I denne metoden simulerer vi en enkel kundeordreflyt ved å bruke tidligere definerte tjenester. Først opprettet vi bestillingen med tre varer og ga den nødvendige frakt- og betalingsinformasjonen. Deretter sendte vi inn bestillingen og til slutt sjekket om den ble sendt og behandlet.

Men hvordan fikk vi alle avhengigheter, og hvorfor gjør det createContainer metode retur Kart<> Objekt>? La oss se nærmere på denne metoden.

5.3. Avhengighetsinjeksjon ved hjelp av ServiceLoader

I dette prosjektet har vi ingen Spring IoC-avhengigheter, så alternativt bruker vi ServiceLoader API for å oppdage implementeringer av tjenester. Dette er ikke en ny funksjon - ServiceLoader API selv har eksistert siden Java 6.

Vi kan få en lasterinstans ved å påkalle en av de statiske laste metoder for ServiceLoader klasse. De laste metoden returnerer Iterabel skriv slik at vi kan gjenta om oppdagede implementeringer.

La oss nå bruke lasteren for å løse våre avhengigheter:

offentlig statisk kart 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 container = ny HashMap (); container.put (OrderService.class, orderService); container.put (ShippingService.class, shippingService); returbeholder; }

Her, vi kaller det statiske laste metode for hvert grensesnitt vi trenger, som skaper en ny lasterinstans hver gang. Som et resultat vil den ikke lagre allerede løste avhengigheter - i stedet vil den opprette nye forekomster hver gang.

Generelt kan tjenesteforekomster opprettes på en av to måter. Enten må klassen for tjenesteimplementering ha en offentlig ikke-arg-konstruktør, eller den må bruke en statisk forsørger metode.

Som en konsekvens har de fleste av våre tjenester ingen arg-konstruktører og settermetoder for avhengigheter. Men, som vi allerede har sett, InMemoryOrderStore klasse implementerer to grensesnitt: CustomerOrderRepository og ShippingOrderRepository.

Imidlertid, hvis vi ber om hvert av disse grensesnittene ved hjelp av laste metode, får vi forskjellige forekomster av InMemoryOrderStore. Det er ikke ønskelig oppførsel, så la oss bruke forsørger metode teknikk for å cache forekomsten:

offentlig klasse InMemoryOrderStore implementerer CustomerOrderRepository, ShippingOrderRepository {privat flyktig statisk InMemoryOrderStore-forekomst = ny InMemoryOrderStore (); offentlig statisk InMemoryOrderStore-leverandør () {returinstans; }}

Vi har brukt Singleton-mønsteret for å cache en enkelt forekomst av InMemoryOrderStore klasse og returnere den fra forsørger metode.

Hvis tjenesteleverandøren erklærer en forsørger metode, deretter ServiceLoader påkaller denne metoden for å skaffe en forekomst av en tjeneste. Ellers vil den prøve å opprette en forekomst ved hjelp av ikke-argumenter-konstruktøren via refleksjon. Som et resultat kan vi endre tjenesteleverandørmekanismen uten å påvirke vår createContainer metode.

Og til slutt gir vi løste avhengigheter til tjenester via settere og returnerer de konfigurerte tjenestene.

Endelig kan vi kjøre applikasjonen.

6. Konklusjon

I denne artikkelen har vi diskutert noen kritiske DDD-konsepter: Bounded Context, Ubiquitous Language og Context Mapping. Selv om det å dele et system i avgrensede sammenhenger har mange fordeler, er det ikke nødvendig å bruke denne tilnærmingen overalt.

Deretter har vi sett hvordan du bruker Java 9 Module System sammen med Bounded Context for å lage sterkt innkapslede moduler.

Videre har vi dekket standard ServiceLoader mekanisme for å oppdage avhengigheter.

Hele kildekoden til prosjektet er tilgjengelig på GitHub.


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