DDD-afgrænsede sammenhænge og Java-moduler

1. Oversigt

Domain-Driven Design (DDD) er et sæt principper og værktøjer, der hjælper os med at designe effektive softwarearkitekturer til at levere højere forretningsværdi. Bounded Context er et af de centrale og vigtige mønstre for at redde arkitektur fra Big Ball Of Mud ved at adskille hele applikationsdomænet i flere semantisk konsistente dele.

Samtidig med Java 9-modulsystemet kan vi oprette stærkt indkapslede moduler.

I denne vejledning opretter vi en simpel butiksprogram og ser, hvordan man udnytter Java 9-moduler, mens vi definerer eksplicitte grænser for afgrænsede sammenhænge.

2. DDD-afgrænsede sammenhænge

I dag er softwaresystemer ikke enkle CRUD-applikationer. Faktisk består det typiske monolitiske enterprise-system af nogle ældre codebase og nyligt tilføjede funktioner. Imidlertid bliver det sværere og sværere at vedligeholde sådanne systemer med hver ændring foretaget. Til sidst kan det blive helt uvedligeholdeligt.

2.1. Begrænset kontekst og allestedsnærværende sprog

For at løse det adresserede problem leverer DDD konceptet Bounded Context. En afgrænset kontekst er en logisk grænse for et domæne, hvor bestemte vilkår og regler gælder konsekvent. Inde i denne grænse, alle termer, definitioner og begreber udgør det allestedsnærværende sprog.

Især er den største fordel ved allestedsnærværende sprog at gruppere projektmedlemmer fra forskellige områder omkring et specifikt forretningsdomæne.

Derudover kan flere sammenhænge arbejde med den samme ting. Det kan dog have forskellige betydninger inden for hver af disse sammenhænge.

2.2. Ordrekontekst

Lad os begynde at implementere vores ansøgning ved at definere ordrekonteksten. Denne sammenhæng indeholder to enheder: Bestillingsvare og Kundeordre.

Det Kundeordre enhed er en samlet rod:

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

Som vi kan se, indeholder denne klasse calcTotalPrice forretningsmetode. Men i et virkeligt projekt vil det sandsynligvis være meget mere kompliceret - for eksempel inklusive rabatter og afgifter i den endelige pris.

Lad os derefter oprette Bestillingsvare klasse:

offentlig klasse OrderItem {private int productId; privat int mængde; privat flyderenhed Pris; privat flyderenhed Vægt; }

Vi har defineret enheder, men vi skal også udsætte noget API for andre dele af applikationen. Lad os oprette CustomerOrderService klasse:

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

Her har vi nogle vigtige punkter at fremhæve. Det Angiv bestilling metode er ansvarlig for behandling af kundeordrer. Når en ordre er behandlet, offentliggøres begivenheden til EventBus. Vi diskuterer den hændelsesdrevne kommunikation i de næste kapitler. Denne tjeneste giver standardimplementeringen til OrderService grænseflade:

offentlig grænseflade OrderService udvider ApplicationService {void placeOrder (CustomerOrder order); ugyldigt setOrderRepository (CustomerOrderRepository orderRepository); }

Desuden kræver denne tjeneste CustomerOrderRepository at bestå ordrer:

offentlig grænseflade CustomerOrderRepository {void saveCustomerOrder (CustomerOrder order); }

Det, der er vigtigt, er det denne grænseflade er ikke implementeret inden for denne sammenhæng, men vil blive leveret af infrastrukturmodulet, som vi får se senere.

2.3. Forsendelseskontekst

Lad os nu definere forsendelseskonteksten. Det vil også være ligetil og indeholde tre enheder: Parcel, PackageItemog Kan bestilles.

Lad os starte med Kan bestilles enhed:

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

I dette tilfælde indeholder enheden ikke betalingsmetode Mark. Det skyldes, at vi i vores forsendelsessammenhæng ikke er ligeglad med, hvilken betalingsmetode der bruges. Shipping Context er kun ansvarlig for behandling af forsendelser af ordrer.

Også den Parcel enhed er specifik for forsendelseskonteksten:

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

Som vi kan se, indeholder den også specifikke forretningsmetoder og fungerer som en samlet rod.

Lad os endelig definere ParcelShippingService:

public class ParcelShippingService implementerer ShippingService {public static final String EVENT_ORDER_READY_FOR_SHIPMENT = "OrderReadyForShipmentEvent"; privat ShippingOrderRepository orderRepository; privat EventBus eventBus; privat kort shippedParcels = nyt HashMap (); @Override public void shipOrder (int orderId) {Optional order = this.orderRepository.findShippableOrder (orderId); order.ifPresent (completedOrder -> {Parcel parcel = new Parcel (completedOrder.getOrderId (), completedOrder.getAddress (), completedOrder.getPackageItems ()); if (parcel.isTaxable ()) {// Beregn yderligere afgifter} // 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.getPayloadidal)) }); } @ Override public Valgfri getParcelByOrderId (int orderId) {returner Optional.ofNullable (this.shippedParcels.get (orderId)); }}

Denne service bruger ligeledes ShippingOrderRepository til at hente ordrer efter id. Mere vigtigt er det, at det abonnerer på OrderReadyForShipmentEvent begivenhed, som offentliggøres i en anden kontekst. Når denne begivenhed finder sted, anvender tjenesten nogle regler og sender ordren. Af enkelheds skyld gemmer vi leverede ordrer i en HashMap.

3. Kontekstkort

Indtil videre definerede vi to sammenhænge. Vi indstillede dog ikke nogen eksplicitte relationer mellem dem. Til dette formål har DDD begrebet Context Mapping. Et kontekstkort er en visuel beskrivelse af forholdet mellem forskellige sammenhænge i systemet. Dette kort viser, hvordan forskellige dele eksisterer sammen for at danne domænet.

Der er fem hovedtyper af forhold mellem afgrænsede sammenhænge:

  • Partnerskab - et forhold mellem to sammenhænge, ​​der samarbejder om at tilpasse de to hold til afhængige mål
  • Delt kerne - en slags forhold, når fælles dele af flere sammenhænge ekstraheres til en anden kontekst / modul for at reducere kodedobling
  • Kunde-leverandør - en forbindelse mellem to sammenhænge, ​​hvor den ene kontekst (opstrøms) producerer data, og den anden (nedstrøms) bruger den. I dette forhold er begge sider interesseret i at etablere den bedst mulige kommunikation
  • Konformist - dette forhold har også opstrøms og nedstrøms, men nedstrøms er altid i overensstemmelse med opstrøms API'er
  • Antikorruptionslag - denne type forhold bruges i vid udstrækning til ældre systemer til at tilpasse dem til en ny arkitektur og gradvist migrere fra den ældre kodebase. Antikorruptionslaget fungerer som en adapter til at oversætte data fra opstrøms og beskytte mod uønskede ændringer

I vores særlige eksempel bruger vi Shared Kernel-forholdet. Vi definerer det ikke i sin rene form, men det fungerer mest som en formidler af begivenheder i systemet.

Således indeholder SharedKernel-modulet ingen konkrete implementeringer, kun grænseflader.

Lad os starte med EventBus grænseflade:

offentlig grænseflade EventBus {ugyldig udgivelse (E begivenhed); ugyldig abonnement (String eventType, EventSubscriber subscriber); ugyldig afmelding (String eventType, EventSubscriber subscriber); }

Denne grænseflade implementeres senere i vores infrastrukturmodul.

Dernæst opretter vi en basistjenesteinterface med standardmetoder til understøttelse af begivenhedsdrevet kommunikation:

offentlig grænseflade ApplicationService {standard ugyldig publishEvent (E begivenhed) {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, subscriber); }} standard ugyldig afmelding (String eventType, EventSubscriber subscriber) {EventBus eventBus = getEventBus (); hvis (eventBus! = null) {eventBus.unsubscribe (eventType, subscriber); }} EventBus getEventBus (); ugyldigt setEventBus (EventBus eventBus); }

Så servicegrænseflader i afgrænsede sammenhænge udvider denne grænseflade til at have fælles begivenhedsrelateret funktionalitet.

4. Java 9-modularitet

Nu er det tid til at undersøge, hvordan Java 9-modulsystemet kan understøtte den definerede applikationsstruktur.

Java Platform Module System (JPMS) opfordrer til at opbygge mere pålidelige og stærkt indkapslede moduler. Som et resultat kan disse funktioner hjælpe med at isolere vores sammenhænge og etablere klare grænser.

Lad os se vores sidste moduldiagram:

4.1. SharedKernel-modul

Lad os starte med SharedKernel-modulet, som ikke har nogen afhængighed af andre moduler. Så modul-info.java ligner:

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

Vi eksporterer modulgrænseflader, så de er tilgængelige for andre moduler.

4.2. OrderContext Modul

Lad os derefter flytte vores fokus til OrderContext-modulet. Det kræver kun grænseflader, der er defineret i SharedKernel-modulet:

modul com.baeldung.dddmodules.ordercontext {kræver com.baeldung.dddmodules.sharedkernel; eksporterer com.baeldung.dddmodules.ordercontext.service; eksporterer com.baeldung.dddmodules.ordercontext.model; eksporterer com.baeldung.dddmodules.ordercontext.repository; giver com.baeldung.dddmodules.ordercontext.service.OrderService med com.baeldung.dddmodules.ordercontext.service.CustomerOrderService; }

Vi kan også se, at dette modul eksporterer standardimplementeringen til OrderService interface.

4.3. ShippingContext Modul

På samme måde som det foregående modul, lad os oprette ShippingContext-modulets definitionsfil:

modul com.baeldung.dddmodules.shippingcontext {kræver com.baeldung.dddmodules.sharedkernel; eksporterer com.baeldung.dddmodules.shippingcontext.service; eksporterer com.baeldung.dddmodules.shippingcontext.model; eksporterer com.baeldung.dddmodules.shippingcontext.repository; giver com.baeldung.dddmodules.shippingcontext.service.ShippingService med com.baeldung.dddmodules.shippingcontext.service.ParcelShippingService; }

På samme måde eksporterer vi standardimplementeringen til ShippingService interface.

4.4. Infrastrukturmodul

Nu er det tid til at beskrive infrastrukturmodulet. Dette modul indeholder implementeringsoplysninger for de definerede grænseflader. Vi starter med at skabe en simpel implementering til EventBus grænseflade:

offentlig klasse SimpleEventBus implementerer EventBus {privat 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); }}}

Dernæst skal vi implementere CustomerOrderRepository og ShippingOrderRepository grænseflader. I de fleste tilfælde er Bestille enhed gemmes i den samme tabel, men bruges som en anden enhedsmodel i afgrænsede sammenhænge.

Det er meget almindeligt at se en enkelt enhed, der indeholder blandet kode fra forskellige områder af forretningsdomænet eller databasetilknytninger på lavt niveau. Til vores implementering har vi opdelt vores enheder efter de afgrænsede sammenhænge: Kundeordre og Kan bestilles.

Lad os først oprette en klasse, der repræsenterer en hel vedvarende model:

offentlig statisk klasse PersistenceOrder {public int orderId; offentlig betalingsstrengemetode; offentlig streng adresse; offentlig listebestillingsartikler; offentlig statisk klasse OrderItem {public int productId; offentlig flyde enhed Pris; offentlige flydende genstand Vægt; offentlig int mængde; }}

Vi kan se, at denne klasse indeholder alle felter fra begge Kundeordre og Kan bestilles enheder.

For at gøre tingene enkle, lad os simulere en database i hukommelsen:

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 Valgfri findShippableOrder (int orderId) {hvis (! This.ordersDb.containsKey (orderId)) returnerer 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)). toList ()))); }}

Her vedvarer vi og henter forskellige typer enheder ved at konvertere vedvarende modeller til eller fra en passende type.

Lad os endelig oprette moduldefinitionen:

modul com.baeldung.dddmodules.infrastructure {kræver transitiv com.baeldung.dddmodules.sharedkernel; kræver transitiv com.baeldung.dddmodules.ordercontext; kræver transitiv com.baeldung.dddmodules.shippingcontext; giver com.baeldung.dddmodules.sharedkernel.events.EventBus med com.baeldung.dddmodules.infrastructure.events.SimpleEventBus; giver com.baeldung.dddmodules.ordercontext.repository.CustomerOrderRepository med com.baeldung.dddmodules.infrastructure.db.InMemoryOrderStore; giver com.baeldung.dddmodules.shippingcontext.repository.ShippingOrderRepository med com.baeldung.dddmodules.infrastructure.db.InMemoryOrderStore; }

Bruger giver med klausul, vi leverer implementeringen af ​​et par grænseflader, der blev defineret i andre moduler.

Desuden fungerer dette modul som en aggregator af afhængigheder, så vi bruger kræver transitiv nøgleord. Som et resultat får et modul, der kræver infrastrukturmodulet, alle disse afhængigheder transitivt.

4.5. Hovedmodul

For at afslutte, lad os definere et modul, der vil være indgangspunktet for vores applikation:

modul com.baeldung.dddmodules.mainapp {bruger com.baeldung.dddmodules.sharedkernel.events.EventBus; bruger com.baeldung.dddmodules.ordercontext.service.OrderService; bruger com.baeldung.dddmodules.ordercontext.repository.CustomerOrderRepository; bruger com.baeldung.dddmodules.shippingcontext.repository.ShippingOrderRepository; bruger com.baeldung.dddmodules.shippingcontext.service.ShippingService; kræver transitiv com.baeldung.dddmodules.infrastructure; }

Da vi lige har indstillet transitive afhængigheder af infrastrukturmodulet, behøver vi ikke kræve dem eksplicit her.

På den anden side viser vi disse afhængigheder med anvendelser nøgleord. Det anvendelser klausul instruerer ServiceLoader, som vi finder ud af i næste kapitel, at dette modul ønsker at bruge disse grænseflader. Imidlertid, det kræver ikke, at implementeringer er tilgængelige under kompileringstiden.

5. Kørsel af applikationen

Endelig er vi næsten klar til at opbygge vores applikation. Vi vil bruge Maven til at opbygge vores projekt. Dette gør det meget nemmere at arbejde med moduler.

5.1. Projektstruktur

Vores projekt indeholder fem moduler og overordnet modul. Lad os se på vores projektstruktur:

ddd-moduler (rodmappen) pom.xml | - infrastruktur | - src | - main | - 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. Hovedapplikation

Nu har vi alt undtagen hovedapplikationen, så lad os definere vores vigtigste 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 kundeOrder (); int orderId = 1; customerOrder.setOrderId (orderId); Liste orderItems = ny ArrayList (); orderItems.add (nyt OrderItem (1, 2, 3, 1)); orderItems.add (nyt OrderItem (2, 1, 1, 1)); orderItems.add (nyt OrderItem (3, 4, 11, 21)); customerOrder.setOrderItems (orderItems); customerOrder.setPaymentMethod ("PayPal"); customerOrder.setAddress ("Fuld adresse her"); orderService.placeOrder (kundeordre); hvis (orderId == shippingService.getParcelByOrderId (orderId) .get (). getOrderId ()) {System.out.println ("Ordren er behandlet og sendt med succes"); }}

Lad os kort diskutere vores hovedmetode. I denne metode simulerer vi et simpelt kundeordreflow ved hjælp af tidligere definerede tjenester. Først oprettede vi ordren med tre varer og leverede de nødvendige forsendelses- og betalingsoplysninger. Derefter sendte vi ordren og kontrollerede endelig, om den blev sendt og behandlet med succes.

Men hvordan fik vi alle afhængigheder, og hvorfor gør det createContainer metode retur Kort<> Objekt>? Lad os se nærmere på denne metode.

5.3. Afhængighedsinjektion ved hjælp af ServiceLoader

I dette projekt har vi ingen Spring IoC-afhængigheder, så alternativt bruger vi ServiceLoader API til at opdage implementeringer af tjenester. Dette er ikke en ny funktion - ServiceLoader API selv har eksisteret siden Java 6.

Vi kan få en læsserinstans ved at påkalde en af ​​de statiske belastning metoder til ServiceLoader klasse. Det belastning metoden returnerer Iterabel skriv, så vi kan gentage opdagede implementeringer.

Lad os nu anvende læsseren for at løse vores afhængigheder:

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

Her, vi kalder det statiske belastning metode til hver grænseflade, vi har brug for, hvilket skaber en ny læsserinstans hver gang. Som et resultat cachelagrer den ikke allerede løste afhængigheder - i stedet opretter den nye forekomster hver gang.

Generelt kan serviceinstanser oprettes på en af ​​to måder. Enten skal serviceimplementeringsklassen have en offentlig no-arg-konstruktør, eller den skal bruge en statisk udbyder metode.

Som en konsekvens har de fleste af vores tjenester ingen arg-konstruktører og settermetoder til afhængigheder. Men som vi allerede har set, InMemoryOrderStore klasse implementerer to grænseflader: CustomerOrderRepository og ShippingOrderRepository.

Men hvis vi beder om hver af disse grænseflader ved hjælp af belastning metode, får vi forskellige forekomster af InMemoryOrderStore. Det er ikke ønskelig adfærd, så lad os bruge udbyder metode teknik til at cache forekomsten:

offentlig klasse InMemoryOrderStore implementerer CustomerOrderRepository, ShippingOrderRepository {privat flygtig statisk InMemoryOrderStore-forekomst = ny InMemoryOrderStore (); offentlig statisk InMemoryOrderStore-udbyder () {return instans; }}

Vi har anvendt Singleton-mønsteret til at cache en enkelt forekomst af InMemoryOrderStore klasse og returnere den fra udbyder metode.

Hvis tjenesteudbyderen erklærer en udbyder metode, derefter ServiceLoader påkalder denne metode for at få en forekomst af en tjeneste. Ellers vil den forsøge at oprette en instans ved hjælp af konstruktionen uden argumenter via refleksion. Som et resultat kan vi ændre tjenesteudbydermekanismen uden at påvirke vores createContainer metode.

Og endelig leverer vi løste afhængigheder til tjenester via settere og returnerer de konfigurerede tjenester.

Endelig kan vi køre applikationen.

6. Konklusion

I denne artikel har vi diskuteret nogle kritiske DDD-koncepter: Bounded Context, Allestedsnærværende sprog og Context Mapping. Mens opdeling af et system i afgrænsede sammenhænge har mange fordele, er det samtidig ikke nødvendigt at anvende denne tilgang overalt.

Dernæst har vi set, hvordan man bruger Java 9-modulsystemet sammen med Bounded Context til at skabe stærkt indkapslede moduler.

Desuden har vi dækket standard ServiceLoader mekanisme til at opdage afhængigheder.

Den fulde kildekode for projektet er tilgængelig på GitHub.