Organisering av lag med sekskantet arkitektur, DDD og vår

Java Top

Jeg kunngjorde nettopp det nye Lær våren kurs, med fokus på det grunnleggende i vår 5 og vårstøvel 2:

>> KONTROLLER KURSET

1. Oversikt

I denne veiledningen implementerer vi en vårapplikasjon ved hjelp av DDD. I tillegg organiserer vi lag ved hjelp av Hexagonal Architecture.

Med denne tilnærmingen kan vi enkelt bytte ut de forskjellige lagene i applikasjonen.

2. Sekskantet arkitektur

Sekskantet arkitektur er en modell av utforme programvare rundt domenelogikk å isolere det fra eksterne faktorer.

Domenelogikken er spesifisert i en forretningskjerne, som vi vil kalle den indre delen, resten er utenfor deler. Tilgang til domenelogikk utenfra er tilgjengelig via porter og adaptere.

3. Prinsipper

For det første bør vi definere prinsipper for å dele koden vår. Som allerede forklart kort, definerer sekskantet arkitektur innsiden og utsiden.

Det vi skal gjøre i stedet er å dele applikasjonen vår i tre lag; applikasjon (utenfor), domene (inne) og infrastruktur (utenfor):

Gjennom applikasjonslaget, brukeren eller et annet program samhandler med søknaden. Dette området skal inneholde ting som brukergrensesnitt, RESTful-kontrollere og JSON-serialiseringsbiblioteker. Det inkluderer hva som helst som avslører inngang til søknaden vår og orkestrerer utførelsen av domenelogikken.

I domenelaget beholder vi koden som berører og implementerer forretningslogikk. Dette er kjernen i søknaden vår. I tillegg bør dette laget isoleres fra både applikasjonsdelen og infrastrukturdelen. På toppen av det, bør det også inneholde grensesnitt som definerer API for å kommunisere med eksterne deler, som databasen, som domenet samhandler med.

Til slutt, infrastrukturlag er den delen som inneholder alt som applikasjonen trenger for å fungere som databasekonfigurasjon eller fjærkonfigurasjon. Dessuten implementerer den også infrastrukturavhengige grensesnitt fra domenelaget.

4. Domenelag

La oss begynne med å implementere kjernelaget vårt, som er domenelaget.

For det første bør vi lage Rekkefølge klasse:

offentlig klasse Bestill {privat UUID-id; privat OrderStatus-status; private listeordreelementer; privat BigDecimal pris; offentlig orden (UUID-id, produktprodukt) {this.id = id; this.orderItems = new ArrayList (Arrays.astList (new OrderItem (product))); this.status = OrderStatus.CREATED; this.price = product.getPrice (); } offentlig tomrom fullført () {validateState (); this.status = OrderStatus.COMPLETED; } offentlig ugyldig addOrder (produktprodukt) {validateState (); validateProduct (produkt); orderItems.add (nytt OrderItem (produkt)); pris = price.add (product.getPrice ()); } offentlig ugyldig removeOrder (UUID id) {validateState (); endelig OrderItem orderItem = getOrderItem (id); orderItems.remove (orderItem); pris = price.subtract (orderItem.getPrice ()); } // getters}

Dette er vår samlede rot. Alt relatert til vår forretningslogikk vil gå gjennom denne klassen. I tillegg Rekkefølge er ansvarlig for å holde seg i riktig tilstand:

  • Bestillingen kan bare opprettes med gitt ID og basert på en Produkt - konstruktøren selv legger også inn bestillingen med OPPRETT status
  • Når bestillingen er fullført, endres Bestillingsvares er umulig
  • Det er umulig å endre Rekkefølge utenfor domeneobjektet, som med en setter

Videre er den Rekkefølge klasse er også ansvarlig for å skape sin Bestillingsvare.

La oss lage Bestillingsvare klasse da:

offentlig klasse OrderItem {private UUID productId; privat BigDecimal pris; public OrderItem (Product product) {this.productId = product.getId (); this.price = product.getPrice (); } // getters}

Som vi kan se, Bestillingsvare er opprettet basert på en Produkt. Den holder referansen til den og lagrer den nåværende prisen på Produkt.

Deretter oppretter vi et depotgrensesnitt (a havn i sekskantet arkitektur). Implementeringen av grensesnittet vil være i infrastrukturlaget:

offentlig grensesnitt OrderRepository {Valgfri findById (UUID id); ugyldig lagre (bestillingsordre); }

Til slutt bør vi sørge for at Rekkefølge vil alltid bli lagret etter hver handling. Å gjøre det, vi definerer en domenetjeneste, som vanligvis inneholder logikk som ikke kan være en del av vår rot:

offentlig klasse DomainOrderService implementerer OrderService {private final OrderRepository orderRepository; public DomainOrderService (OrderRepository orderRepository) {this.orderRepository = orderRepository; } @ Override public UUID createOrder (Product product) {Order order = new Order (UUID.randomUUID (), product); orderRepository.save (ordre); returordre.getId (); } @ Override public void addProduct (UUID id, Product product) {Order order = getOrder (id); order.addOrder (produkt); orderRepository.save (ordre); } @ Override public void completeOrder (UUID id) {Order order = getOrder (id); bestilling fullført(); orderRepository.save (ordre); } @Override public void deleteProduct (UUID id, UUID productId) {Order order = getOrder (id); order.removeOrder (productId); orderRepository.save (ordre); } privat Order getOrder (UUID id) {return orderRepository .findById (id) .orElseThrow (RuntimeException :: new); }}

I en sekskantet arkitektur er denne tjenesten en adapter som implementerer porten. I tillegg Vi registrerer den ikke som en vårbønnefordi, fra et domeneperspektiv, er dette i den indre delen, og vårkonfigurasjonen er på utsiden. Vi kobler den manuelt med Spring i infrastrukturlaget litt senere.

Fordi domenelaget er helt frakoblet fra applikasjons- og infrastrukturlag, vikan også test det uavhengig:

klasse DomainOrderServiceUnitTest {private OrderRepository orderRepository; privat DomainOrderService testet; @BeforeEach void setUp () {orderRepository = mock (OrderRepository.class); testet = ny DomainOrderService (orderRepository); } @Test ugyldig shouldCreateOrder_thenSaveIt () {final product product = new Product (UUID.randomUUID (), BigDecimal.TEN, "productName"); endelig UUID id = testet.createOrder (produkt); verifiser (orderRepository) .save (any (Order.class)); assertNotNull (id); }}

5. Påføringslag

I denne delen implementerer vi applikasjonslaget. Vi lar brukeren kommunisere med applikasjonen vår via et RESTful API.

La oss derfor lage OrderController:

@RestController @RequestMapping ("/ orders") offentlig klasse OrderController {private OrderService orderService; @Autowired offentlig OrderController (OrderService orderService) {this.orderService = orderService; } @PostMapping CreateOrderResponse createOrder (@RequestBody CreateOrderRequest request) {UUID id = orderService.createOrder (request.getProduct ()); returner nye CreateOrderResponse (id); } @PostMapping (value = "/ {id} / products") ugyldig addProduct (@PathVariable UUID id, @RequestBody AddProductRequest forespørsel) {orderService.addProduct (id, request.getProduct ()); } @DeleteMapping (value = "/ {id} / products") ugyldig deleteProduct (@PathVariable UUID id, @RequestParam UUID productId) {orderService.deleteProduct (id, productId); } @PostMapping ("/ {id} / complete") ugyldig completeOrder (@PathVariable UUID id) {orderService.completeOrder (id); }}

Denne enkle vårhvilenheten er ansvarlig for å organisere utførelsen av domenelogikk.

Denne kontrolleren tilpasser det ytre RESTful-grensesnittet til domenet vårt. Det gjør det ved å ringe de riktige metodene fra OrderService (havn).

6. Infrastrukturlag

Infrastrukturlaget inneholder logikken som trengs for å kjøre applikasjonen.

Derfor begynner vi med å lage konfigurasjonsklassene. For det første, la oss implementere en klasse som vil registrere vår OrderService som en vårbønne:

@Configuration public class BeanConfiguration {@Bean OrderService orderService (OrderRepository orderRepository) {returner nytt DomainOrderService (orderRepository); }}

La oss deretter opprette konfigurasjonen som er ansvarlig for å aktivere Spring Data-lagringene vi bruker:

@EnableMongoRepositories (basePackageClasses = SpringDataMongoOrderRepository.class) offentlig klasse MongoDBConfiguration {}

Vi har brukt basePackageClasses eiendom fordi disse depotene bare kan være i infrastrukturlaget. Derfor er det ingen grunn til at Spring skal skanne hele applikasjonen. Videre kan denne klassen inneholde alt relatert til å etablere en forbindelse mellom MongoDB og vår applikasjon.

Til slutt implementerer vi OrderRepository fra domenelaget. Vi bruker vår SpringDataMongoOrderRepository i implementeringen vår:

@Komponent offentlig klasse MongoDbOrderRepository implementerer OrderRepository {private SpringDataMongoOrderRepository orderRepository; @Autowired public MongoDbOrderRepository (SpringDataMongoOrderRepository orderRepository) {this.orderRepository = orderRepository; } @ Override public Valgfritt findById (UUID id) {return orderRepository.findById (id); } @ Overstyr offentlig tomrom lagre (bestillingsordre) {orderRepository.save (ordre); }}

Denne implementeringen lagrer vår Rekkefølge i MongoDB. I en sekskantet arkitektur er denne implementeringen også en adapter.

7. Fordeler

Den første fordelen med denne tilnærmingen er at vi separat arbeid for hvert lag. Vi kan fokusere på ett lag uten å påvirke andre.

Videre er de naturlig lettere å forstå fordi hver av dem fokuserer på logikken.

En annen stor fordel er at vi har isolert domenelogikken fra alt annet. Domenedelen inneholder bare forretningslogikk og kan enkelt flyttes til et annet miljø.

La oss faktisk endre infrastrukturlaget for å bruke Cassandra som en database:

@Komponent offentlig klasse CassandraDbOrderRepository implementerer OrderRepository {private final SpringDataCassandraOrderRepository orderRepository; @Autowired public CassandraDbOrderRepository (SpringDataCassandraOrderRepository orderRepository) {this.orderRepository = orderRepository; } @ Override public Valgfri findById (UUID id) {Valgfri orderEntity = orderRepository.findById (id); hvis (orderEntity.isPresent ()) {return Optional.of (orderEntity.get () .toOrder ()); } annet {retur Optional.empty (); }} @ Overstyr offentlig tomrom lagre (ordreordre) {orderRepository.save (ny OrderEntity (ordre)); }}

I motsetning til MongoDB bruker vi nå en OrderEntity for å vedvare domenet i databasen.

Hvis vi legger til teknologispesifikke merknader til Rekkefølge domeneobjekt, deretter vi bryter frakoblingen mellom infrastruktur og domenelag.

Datalageret tilpasser domenet til våre vedvarende behov.

La oss gå et skritt videre og forvandle vår RESTful-applikasjon til en kommandolinjeapplikasjon:

@Komponent offentlig klasse CliOrderController {privat statisk slutt Logger LOG = LoggerFactory.getLogger (CliOrderController.class); privat endelig OrderService orderService; @Autowired offentlig CliOrderController (OrderService orderService) {this.orderService = orderService; } offentlig tomrum createCompleteOrder () {LOG.info ("<>"); UUID orderId = createOrder (); orderService.completeOrder (orderId); } offentlig tomrom createIncompleteOrder () {LOG.info ("<>"); UUID orderId = createOrder (); } privat UUID createOrder () {LOG.info ("Plasser en ny bestilling med to produkter"); Produkt mobilePhone = nytt produkt (UUID.randomUUID (), BigDecimal.valueOf (200), "mobil"); Produkt barberhøvel = nytt produkt (UUID.randomUUID (), BigDecimal.valueOf (50), "barberhøvel"); LOG.info ("Opprette bestilling med mobiltelefon"); UUID orderId = orderService.createOrder (mobilePhone); LOG.info ("Legge til en barberhøvel i bestillingen"); orderService.addProduct (orderId, barberhøvel); returordre }}

I motsetning til tidligere har vi nå koblet et sett med forhåndsdefinerte handlinger som samhandler med domenet vårt. Vi kan bruke dette til å fylle ut applikasjonen vår med spottede data, for eksempel.

Selv om vi fullstendig endret formålet med applikasjonen, har vi ikke berørt domenelaget.

8. Konklusjon

I denne artikkelen har vi lært hvordan vi kan skille logikken knyttet til applikasjonen vår i bestemte lag.

Først definerte vi tre hovedlag: applikasjon, domene og infrastruktur. Etter det beskrev vi hvordan vi skulle fylle dem og forklarte fordelene.

Så kom vi på implementeringen for hvert lag:

Til slutt byttet vi applikasjons- og infrastrukturlagene uten å påvirke domenet.

Som alltid er koden for disse eksemplene tilgjengelig på GitHub.

Java bunn

Jeg kunngjorde nettopp det nye Lær våren kurs, med fokus på det grunnleggende i vår 5 og vårstøvel 2:

>> KONTROLLER KURSET

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