Lagen ordenen met behulp van hexagonale architectuur, DDD en Spring

Java Top

Ik heb zojuist het nieuwe aangekondigd Leer de lente natuurlijk, gericht op de basisprincipes van Spring 5 en Spring Boot 2:

>> BEKIJK DE CURSUS

1. Overzicht

In deze tutorial implementeren we een Spring-applicatie met DDD. Bovendien organiseren we lagen met behulp van Hexagonal Architecture.

Met deze aanpak kunnen we eenvoudig de verschillende lagen van de applicatie uitwisselen.

2. Zeshoekige architectuur

Hexagonale architectuur is een model van het ontwerpen van softwareapplicaties rond domeinlogica om het te isoleren van externe factoren.

De domeinlogica wordt gespecificeerd in een zakelijke kern, die we het interne deel zullen noemen, de rest zijn externe delen. Toegang tot domeinlogica van buitenaf is beschikbaar via poorten en adapters.

3. Principes

Ten eerste moeten we principes definiëren om onze code te verdelen. Zoals al kort uitgelegd, definieert hexagonale architectuur het binnen- en buitendeel.

Wat we in plaats daarvan zullen doen, is onze applicatie in drie lagen verdelen; applicatie (buiten), domein (binnen) en infrastructuur (buiten):

Door de applicatielaag, de gebruiker of enig ander programma werkt samen met de applicatie. Dit gebied zou zaken moeten bevatten als gebruikersinterfaces, RESTful-controllers en JSON-serialiseringsbibliotheken. Het omvat alles die toegang tot onze applicatie blootstelt en de uitvoering van domeinlogica orkestreert.

In de domeinlaag bewaren we de code die zakelijke logica raakt en implementeert. Dit is de kern van onze applicatie. Bovendien moet deze laag worden geïsoleerd van zowel het applicatiegedeelte als het infrastructuurgedeelte. Bovendien moet het ook interfaces bevatten die de API definiëren om te communiceren met externe onderdelen, zoals de database, waarmee het domein communiceert.

Ten slotte de infrastructuurlaag is het deel dat alles bevat dat de applicatie nodig heeft om te werken zoals databaseconfiguratie of Spring-configuratie. Daarnaast implementeert het ook infrastructuurafhankelijke interfaces vanuit de domeinlaag.

4. Domeinlaag

Laten we beginnen met het implementeren van onze kernlaag, de domeinlaag.

Ten eerste moeten we het Bestellen klasse:

openbare klasse Order {privé UUID-id; privé OrderStatus-status; privélijst orderItems; privé BigDecimal-prijs; openbare bestelling (UUID-id, productproduct) {this.id = id; this.orderItems = nieuwe ArrayList (Arrays.astList (nieuw OrderItem (product))); this.status = OrderStatus.CREATED; this.price = product.getPrice (); } public void complete () {validateState (); this.status = OrderStatus.COMPLETED; } public void addOrder (Productproduct) {validateState (); validateProduct (product); orderItems.add (nieuw OrderItem (product)); price = price.add (product.getPrice ()); } openbare ongeldige removeOrder (UUID-id) {validateState (); laatste OrderItem orderItem = getOrderItem (id); orderItems.remove (orderItem); price = price.subtract (orderItem.getPrice ()); } // getters}

Dit is onze totale root. Alles wat met onze bedrijfslogica te maken heeft, zal door deze les gaan. Bovendien, Bestellen is verantwoordelijk voor het in de juiste staat houden:

  • De bestelling kan alleen worden gemaakt met de opgegeven ID en op basis van één Product - de constructor voert zelf ook de bestelling uit met GECREËERD toestand
  • Zodra de bestelling is voltooid, verandert u Bestel items is onmogelijk
  • Het is onmogelijk om het Bestellen van buiten het domeinobject, zoals bij een setter

Bovendien is de Bestellen class is ook verantwoordelijk voor het maken van zijn Bestel item.

Laten we het Bestel item klas dan:

openbare klasse OrderItem {privé UUID productId; privé BigDecimal-prijs; openbare OrderItem (Productproduct) {this.productId = product.getId (); this.price = product.getPrice (); } // getters}

Zoals we kunnen zien, Bestel item is gemaakt op basis van een Product. Het behoudt de verwijzing ernaar en slaat de huidige prijs van het Product.

Vervolgens maken we een repository-interface (a haven in zeshoekige architectuur). De implementatie van de interface vindt plaats in de infrastructuurlaag:

openbare interface OrderRepository {Optioneel findById (UUID-id); ongeldig opslaan (bestelling bestellen); }

Ten slotte moeten we ervoor zorgen dat de Bestellen wordt altijd opgeslagen na elke actie. Om dat te doen, we zullen een Domain Service definiëren, die meestal logica bevat die geen deel kan uitmaken van onze root:

openbare klasse DomainOrderService implementeert OrderService {privé definitieve OrderRepository orderRepository; openbare DomainOrderService (OrderRepository orderRepository) {this.orderRepository = orderRepository; } @Override public UUID createOrder (Productproduct) {Order order = new Order (UUID.randomUUID (), product); orderRepository.save (bestelling); retour order.getId (); } @Override public void addProduct (UUID id, Productproduct) {Order order = getOrder (id); order.addOrder (product); orderRepository.save (bestelling); } @Override public void completeOrder (UUID id) {Order order = getOrder (id); Bestelling voltooid(); orderRepository.save (bestelling); } @Override public void deleteProduct (UUID id, UUID productId) {Order order = getOrder (id); order.removeOrder (productId); orderRepository.save (bestelling); } private Order getOrder (UUID id) {retour orderRepository .findById (id) .orElseThrow (RuntimeException :: nieuw); }}

In een hexagonale architectuur is deze service een adapter die de poort implementeert. Bovendien, we zullen het niet registreren als Spring beanomdat, vanuit een domeinperspectief, dit zich aan de binnenkant bevindt en de veerconfiguratie aan de buitenkant. We zullen het iets later handmatig bedraden met Spring in de infrastructuurlaag.

Omdat de domeinlaag volledig is ontkoppeld van applicatie- en infrastructuurlagen, wijkan ook test het onafhankelijk:

klasse DomainOrderServiceUnitTest {privé OrderRepository orderRepository; private DomainOrderService getest; @BeforeEach void setUp () {orderRepository = mock (OrderRepository.class); getest = nieuwe DomainOrderService (orderRepository); } @Test void shouldCreateOrder_thenSaveIt () {eindproduct product = nieuw product (UUID.randomUUID (), BigDecimal.TEN, "productName"); definitieve UUID-id = getest.createOrder (product); verifieer (orderRepository) .save (elke (Order.class)); assertNotNull (id); }}

5. Applicatielaag

In deze sectie zullen we de applicatielaag implementeren. We laten de gebruiker communiceren met onze applicatie via een RESTful API.

Laten we daarom het OrderController:

@RestController @RequestMapping ("/ orders") openbare klasse OrderController {privé OrderService orderService; @Autowired openbare OrderController (OrderService orderService) {this.orderService = orderService; } @PostMapping CreateOrderResponse createOrder (@RequestBody CreateOrderRequest-verzoek) {UUID id = orderService.createOrder (request.getProduct ()); retourneer nieuwe CreateOrderResponse (id); } @PostMapping (value = "/ {id} / products") void addProduct (@PathVariable UUID-id, @RequestBody AddProductRequest-verzoek) {orderService.addProduct (id, request.getProduct ()); } @DeleteMapping (value = "/ {id} / products") void deleteProduct (@PathVariable UUID id, @RequestParam UUID productId) {orderService.deleteProduct (id, productId); } @PostMapping ("/ {id} / complete") void completeOrder (@PathVariable UUID id) {orderService.completeOrder (id); }}

Deze simpele Spring Rest controller is verantwoordelijk voor het orkestreren van de uitvoering van domeinlogica.

Deze controller past de externe RESTful-interface aan ons domein aan. Het doet het door de juiste methoden aan te roepen van Bestelservice (haven).

6. Infrastructuurlaag

De infrastructuurlaag bevat de logica die nodig is om de applicatie uit te voeren.

Daarom beginnen we met het maken van de configuratieklassen. Laten we eerst een klasse implementeren die onze Bestelservice als lenteboon:

@Configuration openbare klasse BeanConfiguration {@Bean OrderService orderService (OrderRepository orderRepository) {retourneer nieuwe DomainOrderService (orderRepository); }}

Laten we vervolgens de configuratie maken die verantwoordelijk is voor het inschakelen van de Spring Data-opslagplaatsen die we zullen gebruiken:

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

We hebben de basePackageClasses eigenschap omdat die repositories zich alleen in de infrastructuurlaag kunnen bevinden. Daarom heeft Spring geen reden om de hele applicatie te scannen. Verder kan deze klasse alles bevatten met betrekking tot het tot stand brengen van een verbinding tussen MongoDB en onze applicatie.

Ten slotte implementeren we het OrderRepository van de domeinlaag. We gebruiken onze SpringDataMongoOrderRepository in onze implementatie:

@Component openbare klasse MongoDbOrderRepository implementeert OrderRepository {privé SpringDataMongoOrderRepository orderRepository; @Autowired openbare MongoDbOrderRepository (SpringDataMongoOrderRepository orderRepository) {this.orderRepository = orderRepository; } @Override public Optioneel findById (UUID id) {return orderRepository.findById (id); } @Override public void save (Order order) {orderRepository.save (order); }}

Deze implementatie slaat onze Bestellen in MongoDB. In een hexagonale architectuur is deze implementatie ook een adapter.

7. Voordelen

Het eerste voordeel van deze aanpak is dat we apart werk voor elke laag. We kunnen ons op één laag concentreren zonder anderen te beïnvloeden.

Bovendien zijn ze van nature gemakkelijker te begrijpen omdat ze zich allemaal concentreren op de logica ervan.

Een ander groot voordeel is dat we de domeinlogica van al het andere hebben geïsoleerd. Het domeingedeelte bevat alleen bedrijfslogica en kan eenvoudig naar een andere omgeving worden verplaatst.

Laten we in feite de infrastructuurlaag wijzigen om Cassandra als een database te gebruiken:

@Component openbare klasse CassandraDbOrderRepository implementeert OrderRepository {privé laatste SpringDataCassandraOrderRepository orderRepository; @Autowired openbare CassandraDbOrderRepository (SpringDataCassandraOrderRepository orderRepository) {this.orderRepository = orderRepository; } @Override public Optioneel findById (UUID id) {Optioneel orderEntity = orderRepository.findById (id); if (orderEntity.isPresent ()) {return Optioneel.of (orderEntity.get () .toOrder ()); } else {return Optioneel.empty (); }} @Override public void save (Order order) {orderRepository.save (nieuwe OrderEntity (order)); }}

In tegenstelling tot MongoDB gebruiken we nu een OrderEntity om het domein in de database te behouden.

Als we technologiespecifieke annotaties toevoegen aan onze Bestellen domeinobject, dan we schenden de ontkoppeling tussen infrastructuur- en domeinlagen.

De repository past het domein aan onze persistentiebehoeften aan.

Laten we een stap verder gaan en onze RESTful-applicatie transformeren in een opdrachtregelapplicatie:

@Component openbare klasse CliOrderController {privé statische laatste Logger LOG = LoggerFactory.getLogger (CliOrderController.class); privé definitieve OrderService orderService; @Autowired openbare CliOrderController (OrderService orderService) {this.orderService = orderService; } public void createCompleteOrder () {LOG.info ("<>"); UUID orderId = createOrder (); orderService.completeOrder (orderId); } public void createIncompleteOrder () {LOG.info ("<>"); UUID orderId = createOrder (); } private UUID createOrder () {LOG.info ("Een nieuwe bestelling plaatsen met twee producten"); Product mobilePhone = nieuw product (UUID.randomUUID (), BigDecimal.valueOf (200), "mobiel"); Product razor = nieuw product (UUID.randomUUID (), BigDecimal.valueOf (50), "razor"); LOG.info ("Bestelling aanmaken met gsm"); UUID orderId = orderService.createOrder (mobilePhone); LOG.info ("Een scheermes aan de bestelling toevoegen"); orderService.addProduct (orderId, scheermes); retour orderId; }}

In tegenstelling tot voorheen hebben we nu een reeks vooraf gedefinieerde acties bedraad die communiceren met ons domein. We zouden dit kunnen gebruiken om onze applicatie bijvoorbeeld te vullen met bespotte data.

Hoewel we het doel van de applicatie volledig hebben veranderd, hebben we de domeinlaag niet aangeraakt.

8. Conclusie

In dit artikel hebben we geleerd hoe we de logica met betrekking tot onze applicatie in specifieke lagen kunnen scheiden.

Eerst hebben we drie hoofdlagen gedefinieerd: applicatie, domein en infrastructuur. Daarna hebben we beschreven hoe we ze moesten invullen en legden we de voordelen uit.

Vervolgens hebben we voor elke laag de implementatie bedacht:

Ten slotte hebben we de applicatie- en infrastructuurlagen verwisseld zonder het domein te beïnvloeden.

Zoals altijd is de code voor deze voorbeelden beschikbaar op GitHub.

Java onderkant

Ik heb zojuist het nieuwe aangekondigd Leer de lente natuurlijk, gericht op de basisprincipes van Spring 5 en Spring Boot 2:

>> BEKIJK DE CURSUS