自我封裝

2017 年 3 月 9 日

資料封裝是物件導向風格中的核心原則。這表示物件的欄位不應公開顯示,所有來自物件外部的存取都應透過存取方法(取得器和設定器)進行。有些語言允許公開存取的欄位,但我們通常會告誡程式設計師不要這樣做。自我封裝更進一步,表示對資料欄位的所有內部存取也應透過存取方法進行。只有存取方法才能觸及資料值本身。如果資料欄位未公開顯示,這表示需要新增其他私人存取器。

以下是合理封裝的 Java 類別範例

class Charge…

  private int units;
  private double rate;

  public Charge(int units, double rate) {
    this.units = units;
    this.rate = rate;
  }
  public int getUnits() { return units; }
  public Money getAmount() { return Money.usd(units * rate); }

兩個欄位都是不可變的。units 欄位透過取得器公開給類別的用戶端,但 rate 欄位只在內部使用,因此不需要取得器。

以下是使用自我封裝的版本。

class ChargeSE…

  private int units;
  private double rate;

  public ChargeSE(int units, double rate) {
    this.units = units;
    this.rate = rate;
  }
  public int getUnits()    { return units; }
  private double getRate() { return rate; }
  public Money getAmount() { return Money.usd(getUnits() * getRate()); }

自我封裝表示 getAmount 需要透過取得器存取兩個欄位。這也表示我必須為 rate 新增取得器,而我應將它設為私人。

封裝可變資料通常是個好主意。更新函式可以包含執行驗證和後續邏輯的程式碼。透過限制函式存取,我們可以支援 UniformAccessPrinciple,讓我們可以隱藏哪些資料是計算出來的,哪些是儲存的。這些存取器讓我們可以在保留相同公開介面的同時修改資料結構。不同的語言在「外部」是什麼的細節上有所不同,取決於各種 AccessModifier,但大多數環境在某種程度上都支援資料封裝。

我遇到過一些強制使用自我封裝的組織,而是否要使用它自 90 年代以來一直是定期辯論的話題。它的支持者表示封裝是一個好處,你想要將它納入內部存取中。批評者則認為這是不必要的儀式,導致不必要的程式碼,模糊了正在發生的事情。

我對此的看法是,大多數時候自我封裝的價值很低。封裝的價值與資料存取的範圍成正比。類別通常很小(至少我的類別很小),因此直接存取在該範圍內不會是個問題。大多數存取器都是設定項的簡單指派和取得項的擷取,因此在內部使用它們的價值很低。

但在一些常見情況下,自我封裝是值得付出的。如果設定項中有邏輯,那麼也明智地考慮對任何內部更新進行設定。另一種情況是當類別是繼承結構的一部分時,在這種情況下,存取器為子類別提供有價值的掛鉤點,以覆寫行為。

因此,我通常的第一步是使用直接存取欄位,但如果情況需要,則使用 自我封裝欄位 進行重構。通常,促使我考慮自我封裝的力量,我可以透過 萃取新類別 來解決。

進一步閱讀

Kent Beck 在 Implementation PatternsSmalltalk Best Practice Patterns 中以直接存取和間接存取的名稱討論了這些權衡。

致謝

Ian Cartwright、Matteo Vaccari 和 Philip Duldig 對這篇文章的草稿發表了評論