自封装(SelfEncapsulation) - Martin Fowler博客


/**
 * 献给我最尊敬的偶像Martin Fowler
 * 原文出处:https://martinfowler.com/bliki/SelfEncapsulation.html
 * @author dogstar.huang <chanzonghuang@gmail.com> 2017-03-17
 */

本翻译已征得Martin Fowler同意,并链接在博客原文下方。

自封装

数据封装是面向对象风格的核心原则。这表明一个对象的字段不应该公开地暴露,而是来自对象以外的全部访问都应该通过访问器方法(getter和setter)。有些语言允许公开访问的字段,但我们通常提醒程序员不要这样做。自封装(Self-encapsulation)更进了一步,意味着全部对于数据字段的内部访问同样也应该通过访问器方法。只有访问器方法本身才能接触到这些数据变量。如果数据字段不暴露给外部,这就意味着要添加额外的私有访问器。

以下是一个合理封装的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字体通过一个getter暴露给了这个类的用户,但rate字段是用于内部,所以不需要getter。

下面是使用了自封装的版本。

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()); }

自封装意味着getAmounts需要通过getter访问这两个字段。这也意味着我需要为rate添加一个getter,并且设置为私有方法。

封装不可变数据通常是一个好主意。更新方法可以包含执行验证和重要逻辑的代码。通过约束用函数来访问,我们可以支持统一访问原则,使得我们可以隐藏计算了哪些数据和存储了哪些数据。这些访问器允许我们在保持相同公开接口的同时修改数据结构。在关于对于一个对象什么是“外部(outside)”,根据各种各样的访问修饰符,不同的语言在细节上会有所不同,但大部分环境都支持某种程度上的数据封装。

我遇到过一些强制自封装的组织,并且自从90年代后用不用它就成了一个常规辩论的主题。它的提倡者说自封装是这么一个好处,你也会想把它结合到内部访问。批评家则认为它是导致掩盖发生了什么的多余代码的无用仪式。

对于这点,我的观点是大多数情况下自封装有一点价值。封装的价值与数据访问的作用域成正比。类通常很小(至少我的是),所以在那样的作用域内直接访问不会有问题。大多数访问器都是简单赋值的setter和提取的getter,所以内部地使用自封装价值很小。

但也有值得为自封装付出努力的通用情况。如果有逻辑在setter里,那么为全部内部的更新也考虑这一点是很明智的。另一种情况是当类是继承结构中的一部分时,访问器为子类提供有用的钩子以便重载行为。

所以通常我的第一举动是直接访问字段,但对于需要自封装的情况则使用自封装字段进行重构。通常,可以使用提取一个新的类来解决导致我考虑使用自封装的压力。

扩展阅读

实现模式和[Smalltalk最侍实践模式]里,Kent Beck讨论了直接访问(Direct Access)和间接访问(Indirect Access)之间的权衡。

致谢

Ian Cartwright,Matteo Vaccari,和Philip Duldig在这篇文章的草稿上进行了评论。

dogstar

一位喜欢翻译的开发人员,艾翻译创始人之一。

广州