本章透過一個經典的設計練習——Mark IV Special 咖啡機——來展示常見的設計錯誤與修正方法。這個案例綜合運用了前面學到的所有 SOLID 原則與 UML 技巧,是 Section 2 的總結性案例。

規格說明#

Mark IV Special 咖啡機可以一次沖泡 12 杯咖啡。需要控制的硬體包括:

  • 加熱器(Boiler Heater):可開關
  • 保溫板(Warmer Plate Heater):可開關
  • 保溫板感測器:三種狀態——warmerEmptypotEmptypotNotEmpty
  • 鍋爐感測器:兩種狀態——boilerEmptyboilerNotEmpty
  • 沖泡按鈕(Brew Button):帶有指示燈
  • 壓力釋放閥(Pressure-Relief Valve):控制水的流動

硬體工程師已提供一個低階 API(CoffeeMakerAPI)來操控這些裝置。

常見但糟糕的解法#

Figure 20.1: Hyperconcrete coffee maker

大多數學生會提出類似上圖的設計:一個中央的 CoffeeMaker 類別包含 BoilerWarmerPlateButtonLight 等物件,並且有 SensorHeater 兩個基底類別。

這個設計看起來「很物件導向」,但實際上充滿問題

缺少方法(Missing Methods)#

圖中的類別沒有方法。軟體是關於行為的,一個沒有方法的設計等於還沒開始設計。當設計者只畫出物件而不考慮行為時,他們是根據物理結構而非行為來切割系統——這幾乎必然導致錯誤。

空殼類別(Vapor Classes)#

Light 類別為例:它的 On()Off() 方法只是把呼叫轉發給 CoffeeMakerAPI.SetIndicatorState()。它沒有自己的狀態、沒有有意義的邏輯——只是一個呼叫轉換器

ButtonBoilerWarmerPlate 同樣如此。它們可以從設計中完全移除,不影響任何邏輯。作者稱這些類別為 vapor classes(空殼類別)。

想像出來的抽象(Imaginary Abstraction)#

SensorHeater 基底類別看起來很合理,但沒有任何類別使用它們。而且 Sensor 介面的 Sense() 方法回傳 int,但不同的感測器回傳不同數量的值——這個抽象根本不成立。

注意: 抽象不是隨處可見的。一個沒有使用者的抽象、一個方法語意不一致的介面,只是想像出來的抽象。應該先問:「誰在使用它?」——如果答案是「沒有人」,就不該存在。

上帝類別(God Classes)#

深入分析後會發現,所有有意義的邏輯都集中在 CoffeeMaker 一個類別中,其他類別都是空殼或想像出來的抽象。這實質上就是一個上帝類別,只是被包裝成看似分散的物件模型。

改進的解法#

找到本質抽象#

解決問題的關鍵是抽離硬體細節,專注於「如何泡咖啡」這個核心問題:

  • HotWaterSource:負責加熱水並將熱水送到咖啡上
  • ContainmentVessel:負責收集咖啡並保溫
  • UserInterface:負責接收使用者指令並回報狀態

Figure 20.2: Crossed wires

Crossed Wires 陷阱#

上圖中的關聯方向是錯誤的——它根據咖啡的物理流動(從 HotWaterSourceContainmentVessel)來畫關聯。但在軟體中,關聯代表訊息傳遞的路徑,與物理流動無關。

實際上是 ContainmentVessel 告訴 HotWaterSource 何時開始和停止:

重點: 關聯是物件之間傳遞訊息的通道,不是物理世界中物質流動的方向。這個錯誤稱為 crossed wires——邏輯域與物理域的連線搞混了。

用 Use Cases 驅動設計#

透過分析四個 Use Cases 來逐步建立物件之間的協作關係:

Use Case 1:使用者按下沖泡按鈕

Figure 20.4: Brew button pressed, checking for ready

UserInterface 先向 HotWaterSourceContainmentVessel 詢問是否準備好(IsReady),兩者都確認後才發送 Start 訊息。

Use Case 2:容器未就緒

Figure 20.6: Pausing and resuming the flow of hot water

使用者在沖泡過程中移走咖啡壺。ContainmentVessel 偵測到後通知 HotWaterSource 暫停(Pause),放回後恢復(Resume)。

Use Case 3:沖泡完成

Figure 20.7: Detecting when brewing is complete

HotWaterSourceContainmentVessel 偵測到沖泡完成後,通知所有其他物件(Done 訊息)。

Use Case 4:咖啡喝完了

Figure 20.8: Coffee all gone

ContainmentVessel 偵測到空壺放回保溫板,發送 Complete 訊息給 UserInterface

類別圖#

Figure 20.9: Class diagram

從上述協作圖可以推導出類別圖:三個類別形成一個三角形的關聯結構,各自承擔明確的職責。

實作抽象模型#

作者制定了一個重要規則:三個核心類別絕不能知道 Mark IV 的任何細節。這就是 Dependency-Inversion Principle(DIP)——高層策略不依賴低層實作。

套用 DIP#

Figure 20.10: Detecting the Brew button

UserInterface 是抽象類別,包含高層策略(StartBrewing 方法)。M4UserInterface 是衍生類別,負責呼叫 CoffeeMakerAPI

public class UserInterface
{
    private HotWaterSource hws;
    private ContainmentVessel cv;

    protected void StartBrewing()
    {
        if (hws.IsReady() && cv.IsReady())
        {
            hws.Start();
            cv.Start();
        }
    }
}

技巧: 注意 IsReady() 檢查與 Start() 呼叫放在抽象基底類別中,而非 Mark IV 衍生類別中。這些是高層策略——不論沖什麼樣的咖啡機,流程都是一樣的。只有直接與硬體相關的程式碼才放進衍生類別。

實作 IsReady 方法#

Figure 20.11: Implementing the isReady methods

HotWaterSourceContainmentVesselIsReady() 是抽象方法,由 M4HotWaterSourceM4ContainmentVessel 分別呼叫對應的 CoffeeMakerAPI 函式來實作。

Polling 機制#

Figure 20.12: Pollable coffee maker

threading 還是 polling 的選擇與設計無關,可以在最後才決定。作者引入 Pollable 介面,讓三個 M4 衍生類別各自實作 Poll() 方法,由 Main 函式的無限迴圈輪詢呼叫。

public static void Main(string[] args)
{
    CoffeeMakerAPI api = new M4CoffeeMakerAPI();
    M4UserInterface ui = new M4UserInterface(api);
    M4HotWaterSource hws = new M4HotWaterSource(api);
    M4ContainmentVessel cv = new M4ContainmentVessel(api);

    ui.Init(hws, cv);
    hws.Init(ui, cv);
    cv.Init(hws, ui);

    while (true)
    {
        ui.Poll();
        hws.Poll();
        cv.Poll();
    }
}

設計的優點#

Figure 20.13: Coffee maker components

作者在三個抽象類別周圍畫了一條線——所有跨越這條線的依賴都指向內部。抽象類別不知道按鈕、燈號、閥門、感測器等硬體細節,而衍生類別則被這些細節所支配。

這個設計的優點:

  • 三個抽象類別可以重用於不同的咖啡機(甚至泡茶機、雞湯機)
  • 高層策略與低層實作完全分離
  • 符合 SRPOCPLSPDIPISP

OOverkill#

補充: 作者誠實地指出:對於這個簡單的問題,OO 設計其實是 overkill。用 FSM 來實作(7 個狀態、18 個轉換),整個程式不到一頁。OO 版本光是不含測試就有五頁。在小型問題上,依賴管理與關注點分離的成本可能大於效益。然而在大型系統中,這些原則的價值是不可替代的。

本章小結#

Mark IV 咖啡機案例完美展示了敏捷設計的核心思維:

  • 不要從物理結構出發設計——先考慮行為
  • 抽象應該來自真實需求,而非憑空想像
  • SOLID 原則(SRP、OCP、LSP、DIP、ISP)在真實案例中是如何被綜合運用的
  • 好的設計是迭代出來的,不是一次畫完的