業務程式碼程式設計陷阱案例 - jaxenter

banq發表於2020-02-18

當我們開始編寫軟體時,我們總是希望有一個好的設計。我們閱讀書籍,運用最佳實踐,最後,我們常常一團糟。根據我在一家定製軟體開發公司的經驗,我每天必須處理此類程式碼,尤其是在某些舊系統上工作時。

造成這種情況的原因多種多樣,我將嘗試在一系列文章中以一些實際的方式來探討其中的一些原因。在我的第一個示例中,我將說明為什麼簡單的軟體會演變成一場噩夢,並建議進行一些改進。我將只專注於處理業務邏輯的服務層。

讓我們從一個簡單的儲存應用程式開始。我們擁有帶有服務,儲存庫的產品資源,並且我們可以執行我們認為需要的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);
    }
}

還會有其他一些東西,例如DTO到實體的對映,控制器等。但是正如我所說的,我們將考慮將它們編寫為簡化起見。我們的產品實體是簡單的Java Bean,我們的儲存庫儲存在正確的資料庫表中。然後,我們得到另一個要求,即我們還將建立一個線上商店,並且需要一種下訂單的方法。因此,我們新增了快速訂購服務來滿足我們仍然很簡單的要求:

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

它簡單,易讀且有效!然後,下訂單時就需要更新庫存中的產品的新要求。我們這樣做:

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);
    }
}

我們又碰到三個新需求:

  • 我們需要致電運輸服務將該產品運送到一個地址
  • 如果沒有足夠的庫存來履行訂單,則丟擲一個錯誤
  • 如果產品的可用數量低於最低數量以進行重新庫存。

結果如下:

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
        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);
    }
}

我知道這可能是一個極端的例子,但是我確信我們在專案中已經看到了類似的程式碼。這樣做有多個問題–責任z職責共擔,與其他領域的邏輯和基礎架構混淆在以前等等。如果這是一個真實的商店,那麼接單的人就像總經理–照顧一切,從實際訂購庫存維護和交付。

更好的版本

讓我們嘗試以不同的方式處理相同的情況。我將從訂購服務開始。為什麼我們呼叫方法saveOrder?因為我們將其視為開發人員,而不是從業務角度來看。我們開發人員的想法通常是資料庫驅動的(或REST驅動的),我們將我們的軟體視為一系列CRUD操作。通常,當我們閱讀有關領域驅動設計的書籍時,會提到通用語言-開發人員和使用者之間的通用語言。如果我們嘗試在我們的程式碼中為業務建模,那麼為什麼不使用正確的術語。我們可以將初始程式碼更改為:

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

使用placeOrder替代了原理的saveOrder方法名。

進行很小的更改,但即使那樣也會使其更具可讀性。這是業務層,而不是資料庫層–我們去商店時下訂單place order,但不儲存訂單save order。然後,當其他需求出現時,而不是開始使用帶有CRUD操作的現有服務來編碼它們,我們可以嘗試重新建立業務模型。我們詢問業務人員,他們告訴我們,下訂單時,接單的人員會致電庫存部門,詢問他們產品是否可用,然後進行儲備並致電帶有預定編號和地址的交貨人員,以便他們裝運它。是什麼阻止我們在程式碼中執行相同的操作?

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);
    }
}

在我看來,它看起來更加乾淨,代表了實際商店中發生的事件的順序。訂單服務不需要知道產品如何工作或運輸如何工作。它只是使用完成工作所需的方法。我們也需要修改其他服務:

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);
    }
}

產品服務提供了其他服務要使用的兩種方法,但對它們的結構一無所知。它不關心訂單,發貨等。當產品需要補貨以及產品數量是否足夠時,邏輯就在實際產品內部。

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;
    }
}

裝貨服務:

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);
    }
}

我並不是說這是最好的設計,但我認為它要乾淨得多。每個服務都照顧自己的領域,並且對其他服務瞭解得最少。實際的實體不僅是資料持有者,而且還攜帶與之相關的邏輯,因此服務不需要直接修改其內部狀態。在我看來,最有價值的是程式碼真正代表了業務運作方式。

 

相關文章