构造方法必须无代码

在一个构造方法里应该完成多少工作?在构造方法里进行一些运算然后封装结果似乎有些道理。那样的话,当对象方法需要结果时,我们已经准备好了。听起来像是一个好方法。不,不是。这是一个坏主意的原因之一是:它阻止了对象组合并且让它们无法扩展。
现在我们做一个展现一个人名字的接口:

interface Name {
  String first();
}

相当简单,对吧?现在,我们试着实现它:

public final class EnglishName implements Name {
  private final String name;
  public EnglishName(final CharSequence text) {
    this.name= text.toString().split(" ", 2)[0];
  }
  @Override
  public String first() {
    return this.name;
  }

这样做有什么问题吗?它更快,对吧?它一次就把name分割成多个部分并封装了它们。然后,不管我们调用first()方法多少次,它都会返回相同的值并且我们不需要再次进行分割。然后,这个想法有缺陷。让我向你展示正确的方式和解释:

public final class EnglishName implements Name {
  private final CharSequence text;
  public EnglishName(final CharSequence txt) {
    this.text = txt;
  }
  @Override
  public String first() {
    return this.text.toString().split("", 2)[0];
  }
}

这是正确的设计。我能看见正在微笑,所以让我来证明我的观点。

在我开始证明之前,请先阅读这编文章:组合修饰符 vs. 必要实用方法
。文章解释了静态方法与可组合修饰符的区别。上面的第一段代码非常接近一个必要实用方法,即使它看起来像一个对象。第二个例子才是一个真正的对象。

在第一个例子里面,我们滥用了new操作符并且把它转换成了一个静态方法,所有的运算都在里面立刻进行。这就是命令式编程。在命令式编程中,我们立即进行所有的运算并返回完全准备好的结果。相反,在声明式编程中,我们尽可能的推迟计算。
让我们尝试使用EnglishName类:

final Name name = new EnglishName(
  new NameInPostgreSQL(/*...*/)
);
if (/* something goes wrong */) {
  throw new IllegalStateException(
    String.format(
      "Hi, %s, we can't proceed with your application",
      name.first()
    )
  );
}

代码片段的第一行,我们只是创建了一个对象的实例并命名为name。我们还不想去数据库,把从那里获取的全名,分割成部分,并且把它们包装在name里面。我们只是想创建一个对象的实例。这样的一个解析行为会有副作用,那样的话,会拖慢现有应用程序。正如你所看到的,我们只需要name.first(),如果出了问题,我们需要构造一个异常对象。

我的观点是在构造方法里进行任何运算是一个不好的实践并且必须避免,因为有副作用并且不是对象拥有者所需要的。

你可能会问,在name的重用期间性能怎么样呢?如果我们创建一个EnglishName的实例并且调用name.fist()方法5次,我们最终会调用5次String.split()方法。

为了解决这个问题,我们创建另一个类,一个组合修饰符,会帮助我们解决这个重用问题。

public final class CachedName implements Name {
  private final Name origin;
  public CachedName(final Name name) {
    this.origin = name;
  }
  @Override
  @Cacheable(forever = true)
  public String first() {
    return this.origin.first();
  }
}

我使用的是http://aspects.jcabi.com/
里面的Cacheable注解,但是你能使用任何其他的Java缓存工具。

public final class CachedName implements Name {
  private final Cache<Long, String> cache =
    CacheBuilder.newBuilder().build();
  private final Name origin;
  public CachedName(final Name name) {
    this.origin = name;
  }
  @Override
  public String first() {
    return this.cache.get(
      1L,
      new Callable<String>() {
        @Override
        public String call() {
          return CachedName.this.origin.first();
        }
      }
    );
  }
}

但是请别让CachedName可变和延迟加载—它是一个反面模式,这个之前已经在 对象应该不变 中讨论过。
我们的代码现在像这样:

final Name name = new CachedName(
  new EnglishName(
    new NameInPostgreSQL(/*...*/)
  )
);

这是一个非常原始的例子,但我希望你能明白。

在这个设计中,我们基本上把对象分割成了两部分。第一部分知道怎样在英文名字中拿到first name.第二知道怎样在内存中缓存运算结果。我决定我是否需要缓存。这就是对象组合。

让我重申在构造方法里唯一允许的声明就是赋值。如果你需要在构造方法里放其他东西,开始思考重构吧—–你的类肯定需要重新设计。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章