若某模組提供一個可用於多種用途的通用機制,它應該提供那個機制。

  • 不應包含特化某用途的程式碼
  • 也不應混入其他通用機制

與通用機制相關的特殊用途程式碼,應放在另一個模組中(通常是與該特殊用途相關的模組)。

範例:GUI 編輯器(呼應第 6 章)#

最佳設計:

  • 文字類別:通用文字操作
  • UI 模組:選取刪除等 UI 專用操作

把 UI 專用操作放在文字類別會:

  • 製造 UI 與文字類別之間的資訊洩漏
  • 引入額外介面(連帶提高複雜性)

應用於分層#

一般而言,系統的下層偏通用、上層偏特殊

怎麼分離#

  • 把特殊用途程式碼往上拉到上層
  • 下層保持通用

當一個類別同時包含某抽象的通用與特殊功能時,看看能不能把它拆成兩個:

  • 一個包含通用部分
  • 另一個層疊在上面,提供特殊部分

案例 1:插入游標與選取(應分離)#

GUI 編輯器中:

  • 插入游標:閃爍的垂直線,標出輸入位置
  • 選取:高亮文字範圍,用於複製或刪除
  • 兩者有關連:游標總是落在選取的某一端;點擊拖曳會同時設定兩者;插入文字會先刪除選取再插入

某團隊用單一物件同時管理游標與選取:兩個位置 + 一個布林(哪端是游標)+ 一個布林(選取是否存在)。

為什麼不好#

問題說明
上層程式碼仍須區分仍把游標、選取視為兩個不同實體分別操作
實作反而更複雜游標位置不直接儲存,得先讀布林再選端點
介面更難用取游標位置時還得「先測布林、再選一端」

紅旗:通用 / 特殊混合(Special-General Mixture)#

一個通用機制中混入了特化於某用途的程式碼。

機制變得更複雜,並在機制與該用途之間造成資訊洩漏:未來修改該用途時,可能也得動到底層機制。

改善#

  • 拆分後,UI 與實作都更簡單
  • 改用簡潔的 Position 類別表示「行號 + 行內字元位置」
  • 選取:兩個 Position
  • 游標:一個 Position
  • Position 在專案其他地方也派上用場

案例 2:日誌類別(應合併)#

某學生專案在多處有這種程式碼:

try {
    rpcConn = connectionPool.getConnection(dest);
} catch (IOException e) {
    NetworkErrorLogger.logRpcOpenError(req, dest, e);
    return null;
}

NetworkErrorLogger 中:

public static void logRpcOpenError(RpcRequest req, AddrPortTuple dest, Exception e) {
    logger.log(Level.WARNING, "Cannot send message: " + req + ".\n" +
        "Unable to find or open connection to " + dest + " :" + e);
}

該類別還有 logRpcSendErrorlogRpcReceiveError 等多個方法,每個對應不同錯誤。

為什麼不好#

  • 日誌方法很淺:多半只一行程式碼,但需要大量文件
  • 每個方法只在一處被呼叫
  • 高度依賴呼叫處:讀呼叫的人要去看日誌方法、讀日誌方法的人要去看呼叫端 → 來回翻

改善:直接內聯#

把日誌敘述放回偵測錯誤的位置——程式碼更易讀,介面也少了一組

案例 3:編輯器 undo 機制(應分離)#

需求:多層 undo / redo,且不僅文字、連選取、游標、檢視變更都要可 undo。

反例:全部塞進文字類別#

文字類別維護所有可 undo 動作的清單;改文字時自動加入 undo 條目;UI 變更時,UI 程式碼呼叫文字類別的方法新增條目;undo / redo 由文字類別處理(部分動作再 callback 回 UI)。

問題:

  • 文字類別中混雜:通用 undo 機制 + 各種專用 undo handler
  • 選取、游標的 undo handler 與文字類別其他功能毫無關係
  • 文字類別與 UI 之間資訊洩漏
  • 新增可 undo 實體,得改文字類別

改善:抽出 History 類別#

public class History {
    public interface Action {
        public void redo();
        public void undo();
    }

    History() { ... }

    void addAction(Action action) { ... }
    void addFence() { ... }

    void undo() { ... }
    void redo() { ... }
}

設計分層#

層次內容實作位置
通用機制管理動作清單、執行 undo / redo、分組History 類別
動作的具體內容不同類型的 undo(文字、選取、游標)各自獨立類別(UndoableInsertUndoableDeleteUndoableSelectionUndoableCursor
分組策略哪些動作該被視為一個 undo 單位UI 高層程式碼透過 addFence 控制

每一層都不需要理解其他層的細節:

  • History 不知道動作具體類型 → 可重用於各種應用
  • 各動作類別只懂自己一種動作
  • History 與動作類別都不需懂分組策略

關鍵設計決定:把通用部分從特殊部分分離,並把通用部分放進獨立類別。剩下的細節自然落定。

一個重要澄清#

「通用 / 特殊分離」是針對同一個機制的程式碼。

例如:通用 undo(管 history list) vs. 特殊 undo(undo 一個文字插入動作)— 這兩者應分

不同機制的特殊與通用混在一起反而合理。例如文字類別本身是個通用文字機制,裡頭可以包含「只處理文字插入 / 刪除的 undo 程式碼」(屬特殊 undo)。

  • 該特殊 undo 不該與 History 通用基礎設施混合
  • 但放進文字類別合理,因為它與其他文字功能緊密相關