Organiziranje slojeva pomoću heksagonalne arhitekture, DDD-a i proljeća

Java Top

Upravo sam najavio novo Uči proljeće tečaj, usredotočen na osnove Spring 5 i Spring Boot 2:

>> PROVJERITE TEČAJ

1. Pregled

U ovom uputstvu implementirat ćemo proljetnu aplikaciju pomoću DDD-a. Uz to, slojeve ćemo organizirati uz pomoć Heksagonalne arhitekture.

Ovim pristupom možemo lako razmijeniti različite slojeve aplikacije.

2. Heksagonalna arhitektura

Heksagonalna arhitektura uzor je dizajniranje softverskih aplikacija oko logike domene da ga izoliraju od vanjskih čimbenika.

Logika domene navedena je u poslovnoj jezgri, koju ćemo nazvati unutarnjim dijelom, a ostatak su vanjski dijelovi. Pristup logici domene izvana dostupan je putem priključaka i adaptera.

3. Načela

Prvo, trebali bismo definirati načela za podjelu našeg koda. Kao što je već kratko objašnjeno, šestougaona arhitektura definira unutarnji i vanjski dio.

Ono što ćemo umjesto toga podijeliti našu aplikaciju u tri sloja; aplikacija (izvana), domena (iznutra) i infrastruktura (izvana):

Kroz aplikacijski sloj, korisnik ili bilo koji drugi program komunicira uz prijavu. Ovo bi područje trebalo sadržavati stvari poput korisničkog sučelja, RESTful kontrolera i JSON knjižnica za serializaciju. Uključuje sve koji izlaže ulaz našoj aplikaciji i orkestrira izvršavanje logike domene.

U sloju domene zadržavamo kôd koji dodiruje i implementira poslovnu logiku. Ovo je srž naše aplikacije. Uz to, ovaj bi sloj trebao biti izoliran i od aplikacijskog i od infrastrukturnog dijela. Povrh toga, trebao bi sadržavati i sučelja koja definiraju API za komunikaciju s vanjskim dijelovima, poput baze podataka, s kojom domena komunicira.

Na kraju, Infrastrukturni sloj je dio koji sadrži sve što aplikacija treba za rad kao što je konfiguracija baze podataka ili proljetna konfiguracija. Osim toga, također implementira sučelja ovisna o infrastrukturi s sloja domene.

4. Sloj domene

Počnimo s implementacijom našeg osnovnog sloja, koji je sloj domene.

Prvo, trebali bismo stvoriti Narudžba razred:

narudžba javne klase {privatni UUID id; status privatnog OrderStatusa; privatni popis orderItems; privatna cijena BigDecimal; javni nalog (UUID id, proizvod proizvoda) {this.id = id; this.orderItems = novi ArrayList (Arrays.astList (novi OrderItem (proizvod))); this.status = Status narudžbe.STVORENO; this.price = product.getPrice (); } javna praznina dovršena () {validateState (); this.status = OrderStatus.COMPLETED; } javna void addOrder (proizvod proizvoda) {validateState (); validateProduct (proizvod); orderItems.add (novi OrderItem (proizvod)); cijena = cijena.doda (product.getPrice ()); } javna void removeOrder (UUID id) {validateState (); konačni OrderItem orderItem = getOrderItem (id); orderItems.remove (orderItem); price = price.subtract (orderItem.getPrice ()); } // dobivači}

Ovo je naš skupni korijen. Sve što je povezano s našom poslovnom logikom proći će kroz ovu klasu. Dodatno, Narudžba odgovoran je za održavanje samog sebe u ispravnom stanju:

  • Narudžbu je moguće stvoriti samo s danim ID-om i na temelju jedne Proizvod - sam konstruktor također uvodi red s STVORENO status
  • Nakon što je narudžba dovršena, mijenja se Artikl narudžbes je nemoguće
  • Nemoguće je promijeniti Narudžba izvan objekta domene, kao kod postavljača

Nadalje, Narudžba klasa je također odgovorna za stvaranje svog Artikl narudžbe.

Stvorimo Artikl narudžbe razred tada:

javna klasa OrderItem {private UUID productId; privatna cijena BigDecimal; javni OrderItem (proizvod proizvoda) {this.productId = product.getId (); this.price = product.getPrice (); } // dobivači}

Kao što vidimo, Artikl narudžbe stvoren je na temelju a Proizvod. Zadržava referencu i pohranjuje trenutnu cijenu Proizvod.

Zatim ćemo stvoriti sučelje spremišta (a luka u Heksagonalnoj arhitekturi). Implementacija sučelja bit će u sloju infrastrukture:

javno sučelje OrderRepository {Izborno findById (UUID id); void save (Narudžba); }

Na kraju, trebali bismo osigurati da Narudžba uvijek će biti spremljeni nakon svake akcije. Napraviti to, definirat ćemo uslugu domene koja obično sadrži logiku koja ne može biti dio našeg korijena:

javna klasa DomainOrderService implementira OrderService {privatni konačni OrderRepository orderRepository; javna DomainOrderService (OrderRepository orderRepository) {this.orderRepository = orderRepository; } @Override javni UUID createOrder (proizvod proizvoda) {Narudžba = nova narudžba (UUID.randomUUID (), proizvod); orderRepository.save (narudžba); povrat naloga.getId (); } @Override public void addProduct (UUID id, proizvod proizvoda) {Order order = getOrder (id); order.addOrder (proizvod); orderRepository.save (narudžba); } @Override public void completeOrder (UUID id) {Order order = getOrder (id); Narudžba završena(); orderRepository.save (narudžba); } @Override public void deleteProduct (UUID id, UUID productId) {Order order = getOrder (id); order.removeOrder (productId); orderRepository.save (narudžba); } privatna narudžba getOrder (UUID id) {return orderRepository .findById (id) .orElseThrow (RuntimeException :: new); }}

U heksagonalnoj arhitekturi, ova je usluga adapter koji implementira priključak. Dodatno, nećemo ga registrirati kao proljetni grahjer je to iz perspektive domene u unutarnjem dijelu, a Spring konfiguracija izvana. Ručno ćemo ga povezati s Springom u infrastrukturnom sloju nešto kasnije.

Budući da je sloj domene potpuno nevezan iz aplikacijskih i infrastrukturnih slojeva, milimenka također testirajte ga samostalno:

klasa DomainOrderServiceUnitTest {private OrderRepository orderRepository; testiran privatni DomainOrderService; @BeforeEach void setUp () {orderRepository = mock (OrderRepository.class); testirano = nova DomainOrderService (orderRepository); } @Test void shouldCreateOrder_thenSaveIt () {final product product = novi proizvod (UUID.randomUUID (), BigDecimal.TEN, "productName"); konačni UUID id = testiran.createOrder (proizvod); verify (orderRepository) .save (bilo koji (Order.class)); assertNotNull (id); }}

5. Razina aplikacije

U ovom ćemo odjeljku implementirati aplikacijski sloj. Dopustit ćemo korisniku da komunicira s našom aplikacijom putem RESTful API-ja.

Stoga, kreirajmo Kontrolor narudžbe:

@RestController @RequestMapping ("/ orders") javna klasa OrderController {private OrderService orderService; @Autowired javni nalog za upravljanje (OrderService orderService) {this.orderService = orderService; } @PostMapping CreateOrderResponse createOrder (@RequestBody CreateOrderRequest zahtjev) {UUID id = orderService.createOrder (request.getProduct ()); vrati novi CreateOrderResponse (id); } @PostMapping (value = "/ {id} / products") void addProduct (@PathVariable UUID id, @RequestBody AddProductRequest zahtjev) {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); }}

Ovaj jednostavni kontroler Spring Rest odgovoran je za orkestriranje izvršavanja logike domene.

Ovaj kontroler prilagođava vanjsko sučelje RESTful našoj domeni. To čini pozivanjem odgovarajućih metoda iz OrderService (luka).

6. Infrastrukturni sloj

Infrastrukturni sloj sadrži logiku potrebnu za pokretanje aplikacije.

Stoga ćemo započeti s izradom klasa konfiguracije. Prvo, implementiramo klasu koja će registrirati našu OrderService kao proljetni grah:

@Configuration javna klasa BeanConfiguration {@Bean OrderService orderService (OrderRepository orderRepository) {return new DomainOrderService (orderRepository); }}

Dalje, kreirajmo konfiguraciju odgovornu za omogućavanje spremišta Spring Data koje ćemo koristiti:

@EnableMongoRepositories (basePackageClasses = SpringDataMongoOrderRepository.class) javna klasa MongoDBConfiguration {}

Koristili smo basePackageClasses svojstvo jer ta spremišta mogu biti samo u sloju infrastrukture. Stoga, Spring nema razloga skenirati cijelu aplikaciju. Nadalje, ova klasa može sadržavati sve što se odnosi na uspostavljanje veze između MongoDB-a i naše aplikacije.

Na kraju, implementirat ćemo Spremište narudžbi iz sloja domene. Koristit ćemo svoje SpringDataMongoOrderRepository u našoj provedbi:

@Component javna klasa MongoDbOrderRepository implementira OrderRepository {private SpringDataMongoOrderRepository orderRepository; @Autowired javni MongoDbOrderRepository (SpringDataMongoOrderRepository orderRepository) {this.orderRepository = orderRepository; } @Override public Izborni findById (UUID id) {return orderRepository.findById (id); } @Override public void save (Order order) {orderRepository.save (order); }}

Ova implementacija pohranjuje naše Narudžba u MongoDB-u. U heksagonalnoj arhitekturi, ova implementacija je ujedno i adapter.

7. Prednosti

Prva prednost ovog pristupa je što mi odvojeni rad za svaki sloj. Možemo se usredotočiti na jedan sloj, a da ne utječemo na druge.

Nadalje, prirodno ih je lakše razumjeti jer se svaki od njih usredotočuje na svoju logiku.

Druga velika prednost je što smo izolirali logiku domene od svega ostalog. Dio domene sadrži samo poslovnu logiku i može se lako premjestiti u drugo okruženje.

Zapravo, promijenimo sloj infrastrukture da koristimo Cassandru kao bazu podataka:

@Component javna klasa CassandraDbOrderRepository implementira OrderRepository {private final SpringDataCassandraOrderRepository orderRepository; @Autowired javni CassandraDbOrderRepository (SpringDataCassandraOrderRepository orderRepository) {this.orderRepository = orderRepository; } @Override public Izborni findById (UUID id) {Izborni orderEntity = orderRepository.findById (id); if (orderEntity.isPresent ()) {return Neobvezno.of (orderEntity.get () .toOrder ()); } else {return Neobvezno.empty (); }} @Override public void save (Order order) {orderRepository.save (new OrderEntity (order)); }}

Za razliku od MongoDB, sada koristimo OrderEntity zadržati domenu u bazi podataka.

Ako na naše dodamo napomene specifične za tehnologiju Narudžba objekt domene, onda kršimo razdvajanje između infrastrukture i slojeva domene.

Spremište prilagođava domenu našim potrebama upornosti.

Idemo korak dalje i transformiramo našu aplikaciju RESTful u aplikaciju naredbenog retka:

@Component javna klasa CliOrderController {private static final Logger LOG = LoggerFactory.getLogger (CliOrderController.class); privatni konačni OrderService orderService; @Autowired javni CliOrderController (OrderService orderService) {this.orderService = orderService; } javna praznina createCompleteOrder () {LOG.info ("<>"); UUID orderId = createOrder (); orderService.completeOrder (orderId); } javna praznina createIncompleteOrder () {LOG.info ("<>"); UUID orderId = createOrder (); } private UUID createOrder () {LOG.info ("Postavljanje nove narudžbe s dva proizvoda"); Proizvod mobilePhone = novi proizvod (UUID.randomUUID (), BigDecimal.valueOf (200), "mobile"); Britva proizvoda = novi proizvod (UUID.randomUUID (), BigDecimal.valueOf (50), "britva"); LOG.info ("Izrada narudžbe s mobitelom"); UUID orderId = orderService.createOrder (mobilni telefon); LOG.info ("Dodavanje žileta u narudžbu"); orderService.addProduct (orderId, britva); return orderId; }}

Za razliku od prije, sada imamo ožičeni skup unaprijed definiranih radnji koje komuniciraju s našom domenom. Ovo bismo mogli upotrijebiti za popunjavanje naše aplikacije na primjer izruganim podacima.

Iako smo potpuno promijenili svrhu aplikacije, nismo dodirnuli sloj domene.

8. Zaključak

U ovom smo članku naučili kako logiku povezanu s našom aplikacijom razdvojiti na određene slojeve.

Prvo smo definirali tri glavna sloja: aplikaciju, domenu i infrastrukturu. Nakon toga opisali smo kako ih napuniti i objasnili prednosti.

Zatim smo smislili implementaciju za svaki sloj:

Na kraju smo zamijenili slojeve aplikacija i infrastrukture bez utjecaja na domenu.

Kao i uvijek, kod za ove primjere dostupan je na GitHubu.

Dno Java

Upravo sam najavio novo Uči proljeće tečaj, usredotočen na osnove Spring 5 i Spring Boot 2:

>> PROVJERITE TEČAJ