當抽象的某些成員宣告的存取權限比實際所需更寬鬆時——例如該 private 卻寫成 public——就出現 Deficient Encapsulation。極端形式為全域變數/全域資料結構

核心定義#

封裝原則要求介面與實作分離,讓兩者可以幾乎獨立演化。當存取權限過寬:

  • 客戶端會直接相依實作細節
  • 實作一改,客戶端跟著壞。

全域狀態問題更嚴重:任何抽象都能讀寫它,等同於在不直接依賴的兩個抽象之間開了秘密通道。理解、可靠性、可測試性都會被嚴重侵蝕。

常見成因#

  • 為了好測試:把 private 方法改 public,但這等於「為測試方便而打破封裝」。
  • OO 中的程序式思維:習慣把資料拉成全域以方便多處使用。
  • 快速修補:「兩三個類別要共用,懶得建抽象傳參數,乾脆全域」。

範例#

範例 1:java.awt.Point#

java.awt.Point 同時擁有 public 欄位 xy public getter/setter——欄位本應是 private。

Figure 4.4: java.awt.Point 類別中的 public 資料成員

JDK 7 的分析顯示:508 個類別至少有一個 public 欄位;96 個類別有 ≥5 個 public 欄位;極端的 com.sun.imageio.plugins.png.PNGMetadata 竟有超過 85 個 public 欄位!

範例 2:SizeRequirements 的 alignment#

public float alignment;

文件註明 alignment 必須在 0.0 到 1.0 之間,但欄位 public 後類別無法保證這個不變式

範例 3:SwingUtilities 系列#

javax.swing.SwingUtilitiessun.swing.SwingUtilities2sun.java.swing.SwingUtilities3 是 swing 套件的內部工具類,但被宣告為 public,文件甚至直接警告「不要當公開 API 使用」——這暗示它們本就不該 public。

順便一提,命名也很糟:SwingUtilities / 2 / 3 從名稱完全看不出彼此的差別。

範例 4:System.out 與 System.err 的歷史包袱#

java.lang.System 中:

public final static InputStream in = null;
public final static PrintStream out = null;
public final static PrintStream err = null;

雖然 final,仍可被 setOut()setErr() 透過 native 方法重設。因為 public,等同全域變數

Java 1.1.5 引入 PrintWriter 想取代 PrintStream,但因為 System.outSystem.err 直接暴露 PrintStream 型別,整個 PrintStream 類別無法被廢棄——System.out.println 太普遍。

案例:應用程式中的 Data 類別#

一個程式碼品質報表應用以「Parsing → Analytics → Reporting」為三個模組。設計者用 Data 類別當資料中介,23 個欄位全為 public static(且非 final)。 結果:Analytics 的細節被 Parsing 看光、模組之間透過全域變數隱式緊耦合。

案例:Linux kernel 的 current 全域#

書中也引用 Yu et al. 對 Linux 2.4.20 的研究:99 個全域變數中,current 指標被 12 個 kernel 模組讀寫、6 個僅讀,外加 1071 個非 kernel 模組存取——讀/寫總計近 9700 次。極度緊耦合,使核心難以維護

Figure 4.5: Linux kernel 中全域變數 current 的使用情形

建議重構#

套用 encapsulate field 重構:欄位改 private,提供必要的 accessor/mutator;方法該 private/protected 就調回去。

  • 範例 1、2PointSizeRequirements 的欄位 → private + getter/setter;alignment 的 setter 加上範圍驗證。
  • 範例 3:把方法依概念分組為 JavaFXUtilitiesPaneUtilities 等,並改為套件層級(package-private)
  • 全域變數的兩種變體
    • 少數類別共享 → 改用參數傳遞。
    • 多類別共享 → 重新分析責任歸屬,可能需重新切分抽象,再把資料封裝回去。
  • 範例 4:引入 Console 類別封裝 input/output/error stream。Java 1.6 的 java.io.Console 即為此方向的實踐。

對品質屬性的衝擊#

  • 理解性:實作細節外露,介面變複雜;全域變數讓讀寫追蹤幾乎不可能。
  • 可變更性與可擴充性:客戶端直接依賴細節。
  • 可重用性:客戶端被全域狀態綁死。
  • 可測試性:全域狀態不可預測。
  • 可靠性:抽象失去「資料完整性守門員」的角色。

別名#

  • Hideable public attributes/methods:實際只在類別內使用卻 public。
  • Unencapsulated class:類別大量使用全域變數。
  • Class with unparameterized methods:方法大多無參,仰賴類別/全域變數。

實務考量#

內嵌類別中的寬鬆存取#

巢狀/匿名類別只對外圍類別可見時,宣告為 public 是可接受的。

效能取捨#

java.swing.text.Segment 為了避免複製字元陣列,刻意暴露 arrayoffsetcount 三個 public 欄位(並標注「應視為 immutable」):

這是設計者有意識權衡後的妥協。但開發者常以「直接存取較快」為由濫用——現代編譯器與 JIT 會內聯 accessor,多數情況下效能差異微不足道