若某模組提供一個可用於多種用途的通用機制,它應該只提供那個機制。
- 不應包含特化某用途的程式碼
- 也不應混入其他通用機制
與通用機制相關的特殊用途程式碼,應放在另一個模組中(通常是與該特殊用途相關的模組)。
範例: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);
}該類別還有 logRpcSendError、logRpcReceiveError 等多個方法,每個對應不同錯誤。
為什麼不好#
- 日誌方法很淺:多半只一行程式碼,但需要大量文件
- 每個方法只在一處被呼叫
- 高度依賴呼叫處:讀呼叫的人要去看日誌方法、讀日誌方法的人要去看呼叫端 → 來回翻
改善:直接內聯#
把日誌敘述放回偵測錯誤的位置——程式碼更易讀,介面也少了一組。
案例 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(文字、選取、游標) | 各自獨立類別(UndoableInsert、UndoableDelete、UndoableSelection、UndoableCursor) |
| 分組策略 | 哪些動作該被視為一個 undo 單位 | UI 高層程式碼透過 addFence 控制 |
每一層都不需要理解其他層的細節:
History不知道動作具體類型 → 可重用於各種應用- 各動作類別只懂自己一種動作
History與動作類別都不需懂分組策略
關鍵設計決定:把通用部分從特殊部分分離,並把通用部分放進獨立類別。剩下的細節自然落定。
一個重要澄清#
「通用 / 特殊分離」是針對同一個機制的程式碼。
例如:通用 undo(管 history list) vs. 特殊 undo(undo 一個文字插入動作)— 這兩者應分。
但不同機制的特殊與通用混在一起反而合理。例如文字類別本身是個通用文字機制,但裡頭可以包含「只處理文字插入 / 刪除的 undo 程式碼」(屬特殊 undo)。
- 該特殊 undo 不該與
History通用基礎設施混合- 但放進文字類別合理,因為它與其他文字功能緊密相關