即使所有成員的存取權限都「正確」,抽象的公開介面仍可能透露實作細節——這就是 Leaky Encapsulation。
核心定義#
與 Deficient Encapsulation 的差別:那是存取權限太鬆;本壞味道是介面本身洩漏——即使欄位是 private,回傳值或方法名仍可能洩漏實作。
兩個典型後果:
- 實作改動會破壞客戶端。
- 客戶端可拿到內部資料結構的 handle,直接破壞物件狀態。
常見成因#
- 不知道該隱藏什麼:分辨「介面 vs. 實作」需要經驗,新手經常無意間把實作放上介面。
- 黏滯性:時間壓力下做出快又髒的介面。
- 介面顆粒度過細(fine-grained interface):把細部步驟當作 public 方法,外露不必要的內部機制。
範例#
範例 1:TodoList 暴露 LinkedList#

Figure 4.6: TodoList 類別暴露其內部使用的 linked list
public LinkedList<TodoItem> getListEntries();回傳型別 LinkedList:
- 直接揭示「內部用 LinkedList」這個實作細節。
- 將來若要改
ArrayList、HashMap以加快查找,會破壞所有客戶端。 - 回傳值是內部資料結構的引用,客戶端可繞過
addListEntry()直接改動內部狀態。
軼事:OrderList 的 balance() 方法#
一個電商應用的
OrderList繼承自自家樹狀結構。樹歪斜導致效能退化後,開發者公開了balance()方法當 workaround。後來想換 hash table 才發現:因為繼承了 Tree、又公開了balance(),客戶端遍地依賴實作——大規模重構難以避免。
教訓:
- 用組合而非繼承承載資料結構。
- 快速 workaround 經常釀成更大的設計災難。
範例 2:CustomListBox.bubbleSort()#

Figure 4.7: CustomListBox 在介面上洩漏排序演算法名稱
public void bubbleSort();方法名就把演算法暴露出來。要換成 quick sort 時:
- 改名
quickSort()→ 破壞客戶端。 - 保留名稱、改實作 → 名稱與實作不一致,反而誤導客戶端。
兩個選項都糟糕——根本問題是演算法名稱不該出現在介面上。
案例:Image 類別的四步驟#
Image 類別把 load()/process()/validate()/show() 全部公開,必須依特定順序呼叫。新手呼叫順序錯誤就會把物件留在無效狀態。
想用「狀態旗標」維持順序只會讓問題更糟——應該只暴露一個
display()方法,內部依序呼叫四步。粗顆粒方法才是封裝該有的樣子。
建議重構#
重新設計介面,使其完全不暴露實作。
具體手法:
- 演算法名外露 → 改為通用名稱(如
sort());內部可用 Strategy pattern 切換演算法。 - 回傳內部資料結構 →
- 回傳深拷貝。
- 回傳唯讀/不可變物件。
- 回傳 Iterator——往往才是客戶端真正需要的。
- 細顆粒方法 → 改為 private;介面只暴露邏輯上的粗顆粒方法。
範例 1 的具體重構#
- 回傳型別由
LinkedList改為List或Collection。 - 進一步思考客戶端真正想做什麼——若只是遍歷,乾脆回傳
Iterator,連底層型別都看不到。
範例 2 的具體重構#
把 bubbleSort() 改為 sort(),內部用 Strategy 樣式選擇演算法。若已是公開 API,可新增 sort() 並 deprecate bubbleSort()——但 API 設計從來都很難。

Figure 4.8: CustomListBox 的建議重構
對品質屬性的衝擊#
- 理解性:介面被內部細節撐大、變複雜。
- 可變更性與可擴充性:客戶端直接依賴實作。
- 可重用性:客戶端要適應新環境就得剝掉這些依賴(參見 Command Pattern 的設計考量)。
- 可靠性:內部資料結構洩漏 → 物件完整性無法保障。
別名#
- Leaking implementation details in API:API 洩漏實作細節,造成困惑或限制變更自由。
實務考量:低階類別#
嵌入式音訊處理中,metadata(取樣率、單聲道/立體聲)有時必須直接暴露給中介層客戶端。有意為之的暴露並不算壞味道,但要:
- 明確警告客戶端「不當使用會破壞物件完整性」。
- 不讓這類類別出現在高階 API——避免暴露給應用層的開發者。