避免CRUD思維洩漏領域邏輯 - mscharhag

banq發表於2021-11-08

許多軟體架構試圖將域邏輯與應用程式的其他部分分開。為了遵循這種做法,我們總是需要知道什麼是領域邏輯,什麼不是。不幸的是,這並不總是那麼容易分開。如果我們做出錯誤的決定,領域邏輯很容易洩漏到其他元件和層中。

我們將通過檢視使用六邊形應用程式架構的示例來解決這個問題。

 

假設一個商店系統將新訂單釋出到訊息系統(如 Kafka)。我們的產品負責人現在告訴我們,我們必須監聽這些訂單事件並將相應的訂單儲存在資料庫中。

使用六邊形體系結構,在介面卡內實現與訊息傳遞系統的整合。所以,我們從一個簡單的介面卡實現開始,它監聽 Kafka 事件:

@AllArgsConstructor
public class KafkaAdapter {
    private final SaveOrderUseCase saveOrderUseCase;
 
    @KafkaListener(topic = ...)
    public void onNewOrderEvent(NewOrderKafkaEvent event) {
        Order order = event.getOrder();
        saveOrderUseCase.saveOrder(order);
    }
}

的@AllArgsConstructor註釋會生成一個建構函式,該建構函式接受每個欄位(此處為saveOrderUseCase)作為引數。

介面卡將訂單的儲存委託給UseCase實現。

UseCase是我們領域核心的一部分,與領域模型一起實現領域邏輯。我們的簡單示例用例如下所示:

@AllArgsConstructor
public class SaveOrderUseCase {
    private final SaveOrderPort saveOrderPort;
 
    public void saveOrder(Order order) {
        saveOrderPort.saveOrder(order);
    }
}

這裡沒什麼特別的。我們只是使用傳出埠介面來儲存傳遞的訂單。

雖然所示方法可能工作正常,但我們這裡有一個重大問題:我們的業務邏輯已洩漏到 Adapter 介面卡實現中。

也許你想知道:什麼業務邏輯?

 

我們有一個簡單的業務規則要實現:每次檢索到新訂單時,都應該將其持久化。在我們當前的實現中,此規則由介面卡實現,而我們的業務層(用例)僅提供通用儲存操作。

現在假設,一段時間後,新的需求到來:每次檢索到新訂單時,都應將一條訊息寫入審計日誌。

對於我們當前的實現,我們無法在SaveOrderUseCase 中寫入審計日誌訊息。顧名思義,用例用於儲存訂單而不是檢索新訂單,因此可能被其他元件使用。因此,在此處新增稽核日誌訊息可能會產生不良副作用。

也許解決方案很簡單:我們在介面卡中寫入審計日誌訊息:

@AllArgsConstructor

public class KafkaAdapter {
 
    private final SaveOrderUseCase saveOrderUseCase;
    private final AuditLog auditLog;
 
    @KafkaListener(topic = ...)
    public void onNewOrderEvent(NewOrderKafkaEvent event) {
        Order order = event.getOrder();
        saveOrderUseCase.saveOrder(order);
        auditLog.write("New order retrieved, id: " + order.getId());
    }
}

而現在我們讓情況變得更糟。更多的業務邏輯已洩漏到介面卡中。

如果auditLog物件將訊息寫入資料庫,我們也可能搞砸了事務處理,這通常不會在傳入介面卡中處理。

 

使用更具體的域操作

這裡的核心問題是通用的SaveOrderUseCase。我們應該提供更具體的 UseCase 實現,而不是為介面卡提供通用的儲存操作。

例如,我們可以建立一個接受新檢索到的訂單的NewOrderRetrievedUseCase:

@AllArgsConstructor
public class NewOrderRetrievedUseCase {
    private final SaveOrderPort saveOrderPort;
    private final AuditLog auditLog;
 
    @Transactional
    public void onNewOrderRetrieved(Order newOrder) {
        saveOrderPort.saveOrder(order);
        auditLog.write("New order retrieved, id: " + order.getId());
    }
}

現在這兩個業務規則都在用例中實現。我們的介面卡實現現在只負責對映傳入資料並將其傳遞給用例:

@AllArgsConstructor
public class KafkaAdapter {
    private final NewOrderRetrievedUseCase newOrderRetrievedUseCase;
 
    @KafkaListener(topic = ...)
    public void onNewOrderEvent(NewOrderKafkaEvent event) {
        NewOrder newOrder = event.toNewOrder();
        newOrderRetrievedUseCase.onNewOrderRetrieved(newOrder);
    }
}

這種變化似乎只是一個很小的差異。但是,對於未來的需求,我們現在有一個特定的位置來處理我們業務層中的傳入訂單。否則,隨著新需求的出現,我們將更多的業務邏輯洩漏到不應該定位的地方的可能性很高。

像這樣的洩漏尤其經常發生在域層中過於通用的建立、儲存/更新和刪除操作。因此,在實施業務操作時儘量做到非常具體。

 

相關文章