Menyusun Lapisan Menggunakan Senibina Hexagonal, DDD, dan Spring

Java Teratas

Saya baru sahaja mengumumkan kursus Learn Spring yang baru , yang berfokus pada asas-asas Spring 5 dan Spring Boot 2:

>> SEMAK KURSUS

1. Gambaran keseluruhan

Dalam tutorial ini, kami akan menerapkan aplikasi Spring menggunakan DDD. Selain itu, kami akan menyusun lapisan dengan bantuan Senibina Hexagonal.

Dengan pendekatan ini, kita dapat menukar pelbagai lapisan aplikasi dengan mudah.

2. Senibina Heksagon

Senibina heksagon adalah model merancang aplikasi perisian di sekitar logik domain untuk mengasingkannya dari faktor luaran.

Logik domain ditentukan dalam inti perniagaan, yang akan kita panggil bahagian dalam, selebihnya adalah bahagian luar. Akses ke logik domain dari luar tersedia melalui port dan penyesuai.

3. Prinsip

Pertama, kita harus menentukan prinsip untuk membahagikan kod kita. Seperti yang telah dijelaskan secara ringkas, seni bina heksagon menentukan bahagian dalam dan bahagian luar .

Yang akan kita buat ialah membahagikan aplikasi kita kepada tiga lapisan; aplikasi (luar), domain (dalam), dan infrastruktur (luar):

Melalui lapisan aplikasi, pengguna atau program lain berinteraksi dengan aplikasi. Kawasan ini harus mengandungi perkara seperti antara muka pengguna, pengawal RESTful, dan perpustakaan bersiri JSON. Ini merangkumi apa - apa yang memperlihatkan kemasukan ke aplikasi kami dan mengatur pelaksanaan logik domain.

Di lapisan domain, kami menyimpan kod yang menyentuh dan menerapkan logik perniagaan . Ini adalah teras aplikasi kami. Selain itu, lapisan ini harus diasingkan dari bahagian aplikasi dan bahagian infrastruktur. Selain itu, ia juga harus mengandungi antara muka yang menentukan API untuk berkomunikasi dengan bahagian luaran, seperti pangkalan data, yang domainnya berinteraksi.

Terakhir, lapisan infrastruktur adalah bahagian yang mengandungi apa sahaja yang perlu digunakan oleh aplikasi seperti konfigurasi pangkalan data atau konfigurasi Spring. Selain itu, ia juga menerapkan antaramuka yang bergantung pada infrastruktur dari lapisan domain.

4. Lapisan Domain

Mari mulakan dengan menerapkan lapisan teras kami, yang merupakan lapisan domain.

Pertama, kita harus membuat kelas Pesanan :

public class Order { private UUID id; private OrderStatus status; private List orderItems; private BigDecimal price; public Order(UUID id, Product product) { this.id = id; this.orderItems = new ArrayList(Arrays.astList(new OrderItem(product))); this.status = OrderStatus.CREATED; this.price = product.getPrice(); } public void complete() { validateState(); this.status = OrderStatus.COMPLETED; } public void addOrder(Product product) { validateState(); validateProduct(product); orderItems.add(new OrderItem(product)); price = price.add(product.getPrice()); } public void removeOrder(UUID id) { validateState(); final OrderItem orderItem = getOrderItem(id); orderItems.remove(orderItem); price = price.subtract(orderItem.getPrice()); } // getters }

Ini adalah akar agregat kami . Segala yang berkaitan dengan logik perniagaan kami akan melalui kelas ini. Selain itu, Pesanan bertanggungjawab untuk menjaga dirinya dalam keadaan yang betul:

  • Pesanan hanya boleh dibuat dengan ID yang diberikan dan berdasarkan satu Produk - pembina itu sendiri juga membuat pesanan dengan status CREATED
  • Setelah pesanan selesai, menukar OrderItem tidak mustahil
  • Mustahil untuk mengubah Pesanan dari luar objek domain, seperti dengan setter

Selanjutnya, kelas Order juga bertanggungjawab untuk membuat OrderItemnya .

Mari buat kelas OrderItem kemudian:

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

Seperti yang kita lihat, OrderItem dibuat berdasarkan Produk . Ini menyimpan rujukan kepadanya dan menyimpan harga semasa Produk .

Seterusnya, kami akan membuat antara muka repositori ( port dalam Hexagonal Architecture). Pelaksanaan antara muka akan berada di lapisan infrastruktur:

public interface OrderRepository { Optional findById(UUID id); void save(Order order); }

Terakhir, kita harus memastikan bahawa Pesanan akan selalu disimpan setelah setiap tindakan. Untuk melakukannya, kami akan menentukan Perkhidmatan Domain, yang biasanya mengandungi logik yang tidak boleh menjadi sebahagian daripada root kami :

public class DomainOrderService implements 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(order); return order.getId(); } @Override public void addProduct(UUID id, Product product) { Order order = getOrder(id); order.addOrder(product); orderRepository.save(order); } @Override public void completeOrder(UUID id) { Order order = getOrder(id); order.complete(); orderRepository.save(order); } @Override public void deleteProduct(UUID id, UUID productId) { Order order = getOrder(id); order.removeOrder(productId); orderRepository.save(order); } private Order getOrder(UUID id) { return orderRepository .findById(id) .orElseThrow(RuntimeException::new); } }

Dalam seni bina heksagon, perkhidmatan ini adalah penyesuai yang menggunakan port. Selain itu, kami tidak akan mendaftarkannya sebagai biji kacang kerana, dari perspektif domain, ini ada di bahagian dalam, dan konfigurasi Spring berada di luar. Kami akan memasangnya secara manual dengan Spring di lapisan infrastruktur sedikit kemudian.

Kerana lapisan domain sepenuhnya dipisahkan dari lapisan aplikasi dan infrastruktur, kami juga dapat mengujinya secara bebas :

class DomainOrderServiceUnitTest { private OrderRepository orderRepository; private DomainOrderService tested; @BeforeEach void setUp() { orderRepository = mock(OrderRepository.class); tested = new DomainOrderService(orderRepository); } @Test void shouldCreateOrder_thenSaveIt() { final Product product = new Product(UUID.randomUUID(), BigDecimal.TEN, "productName"); final UUID id = tested.createOrder(product); verify(orderRepository).save(any(Order.class)); assertNotNull(id); } }

5. Lapisan Aplikasi

In this section, we'll implement the application layer. We'll allow the user to communicate with our application via a RESTful API.

Therefore, let's create the OrderController:

@RestController @RequestMapping("/orders") public class OrderController { private OrderService orderService; @Autowired public OrderController(OrderService orderService) { this.orderService = orderService; } @PostMapping CreateOrderResponse createOrder(@RequestBody CreateOrderRequest request) { UUID id = orderService.createOrder(request.getProduct()); return new 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); } }

This simple Spring Rest controller is responsible for orchestrating the execution of domain logic.

This controller adapts the outside RESTful interface to our domain. It does it by calling the appropriate methods from OrderService (port).

6. Infrastructure Layer

The infrastructure layer contains the logic needed to run the application.

Therefore, we'll start by creating the configuration classes. Firstly, let's implement a class that will register our OrderService as a Spring bean:

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

Next, let's create the configuration responsible for enabling the Spring Data repositories we'll use:

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

We have used the basePackageClasses property because those repositories can only be in the infrastructure layer. Hence, there's no reason for Spring to scan the whole application. Furthermore, this class can contain everything related to establishing a connection between MongoDB and our application.

Lastly, we'll implement the OrderRepository from the domain layer. We'll use our SpringDataMongoOrderRepository in our implementation:

@Component public class MongoDbOrderRepository implements OrderRepository { private SpringDataMongoOrderRepository orderRepository; @Autowired public MongoDbOrderRepository(SpringDataMongoOrderRepository orderRepository) { this.orderRepository = orderRepository; } @Override public Optional findById(UUID id) { return orderRepository.findById(id); } @Override public void save(Order order) { orderRepository.save(order); } }

This implementation stores our Order in MongoDB. In a hexagonal architecture, this implementation is also an adapter.

7. Benefits

The first advantage of this approach is that we separate work for each layer. We can focus on one layer without affecting others.

Furthermore, they're naturally easier to understand because each of them focuses on its logic.

Another big advantage is that we've isolated the domain logic from everything else. The domain part only contains business logic and can be easily moved to a different environment.

In fact, let's change the infrastructure layer to use Cassandra as a database:

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

Unlike MongoDB, we now use an OrderEntity to persist the domain in the database.

If we add technology-specific annotations to our Order domain object, then we violate the decoupling between infrastructure and domain layers.

The repository adapts the domain to our persistence needs.

Let's go a step further and transform our RESTful application into a command-line application:

@Component public class CliOrderController { private static final Logger LOG = LoggerFactory.getLogger(CliOrderController.class); private final 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("Placing a new order with two products"); Product mobilePhone = new Product(UUID.randomUUID(), BigDecimal.valueOf(200), "mobile"); Product razor = new Product(UUID.randomUUID(), BigDecimal.valueOf(50), "razor"); LOG.info("Creating order with mobile phone"); UUID orderId = orderService.createOrder(mobilePhone); LOG.info("Adding a razor to the order"); orderService.addProduct(orderId, razor); return orderId; } }

Unlike before, we now have hardwired a set of predefined actions that interact with our domain. We could use this to populate our application with mocked data for example.

Even though we completely changed the purpose of the application, we haven't touched the domain layer.

8. Conclusion

In this article, we've learned how to separate the logic related to our application into specific layers.

Pertama, kami menentukan tiga lapisan utama: aplikasi, domain, dan infrastruktur. Selepas itu, kami menerangkan cara mengisinya dan menerangkan kelebihannya.

Kemudian, kami membuat pelaksanaan untuk setiap lapisan:

Akhirnya, kami menukar lapisan aplikasi dan infrastruktur tanpa mempengaruhi domain.

Seperti biasa, kod untuk contoh ini boleh didapati di GitHub.

Bahagian bawah Java

Saya baru sahaja mengumumkan kursus Learn Spring yang baru , yang berfokus pada asas-asas Spring 5 dan Spring Boot 2:

>> SEMAK KURSUS