# Get Your Hands Dirty on Clean Architecture CH5 Use Case

[書本連結](https://learning.oreilly.com/library/view/get-your-hands/9781805128373/)

# 前言

會挑這本書看的人，應該都是關心自己負責專案的軟體架構的人。不只希望開發的軟體能滿足客戶的明確需求，也希望能滿足可維護性（maintainability）的隱性需求，以及自己對結構與美觀習慣的要求。

要能滿足上述這些要求很難，因為專案通常不會按照計畫進行。可能變因有 deadline，最後的 API 與承諾的不同，又或者我們的設計無法很好的貼合需求的變化所需。。因此完美的架構只有在一開始是賞心悅目的。但後面就各種走捷徑。現在，你回頭看你的軟體架構只剩下這些捷徑，導致交付新功能所需的時間不那麼好預估。

這種走捷徑的架構下，在跨團隊協作時，似乎在討論整合跟時程時，最簡單的方法就是派 TPM 去跟對方交涉，要求它們 follow 我們的現行規格（因為改不太動）。

但我們回顧一下，會發現對局勢的控制不那麼順利，很容易會出現以下幾種情況︰

*   TPM 實力不足，在較量中輸了。
    
*   對方抓住了一個我們給的規格的漏洞，從此它們說話就大聲了，變成我們要聽它們的指揮。
    
*   對方還需要<額外的數字>來修復 API or 設計等。
    

這些都導致了同一個結果，我們被迫盡快修改程式碼跟設計…因為 deadline 快到了。

這本書主張自己掌控軟體架構，而非任由外部因素來左右。這本書提供一些概念與作法來分享這種掌控的方式，照著書本的設計走，這樣的軟體架構就能有「彈性flexible」、「可擴展性extensible」和「適應性adaptable」。這樣的架構能幫助我們應用上述的外部因素並減輕我們的壓力。

`關鍵點來了！！` 作者寫這本書是因為它覺得目前關於 DDD 文章跟書的實用性感到失望 XD 例如 Robert C. Martin的 Clean Architecture 和 Alistair Cockburn的Hexagonal Architecture。都只有提供概念，但沒說怎麼運用 。

原文：

> I wrote this book because I was disappointed with the practicality of the resources available on domain-centric architecture styles, such as Robert C. Martin’s Clean Architecture and Alistair Cockburn’s Hexagonal Architecture. Many books and online resources explain valuable concepts but not how we can actually implement them.

因為這些架構風格可能都有不只一種的實做方式。（所以該書就是提供其中一種作者覺得的實做方式。）

# Ch5 Implementing a Use Case

## Use Case 的基本組成

1.  Take the input
    
2.  Validate the business rules
    
3.  Manipulate the model state
    
4.  Return the output
    

![](https://cdn.hashnode.com/uploads/covers/6420f5cbbdbe7d697133d12a/b544d775-803d-491e-b764-c24c2e8565ba.png align="center")

作者說，你有發現它並沒有把第一步命名成`Validate input`嘛？ Why?

因為它覺得 Use Case 應該只專住在領域邏輯，輸入參數的驗證如果也在 Use Case 中，很容易在更換調用方時，就被污染了。 意思是你會需要認知到不同的調用方。你的 Use Case 服務的對象瞬間會從領域邏輯轉成去應付各種調用方可能帶錯參數，為他做一堆防呆。

> `這對應到 SOLID 的哪個原則呢？`

So，作者給個設計建議，Use Case 負責驗證業務規則（組成的第二步）。

如果業務規則被驗證成功了，接著該 Use Case 就會根據某種輸入方式來操作模型的狀態（組成的第三步），這步會變更該領域物件的狀態，並將新狀態傳遞給負責持久化的apapter。

最後一步就是將adapter的return value 轉換成 use case的輸出對象（output object）。

![image](https://hackmd.io/_uploads/ryNRriGXMx.png align="center")

圖5.1 Service 實作Use Case，會修改到 Domain Model，並呼叫 outgoing port以持久化修改後的狀態

注意到沒！ UpdateAccount State Port 與 Load Account Port，這裡是分開獨立的。

如果它們經常一起使用，我們可以將它們合併成一個更廣泛的介面。為了符合 DDD 的語言規範，我們甚至可以將該介面命名為 `AccountRepository`。

> 將各種東西先拆解出來，過程中在根據 Use Case 的使用情況決定要不要組合成一個更完整的個體，是比較好的設計過程。才不會 OverDesign。 `這對應到 SOLID 的哪個原則呢？`

## 剛剛提到的 Validating input

「到底該由誰負責檢查資料是否合法？ 作者覺得輸入驗證這不是Use case的職責，而是應用層的職責。

### 為什麼不能只靠外部（Adapter）來做驗證？

作者認為，如果讓外部（例如網頁介面、API 入口、或排程任務）自己去處理驗證，會有兩個嚴重問題：

*   不信任呼叫者： 我們不能保證每一個對接進來的接口都「確實且正確地」執行了驗證。
    
*   邏輯重複且容易遺漏（DRY 原則破壞）： 如果你寫了 3 種不同的入口（Web、CLI、API），你就要寫 3 次驗證邏輯。只要其中一處少寫了或寫錯了，你的「應用核心」就會收到髒數據。
    

### 為什麼也不建議直接塞在「使用案例（Use Case）」類別裡？

這其實是作者在前文提出的觀點：

*   如果把驗證邏輯混在業務邏輯裡，那個「使用案例類別」會變得非常臃腫。
    
*   它會導致「業務邏輯（怎麼轉帳）」跟「技術驗證（帳號格式對不對）」耦合在一起，違反了職責單一原則（Single Responsibility Principle）。
    

### 作者的結論：輸入模型（Input Model）才是最佳去處

既然「外部不可信」且「塞進業務邏輯又不乾淨」，作者提出的解決方案就是：「把驗證邏輯塞進 Command 物件（輸入模型）的 Constructor 中」。

這意味著：

*   驗證與定義合而為一： 當你宣告「我要轉帳（SendMoneyCommand）」時，這個物件本身就自帶檢查機制。
    
*   「無效物件」無法生成： 你無法建立一個不合法的 Command 物件，因為在 new 的那一刻，建構子就會噴出例外（Exception）。
    
*   保護核心： 只要 SendMoneyService（業務邏輯）收到了這個 Command 物件，它就可以「百分之百放心」地執行業務，因為它知道：如果物件存在，它就一定是有效的。
    

![image](https://hackmd.io/_uploads/ry4Be2fXGl.png align="center")

![image](https://hackmd.io/_uploads/HJuKmnGQMg.png align="center")

能看到他有檢查 account id not null, money > 0 等條件。條件不滿足的情況下就不會建立出對應的物件。

## 構造函數的力量

軟體工程中一個非常經典的兩難：「到底該用『Constructor』還是『Builder Pattern』？」

這意思是，設計時你在權衡的是「程式碼的易讀性」與「編譯時期的安全性」。

一個常見情境是參數過多（Telescoping Constructor）： 如果這類別有 20 個欄位，建構子就會變成一個長到離譜的參數清單，非常難閱讀。

> 依賴多參數，本身也意味著，可能違反了某一個設計原則，或者有壞味道，是哪個呢？

建造者模式的誘惑： 這時 Builder 模式登場了，它讓程式碼看起來很漂亮，像你提供的圖示那樣：.sourceAccountId(...).targetAccountId(...)。

![image](https://hackmd.io/_uploads/Hkfg62G7Ge.png align="center")

作者認為，雖然 Builder 很方便，但它隱藏了編譯器的檢查能力：

*   忘記更新： 軟體開發中斷頻繁，你可能忘記在各處程式碼中去補上這個新欄位的 .transactionDate(...) 呼叫。
    
*   編譯器幫不了你： 因為 Builder 允許你「分段」設定參數，它不會像建構子那樣，強制你在 new 的那一瞬間就傳入所有必要的參數。編譯器不知道你忘了設定某個欄位，所以程式依然能編譯，直到「Runtime」噴出錯誤。意味著有些新欄位可能是 `null`，而物件就被建立出來了，而執行階段使用到該欄位就出現 `null exception`。
    

### 安全性」與「便利性」的完美平衡：強制驗證

作者最後提出了一個關鍵的折衷方案：結合 Builder 與強大的 build() 驗證。

這意味著，雖然我們使用 Builder 帶來便利，但我們不能依賴它「預設不檢查」的特性。我們必須在 build() 方法內進行「防禦性檢查」：

```java
public class SendMoneyCommandBuilder {
    private AccountId sourceAccountId;
    private AccountId targetAccountId;

    public SendMoneyCommand build() {
        // 關鍵：在這裡進行強制檢查
        if (this.sourceAccountId == null || this.targetAccountId == null) {
            throw new IllegalStateException("缺少必要的參數：sourceAccountId 和 targetAccountId 必須設定！");
        }
        return new SendMoneyCommand(sourceAccountId, targetAccountId);
    }
}
```

但如果參數真的太多（例如超過 5-6 個），作者其實隱含了另一個建議：也許你的 Command 物件承載了太多的職責（SRP 違反）。與其依賴 Builder 模式把 20 個參數塞進去，不如將它們拆分成多個小型的 Command。

## 針對不同用例的不同輸入模型

我們可能會忍不住對不同的使用場景使用相同的輸入模型。我們來…考慮「`註冊帳戶`」和「`更新帳戶資訊`」這兩個用例。這兩個用例最初都需要幾乎相同的輸入，即一些帳戶訊息，例如用戶名和電子郵件地址。

*   違背語意：「更新」使用案例需要被更新帳戶的 ID，而「註冊」使用案例則不需要。如果這兩個用例使用相同的輸入模型，我們就必須始終向「註冊」用例傳遞一個空的帳戶 ID 甚至被迫傳遞 null。這在程式設計中是一個危險訊號（代表類別的屬性定義不清）。
    
*   驗證邏輯污染： 你的 SendMoneyService（或 RegisterService）必須寫一堆 if (id != null) 這種髒邏輯。這不是業務規則，這是「為了處理糟糕的設計而寫的補丁」。
    
*   耦合（Coupling）： 兩者被迫綁在一起。如果你明天想為「註冊」增加「邀請碼」欄位，你被迫修改 AccountCommand，這導致「更新」邏輯也被迫跟著異動，即使它根本用不到邀請碼。
    

作者主張「One-to-One Mapping」，也就是`每個用例一個模型`：

*   清晰的界線： RegisterAccountCommand 和 UpdateAccountCommand 分開定義。
    
*   各司其職的驗證：
    
    *   RegisterAccountCommand 的建構子只需要驗證 username 和 email。
        
    *   UpdateAccountCommand 的建構子則強制要求 id 必須存在。
        
*   無副作用： 修改註冊邏輯時，絕對不會影響到更新邏輯，系統的正交性（Orthogonality）大大提升。
    

> 這段實際上是在推廣一個關鍵觀念：「不要因為輸入的數據長得像，就假設它們是同一個業務概念。」。 不要為了「少寫一點 code」而「讓系統變得難以預測」。

![image](https://hackmd.io/_uploads/rkMYlP77fx.png align="center")

## Validating business rules

`業務規則`是應用程式的`核心`，應該謹慎處理。但是，我們什麼時候是在處理`輸入驗證`，什麼時候又是在處理`業務規則`呢？

兩者之間一個非常務實的區別在於，驗證業務規則需要`存取領域模型`的當前狀態，而驗證輸入則不需要。輸入驗證可以像我們之前使用`@NotNull`註解那樣以宣告式的方式實現，而業務規則則需要更多`上下文資訊`。換句話說，輸入驗證是`語法驗證`，而業務規則是用例上下文中的`語義驗證`。

例如轉帳這Use Case，輸入參數的驗證是轉帳數字>0，account id 不為空。而業務規則的驗證，則需要很多context，需要這人的account資訊，需要確認業務規則，也就是餘額是否大於轉帳數字，因為不可透支。

## 充血模型跟貧血模型

人們常討論的問題是，應該遵循 DDD 概念實現一個功能豐富的領域模型，還是應該採用一個「貧血」的領域模型。讓我們來探討一下這兩種模型如何融入我們的架構。

### 充血模型

在充血模型中，盡可能多的領域邏輯實現在應用程式核心的實體中。這些實體提供用於更改狀態的方法，並且只允許符合業務規則的變更。我們之前就是這樣實作 Account實體的。那麼，在這個場景中，我們的用例實作在哪裡呢？

![image](https://hackmd.io/_uploads/HJOeN6G7Gl.png align="center")

在這種情況下，我們的用例充當了領域模型的入口點。用例僅代表使用者的意圖，並將其轉換為對領域實體的一系列方法調用，由這些實體執行實際工作。許多業務規則都位於實體中，而不是用例的實作中。也就是說 Use Case 依然要負責：控制工作流（Workflow Orchestration）。例如：呼叫 Port 讀取資料 $\\rightarrow$ 呼叫充血實體執行業務動作 $\\rightarrow$ 呼叫 Port 儲存資料。Use Case 負責「流程」，Entity 負責「業務狀態改變」。

### 貧血模型

在「貧血」領域模型中，實體本身非常單薄。它們通常只提供用於保存狀態的欄位以及用於讀取和更改狀態的getter和setter方法。它們不包含任何領域邏輯。

這意味著領域邏輯是在用例類別中實現的。它們負責驗證業務規則、更改實體的狀態，並將這些規則傳遞給負責將其儲存到資料庫的輸出連接埠。 「豐富性」體現在用例中，而非實體。

![image](https://hackmd.io/_uploads/r1hVEaz7Me.png align="center")

![image](https://hackmd.io/_uploads/SJC_yPQ7Gl.png align="center")

兩種風格，還可以有其他多種風格。本文討論的架構方法已實作。您可以根據自身需求選擇合適的實作方式。

> 你喜歡 service layer 操作一堆 adapter+port + 貧血模型 還是充血模型本身提供足夠語意來變更狀態，並由 Use Case 負責編排流程與持久化操作呢？

## 針對不同用例的不同輸出模型

作者覺得 output object 應該只包含呼叫者真正需要的資料。

在「匯款」用例的範例程式碼中，我們傳回一個布林值。這是我們在此上下文中可以返回的最小且最具體的值。

我們可能會傾向於將包含更新後實體的完整帳戶傳回給呼叫方。或許調用方對帳戶的新餘額感興趣。

但我們真的希望「匯款」用例傳回這些資料嗎？呼叫者真的需要這些數據嗎？如果需要，我們是否應該創建一個專門的用例來存取這些數據，供不同的呼叫者使用？

這些問題沒有標準答案。但我們應該盡量讓用例具體明確。如有疑問，盡量少回訊息。

Don't! 在不同用例之間共享同一個輸出模型往往會導致用例之間的緊密耦合，初期別這麼做，除非各種 Use Case 已經樣本數多到支持你這樣做。從長遠來看，共享模型往往會因為多種原因而無限膨脹。應用單一職責原則並保持模型分離有助於解耦用例。

## What about **read-only** use cases?

我們該如何實現只讀功能？假設使用者介面需要顯示帳戶餘額，我們是否需要為此建立一個專門的用例實作？

談論這種只讀操作的用例有點`尷尬`。誠然，使用者介面需要這些資料來實現我們稱之為「查看帳戶餘額」的用例，但在某些情況下，稱之為「用例」有點牽強。如果在專案上下文中這被視為一個用例，那麼我們當然應該像實作其他用例一樣實現它。

然而，從應用程式核心的角度來看，這只是一個簡單的資料查詢。因此，如果它不被視為專案上下文中的用例，我們可以將其實現為查詢，以便將其與真正的用例區分開來。

在我們的架構風格中，實現這一目標的一種方法是為查詢建立一個專用的傳入端口，並在「查詢服務」中實現它。

![image](https://hackmd.io/_uploads/SkG5BTfQGl.png align="center")

這樣一來，只讀查詢就能與程式碼庫中的修改Use case（或「Command」）清楚區分開來。我們只要查看輸入類型的名稱就能知道正在處理哪一種。與概念配合得很好例如命令查詢分離（CQS）和命令查詢職責分離（CQRS）。

> 參數命名很重要！ 尾綴Command 代表會有修改模型狀態，需要處理冪等性跟資料一致性等議題。尾綴Query則是安全的唯讀操作。

## 總結

**這如何幫助我建立易於維護的軟體？** 我們的架構讓我們可以根據自己的需求實現領域邏輯，但如果我們獨立地對用例的輸入和輸出進行建模，就可以避免不必要的副作用。

是的，這比在用例之間共享模型要複雜得多。我們需要為每個用例引入一個單獨的模型，並建立該模型與我們的實體之間的映射關係。

但針對特定用例的模型能夠清楚展現用例的本質，從而更易於長期維護。此外，它們還允許多個開發人員並行處理不同的用例，而不會相互幹擾。

結合嚴格的輸入驗證，針對特定用例的輸入輸出模型對於建立可維護的程式碼庫大有裨益。

* * *

從這一章的脈絡來看，這套架構所追求的「可維護性（Maintainability）」**，絕非「少寫幾行程式碼」，相反地，它甚至引入了更多 Model 和 Mapping 的成本。但它換來的，是軟體在面對長期變更時的**低成本**與**高防禦力。

我們可以將這套設計帶來的「可維護性」總結為以下三個核心維度：

* * *

## 1\. 降低變更時的「連帶傷害」（提高正交性\[獨立性/解耦\]）

在傳統的架構中，最讓人痛苦的就是「改 A 壞 B」。本書透過極端的拆解，把這種連帶傷害降到了最低：

*   **「一用例一模型」防止邏輯蔓延：** 將 `RegisterAccountCommand` 和 `UpdateAccountCommand` 分離。當未來「註冊」需要增加活動邀請碼等欄位時，你只需要修改註冊的 Command，**「更新」用例的程式碼完全不需要重新測試或編譯**。
    
*   **介面隔離（ISP）讓依賴最小化：** 將 Port 拆解為 `LoadAccountPort` 與 `UpdateAccountPort`。這代表一個「唯讀」或「只需讀取」的 Use Case，絕不會因為資料庫寫入邏輯的變更而受到波及。
    

> 為什麼叫「正交」？（X 軸與 Y 軸的哲學） 想像一下座標系上的 X 軸與 Y 軸（兩條線互相垂直，在數學上就稱為正交）： 當你沿著 X 軸移動時，你的 Y 軸座標完全不會改變。 這兩條軸擁有各自獨立的變化維度，互不干涉。 投射到系統設計上，如果兩個軟體元件是「正交」的，就意味著：改變其中一個元件的內部實作，不會對另一個元件造成任何連帶影響。 高正交性 ＝ 高度獨立 ＝ 低耦合 ＝ 牽一髮「不」動全身。 舉例：在 `SendMoneyUseCase`中(交集的點）, UpdateAccount State Port (X軸)與 Load Account Port（Y軸)，都在該用例中被依賴。但兩者設計的好，就不會有改 A 壞 B 的可能，因為兩者在這用例，彼此足夠獨立。

* * *

## 2\. 消除程式碼中的「隱形地雷」（提高可預測性）

可維護性最怕的就是「防不勝防」的 NullPointerException 或髒資料。

*   **強迫合法的守門員（Input Model）：** 透過在 Constructor 或精準的 Builder 中進行防禦性驗證，**不合法的物件在核心邊界外就會被阻斷**。進入到 `SendMoneyService` 的資料百分之百是結構正確的。開發人員在寫核心業務時，不需要再寫滿滿的 `if (arg != null)`，程式碼自然乾淨、易讀。
    
*   **Command 與 Query 的語意分流：** 嚴格區分修改狀態的 Command 與唯讀的 Query。維護代碼的人只要看到尾綴，就能立刻預期這個操作會不會引發資料庫一致性或冪等性（Idempotency）的問題，大幅降低認知負載。
    

* * *

## 3\. 職責劃分清晰，Bug 好定位（提高高內聚）

當系統出問題時，你能多快找到問題在哪？

*   **語法驗證 vs 語義驗證：** \* 格式對不對（金額是不是負數） $\\rightarrow$ 去 **Input Model** 找。
    
*   業務邏輯對不對（餘額夠不夠轉帳） $\\rightarrow$ 去 **Entity（充血模型）** 或 **Use Case（工作流編排）** 找。 這讓代碼的職責就像抽屜分類一樣清楚，不會有「驗證邏輯散落各處」的混亂感。
    

* * *

## 💡 總結：可維護性的本質

這章所展示的可維護性，可以用一句話概括：**「用邊界的複雜度，換取核心的純粹度。」**

雖然你必須為每個 Use Case 寫獨立的 Command、Query 和 Output 模型，看似多了很多類別（Boilerplate code），但這些成本是**在開發初期、頭腦最清醒時付出的**。它換來的是當專案越來越大、 deadline 越來越逼近時，系統依然像樂高積木一樣，可以被跨團隊或多位開發者**並行開發、獨立修改，且互不干擾**。這才是能對抗時間與外部因素的「真正可維護性」。
