Usporiadanie vrstiev pomocou hexagonálnej architektúry, DDD a pružiny

Java Top

Práve som oznámil nové Naučte sa jar kurz zameraný na základy jari 5 a Spring Boot 2:

>> SKONTROLUJTE KURZ

1. Prehľad

V tomto tutoriále implementujeme jarnú aplikáciu pomocou DDD. Ďalej budeme organizovať vrstvy pomocou Hexagonal Architecture.

S týmto prístupom môžeme ľahko vymieňať rôzne vrstvy aplikácie.

2. Šesťhranná architektúra

Hexagonal architecture is a model of návrh softvérových aplikácií na základe doménovej logiky izolovať ho od vonkajších faktorov.

Logika domény je uvedená v obchodnom jadre, ktoré nazveme vnútorná časť, zvyšok sú vonkajšie časti. Prístup k doménovej logike je zvonka možný prostredníctvom portov a adaptérov.

3. Princípy

Po prvé, mali by sme definovať princípy rozdelenia nášho kódu. Ako už bolo stručne vysvetlené, definuje sa hexagonálna architektúra vnútorná a vonkajšia časť.

Namiesto toho urobíme rozdelenie našej aplikácie do troch vrstiev; aplikácia (zvonka), doména (vnútri) a infraštruktúra (zvonka):

Skrz interakcia aplikačnej vrstvy, používateľa alebo iného programu s aplikáciou. Táto oblasť by mala obsahovať napríklad používateľské rozhrania, radiče RESTful a knižnice serializácie JSON. Zahŕňa všetko ktorý sprístupňuje vstup do našej aplikácie a organizuje vykonávanie logiky domény.

V doménovej vrstve uchovávame kód, ktorý sa dotýka a implementuje obchodnú logiku. Toto je jadro našej aplikácie. Okrem toho by táto vrstva mala byť izolovaná od aplikačnej aj infraštruktúrnej časti. Okrem toho by mala obsahovať aj rozhrania, ktoré definujú rozhranie API na komunikáciu s externými časťami, napríklad s databázou, s ktorou doména interaguje.

A nakoniec vrstva infraštruktúry je časť, ktorá obsahuje všetko, čo aplikácia potrebuje na prácu ako je konfigurácia databázy alebo jarná konfigurácia. Okrem toho tiež implementuje rozhrania závislé od infraštruktúry z doménovej vrstvy.

4. Doménová vrstva

Začnime implementáciou našej základnej vrstvy, ktorou je doménová vrstva.

Najskôr by sme mali vytvoriť objednať trieda:

public class Order {private UUID id; súkromný stav OrderStatus; zoznam súkromných položiek; súkromná cena BigDecimal; verejná objednávka (identifikátor UUID, produktový produkt) {this.id = id; this.orderItems = nový ArrayList (Arrays.astList (nový OrderItem (produkt))); this.status = OrderStatus.CREATED; this.price = product.getPrice (); } public void complete () {validateState (); this.status = OrderStatus.COMPLETED; } public void addOrder (produktový produkt) {validateState (); validateProduct (produkt); orderItems.add (nový OrderItem (produkt)); price = price.add (product.getPrice ()); } public void removeOrder (UUID id) {validateState (); final OrderItem orderItem = getOrderItem (id); orderItems.remove (orderItem); cena = cena.subtract (orderItem.getPrice ()); } // zakladatelia}

Toto je náš agregovaný koreň. Touto triedou prejde všetko, čo súvisí s našou obchodnou logikou. Navyše, objednať je zodpovedný za udržanie sa v správnom stave:

  • Objednávku je možné vytvoriť iba s daným ID a na základe jedného Produkt - samotný konštruktér tiež zadá objednávku s VYTVORENÉ postavenie
  • Po dokončení objednávky sa mení OrderItems je nemožné
  • Je nemožné zmeniť objednať z vonkajšej strany doménového objektu, ako u nastavovača

Ďalej objednať trieda je tiež zodpovedná za jej vytvorenie OrderItem.

Vytvorme OrderItem trieda potom:

public class OrderItem {private UUID productId; súkromná cena BigDecimal; public OrderItem (produktový produkt) {this.productId = product.getId (); this.price = product.getPrice (); } // zakladatelia}

Ako vidíme, OrderItem sa vytvára na základe a Výrobok. Ponecháva si na ňu referenciu a ukladá aktuálnu cenu Výrobok.

Ďalej vytvoríme rozhranie úložiska (a prístav v šesťhrannej architektúre). Implementácia rozhrania bude prebiehať vo vrstve infraštruktúry:

verejné rozhranie OrderRepository {Voliteľné findById (UUID id); neplatnosť uloženia (objednávka objednávky); }

Nakoniec by sme sa mali ubezpečiť, že: objednať sa po každej akcii vždy uloží. Urobiť to, definujeme doménovú službu, ktorá zvyčajne obsahuje logiku, ktorá nemôže byť súčasťou nášho root:

verejná trieda DomainOrderService implementuje OrderService {private final OrderRepository orderRepository; public DomainOrderService (OrderRepository orderRepository) {this.orderRepository = orderRepository; } @Override public UUID createOrder (produktový produkt) {Order order = new Order (UUID.randomUUID (), product); orderRepository.save (objednávka); vrátiť order.getId (); } @Override public void addProduct (UUID id, Product product) {Objednávka objednávky = getOrder (id); order.addOrder (produkt); orderRepository.save (objednávka); } @Override public void completeOrder (UUID id) {Objednávka objednávky = getOrder (id); Objednávka dokončená(); orderRepository.save (objednávka); } @Override public void deleteProduct (UUID id, UUID productId) {Objednávka objednávky = getOrder (id); order.removeOrder (productId); orderRepository.save (objednávka); } private Order getOrder (UUID id) {return orderRepository .findById (id) .orElseThrow (RuntimeException :: new); }}

V šesťuholníkovej architektúre je táto služba adaptérom, ktorý implementuje port. Navyše, nebudeme to registrovať ako jarnú fazuľupretože z pohľadu domény je to vo vnútornej časti a jarná konfigurácia zvonku. O niečo neskôr to manuálne prepojíme s pružinou vo vrstve infraštruktúry.

Pretože doménová vrstva je úplne oddelená z aplikačných a infraštruktúrnych vrstiev, mymôcť tiež otestujte to nezávisle:

trieda DomainOrderServiceUnitTest {private OrderRepository orderRepository; testovaná súkromná služba DomainOrderService; @BeforeEach void setUp () {orderRepository = falošný (OrderRepository.class); testované = nový DomainOrderService (orderRepository); } @Test void shouldCreateOrder_thenSaveIt () {final Product product = new Product (UUID.randomUUID (), BigDecimal.TEN, "productName"); final UUID id = checked.createOrder (produkt); overiť (orderRepository) .save (any (Order.class)); assertNotNull (id); }}

5. Aplikačná vrstva

V tejto časti implementujeme aplikačnú vrstvu. Umožníme používateľovi komunikovať s našou aplikáciou prostredníctvom RESTful API.

Preto vytvorme OrderController:

@RestController @RequestMapping ("/ commands") verejná trieda OrderController {private OrderService orderService; @Autowired public OrderController (OrderService orderService) {this.orderService = orderService; } @PostMapping CreateOrderResponse createOrder (požiadavka @RequestBody CreateOrderRequest) {UUID id = orderService.createOrder (request.getProduct ()); vrátiť nový CreateOrderResponse (id); } @PostMapping (value = "/ {id} / products") void addProduct (@PathVariable UUID id, @RequestBody AddProductRequest request) {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); }}

Tento jednoduchý ovládač Spring Rest je zodpovedný za organizáciu vykonávania logiky domény.

Tento radič prispôsobuje vonkajšie RESTful rozhranie našej doméne. Robí to volaním vhodných metód z OrderService (port).

6. Vrstva infraštruktúry

Vrstva infraštruktúry obsahuje logiku potrebnú na spustenie aplikácie.

Preto začneme vytvorením konfiguračných tried. Najskôr implementujme triedu, ktorá zaregistruje naše OrderService ako jarná fazuľa:

@Configuration public class BeanConfiguration {@Bean OrderService orderService (OrderRepository orderRepository) {vrátiť nový DomainOrderService (orderRepository); }}

Ďalej vytvoríme konfiguráciu zodpovednú za povolenie jarných dátových úložísk, ktoré použijeme:

@EnableMongoRepositories (basePackageClasses = SpringDataMongoOrderRepository.class) verejná trieda MongoDBConfiguration {}

Použili sme basePackageClasses pretože tieto úložiská môžu byť iba vo vrstve infraštruktúry. Preto nie je dôvod, aby Spring skenoval celú aplikáciu. Ďalej táto trieda môže obsahovať všetko, čo súvisí s nadviazaním spojenia medzi MongoDB a našou aplikáciou.

Nakoniec implementujeme OrderRepository z doménovej vrstvy. Použijeme naše SpringDataMongoOrderRepository v našej implementácii:

@Component verejná trieda MongoDbOrderRepository implementuje OrderRepository {private SpringDataMongoOrderRepository orderRepository; @Autowired public MongoDbOrderRepository (SpringDataMongoOrderRepository orderRepository) {this.orderRepository = orderRepository; } @Override public Voliteľné findById (UUID id) {návrat orderRepository.findById (id); } @Override public void save (objednávka objednávky) {orderRepository.save (objednávka); }}

Táto implementácia ukladá naše objednať v MongoDB. V šesťuholníkovej architektúre je táto implementácia tiež adaptérom.

7. Výhody

Prvou výhodou tohto prístupu je, že my samostatná práca pre každú vrstvu. Môžeme sa sústrediť na jednu vrstvu bez ovplyvnenia ostatných.

Okrem toho sú prirodzene ľahšie pochopiteľné, pretože sa každý z nich zameriava na svoju logiku.

Ďalšou veľkou výhodou je, že sme izolovali logiku domény od všetkého ostatného. Doménová časť obsahuje iba obchodnú logiku a dá sa ľahko presunúť do iného prostredia.

V skutočnosti zmeňme vrstvu infraštruktúry na použitie Cassandry ako databázy:

@ Komponenta verejná trieda CassandraDbOrderRepository implementuje OrderRepository {súkromné ​​finále SpringDataCassandraOrderRepository orderRepository; @Autowired public CassandraDbOrderRepository (SpringDataCassandraOrderRepository orderRepository) {this.orderRepository = orderRepository; } @Override public Voliteľné findById (UUID id) {Voliteľné orderEntity = orderRepository.findById (id); if (orderEntity.isPresent ()) {return Optional.of (orderEntity.get () .toOrder ()); } else {return Optional.empty (); }} @Override public void save (objednávka objednávky) {orderRepository.save (nová OrderEntity (objednávka)); }}

Na rozdiel od MongoDB teraz používame OrderEntity zachovať doménu v databáze.

Ak k našim pridáme anotácie špecifické pre technológiu objednať objekt doménypotom porušujeme oddelenie medzi infraštruktúrnymi a doménovými vrstvami.

Úložisko prispôsobuje doménu našim pretrvávajúcim potrebám.

Poďme o krok ďalej a transformujme našu aplikáciu RESTful na aplikáciu príkazového riadku:

@Component public class CliOrderController {private static final Logger LOG = LoggerFactory.getLogger (CliOrderController.class); súkromná konečná OrderService orderService; @Autowired public 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 ("Zadanie novej objednávky s dvoma produktmi"); Produkt mobilePhone = nový Produkt (UUID.randomUUID (), BigDecimal.valueOf (200), „mobile“); Produkt Razor = nový produkt (UUID.randomUUID (), BigDecimal.valueOf (50), "Razor"); LOG.info ("Vytvorenie objednávky pomocou mobilného telefónu"); UUID orderId = orderService.createOrder (mobilePhone); LOG.info ("Pridanie žiletky k objednávke"); orderService.addProduct (orderId, razor); vrátiť id objednávky; }}

Na rozdiel od predtým sme teraz pevne zapojili skupinu preddefinovaných akcií, ktoré interagujú s našou doménou. To by sme mohli použiť napríklad na naplnenie našej aplikácie falošnými údajmi.

Aj keď sme úplne zmenili účel aplikácie, nedotkli sme sa doménovej vrstvy.

8. Záver

V tomto článku sme sa naučili, ako rozdeliť logiku súvisiacu s našou aplikáciou do konkrétnych vrstiev.

Najskôr sme definovali tri hlavné vrstvy: aplikáciu, doménu a infraštruktúru. Potom sme popísali, ako ich naplniť, a vysvetlili sme výhody.

Potom sme prišli s implementáciou pre každú vrstvu:

Nakoniec sme vymenili vrstvy aplikácií a infraštruktúry bez toho, aby to malo dopad na doménu.

Ako vždy, kód týchto príkladov je k dispozícii na GitHub.

Java dole

Práve som oznámil nové Naučte sa jar kurz zameraný na základy jari 5 a Spring Boot 2:

>> SKONTROLUJTE KURZ

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