매장에 가면 주문을 하지만 주문은 저장하지 않고 주문 저장

원제: Untangling bad code -- Pitfalls in design the application's business layer
원저자: Veselin Davidov Veselin Davidov, Dreamix의 개발 리드 및 풀 스택 개발자
원문 영어 블로그: https://jaxenter.com/ code-app-business-layer -167569.html

번역자: banq(Peng Chenyang)
중국어 번역: 비즈니스 코드 프로그래밍 함정 사례 - jaxenter(jdon.com)

영어 원문은 다음과 같습니다.

잘못된 코드 풀기
-- 애플리케이션의 비즈니스 레이어 설계 시 함정

  • 2020년 2월 14일
    베셀린 다비도프

가장 단순한 소프트웨어도 때때로 스파게티 코드에 얽혀 특히 레거시 시스템에서 탐색하기가 악몽이 될 수 있습니다. 이 기사에서는 애플리케이션의 비즈니스 계층에서 일부 잘못된 코드를 보고 더 나은 설계 사례로 수정하는 방법을 살펴봅니다. 이러한 작은 개선 사항으로 처리하기에 너무 많아지기 전에 악몽 코드를 처리하십시오.

소프트웨어 작성을 시작할 때 우리는 항상 좋은 디자인을 원합니다. 우리는 책을 읽고 모범 사례를 적용하지만 종종 결국 엉망진창으로 끝납니다. 맞춤형 소프트웨어 개발 회사에서의 경험에 비추어 볼 때, 특히 일부 레거시 시스템에서 작업할 때 이러한 코드를 매일 처리해야 합니다.

거기에는 여러 가지 이유가 있으며, 나는 그것들을 실용적인 방법으로 살펴보려는 일련의 기사에서 그 중 일부를 다루려고 노력할 것입니다. 첫 번째 예에서는 단순한 소프트웨어가 악몽으로 발전할 수 있는 이유를 보여주고 약간의 개선을 제안할 것입니다. 비즈니스 로직을 처리하는 서비스 계층에만 집중하겠습니다.

첫째, 우리가 실제로 자주 보고 쓰는 나쁜 코드

간단한 스토리지 애플리케이션부터 시작하겠습니다. 서비스, ​​리포지토리가 있는 제품 리소스가 있으며 필요하다고 생각하는 CRUD 작업을 수행할 수 있습니다. 우리의 제품 서비스는 다음과 같습니다.

public class ProductService {
    public String create(Product product) {
        return productRepository.create(product);
    }
    public String update(Product product) {
        return productRepository.update(product);
    }
    public Product get(String productId) {
        return productRepository.get(productId);
    }
    public void delete(Product product) {
        productRepository.delete(product);
    }
}

There will be some other stuff like DTO to Entity mapping, controllers etc. But as I said, we will consider them written and ready for simplicity. Our Product entity is simple java bean, our repository saves in the correct DB table. Then we get another requirement that we will also create an online store and we need a way to place orders. So, we add a quick order service to cover our still simple requirements:

public class OrderService {
    public String saveOrder(Order order) {
        return orderRepository.save(order);
    }
}

It’s simple, readable and works! Then a new requirement comes to update the products in stock when an order is placed. We do it like:

public class OrderService {
    public String saveOrder(Order order) {
        Product product=productService.get(order.getProductId());
        product.setAvailableQuantity(product.getAvailableQuantity()-order.getQuantity());
        productService.update(product);
        
        return orderRepository.save(order);
    }
}

You can probably see where I am going but this is still readable and works fine. After that, we get three more requirements. 1/ we need to call the shipping service to ship that product to an address 2/ throw an error if there is not enough stock to fulfill the order 3/ if the product’s available quantity goes under some minimum to restock. And there it goes:

public class OrderService {
    public String saveOrder(Order order) {
        Product product=productService.get(order.getProductId());
        
        //The order service works more like a product service in the following liness--笔误多加一个s
        if(product.getAvailableQuantity()<order.getQuantity()){
            throw new ProductNotAvailableException();
        }
        product.setAvailableQuantity(product.getAvailableQuantity()-order.getQuantity());
        productService.update(product);
        if(product.getAvailableQuantity()<Product.MINIMUM_STOCK_QUANTITY){
            productService.restock(product);
        }
        
        //It also needs to know how shipments are created
        Shipment shipment=new Shipment(product, order.getQuantity(), order.getAddressTo());
        shipmentService.save(shipment);
        
        return orderRepository.save(order);
    }
}

I know it might be an extreme example, but I am sure we have seen similar code in our projects. There are multiple problems with that – shared responsibilities, messing with other domains logic and infrastructure etc. If it was a real-life store, then the guy that takes the order would be like the general manager – taking care of everything from taking the actual order to stock maintenance and delivery.

Now to a slightly better version

Let’s try to handle the same scenario in a different way. I will start with the order service. Why do we call our method saveOrder? Because we look at it as developers and not from a business perspective. Our developers’ minds are often database driven (or REST driven) and we see our software as a series of CRUD operations. Usually, when we look at books for Domain-Driven Design there is a term mentioned Ubiquitous Language – common language between the developers and users. If we try to model the business in our code why not using the correct terms. We can change our initial code to:

public class OrderService {
    public String placeOrder(Order order) {
        return orderRepository.save(order);
    }
}

A small change but even that makes it more readable. It’s a business layer, not a DB layer – we place orders when we go to the store, we don’t save them. Then when the other requirements come instead of start coding them using our existing services with CRUD operations, we can try to recreate the business model. We ask the business guys and they tell us that when the order is placed the guy who took it calls the stock department and ask them if the product is available, then reserves it and calls the delivery guys with the reservation number and address so they can ship it. What stops us to do the same in our code?

public class OrderService {
    public String placeOrder(Order order) {
        String productReservationId=productService.requestProductReservation(order.getProductId, order.getQuantity());
        String shippingId=shipmentService.requestDelivery(productReservationId, order.getAddressTo());
        order.addShippingId(shippingId);
        return orderRepository.save(order);
    }
}

In my opinion, it looks much cleaner and represents the sequence of events that happen in the actual store. The order service doesn’t need to know how products work or how shipping works. It just uses the methods needed to do its job. We will need to modify the other services too:

public class ProductService {
    //Method used in Orders Service
    public String requestProductReservation(String productId, int quantity){
        Product product=productRepository.get(productId);
        product.reserve(quantity);
        productRepository.update(product);
        return createProductReservation(product, quantity);
    }
    
    private String createProductReservation(Product product, int quantity){
        ProductReservation reservation=new ProductReservation(product,quantity);
        reservation.setStatus(ReservationStatus.CREATED);
        return reservationRepository.save(reservation);
    }
    
    //Method used in Shipment Service
    public ProductReservation getProductsForDelivery(String reservationId){
        ProductReservation reservation=reservationRepository.getProductReservation(reservationId);
        reservation.getProduct(/*原文此处笔误漏了括号*/).releaseReserved(reservation.getQuantity());
        if(reservation.getProduct().needRestock()){
            this.restock(product);
        }
        reservation.setStatus(ReservationStatus.PROCESSED);
        reservationRepository.update(reservation);
    }
}

The product service exposes two methods to be used from the other services but doesn’t know anything about their structure. It doesn’t care about orders, shipments etc. The logic when a product needs restocking and if a product has enough quantity is inside the actual product.

public class Product() {
    //Fields, getters, setters etc...
    
    public void reserve(int quantity){
        if(this.availableQuantity - this.reservedQuantity > quantity){
            this.reservedQuantity+=quantity;
        } else
            throw new ProductReservationException();
    }
    public releaseReserved(int requested){
        if(this.reservedQuantity>=requested){
            this.reservedQuantity-=requested;
            this.availableQuantity-=requested;
        } else 
            throw new ProductReservationException();
    }
    public boolean needsRestock(){
        return this.availableQuantity<MINIMUM_STOCK_QUANTITY;
    }
}

And the shipment service can be something like that:

public class ShipmentService {
    public String requestDelivery(String reservationId, Address address){
        ProductReservation reservation=productService.getProductForDelivery(reservationId);
        Shipment shipment=new Shipment(reservation, address);
        return shipmentRepository.save(shipment);
    }
}

I am not saying it is the best design but I think it is much cleaner. Each service takes to care for its own domain and knows as little as possible about the others. The actual entities are not just data holders also carry the logic related to them so the service doesn’t need to modify their internal state directly. And what’s most valuable in my opinion is that the code really represents how the business works.

Conclusion

If we don’t go into the situation from the first part of the article, we should try to take our time and understand our model properly. Even if new requirements come and we are pressured by time or if it will take more time to refactor, we shouldn’t be lazy. Mixing logic from different domains in services and entities seems maintainable at first but becomes spaghetti when the project grows bigger. Just as in our real-life store example – a small online store owner can take care of everything from taking orders, stocking, delivery, and finance. But when the store grows, he won’t be able to and it will become a mess.

추천

출처blog.csdn.net/liuqun69/article/details/125275437