构造函数中应该完成多少工作?在构造函数中进行一些计算然后封装结果似乎是合理的。这样,当对象方法需要结果时,我们就会准备好它们。听起来是个好方法?不,这不对。出于一个原因,这是一个坏主意:它阻止了对象的组合并使它们不可扩展。
杀死比尔:卷。 2 (2004) 昆汀·塔伦蒂诺
假设我们正在制作一个代表人名的界面:
interface name {
string first();
}
很简单,对吧?现在,让我们尝试实现它:
interface name {
string first();
}
这有什么问题?它更快,对吧?它只将名称分成几部分并将它们封装起来。那么,无论我们调用多少次
first()
方法,它都会返回相同的值,并且不需要再次进行拆分。然而,这是有缺陷的想法!让我告诉你正确的方法并解释:
interface name {
string first();
}
这是正确的设计。我可以看到你在微笑,所以让我证明我的观点。
不过,在我开始证明之前,让我请您阅读这篇文章:可组合装饰器与命令式实用方法。它解释了静态方法和可组合装饰器之间的区别。上面的第一个片段非常接近命令式实用方法,尽管它看起来像一个对象。第二个例子是一个真实的对象。
在第一个例子中,我们滥用了
new
运算符并将它变成了一个静态方法,它
现在就
为我们做所有的计算。这就是
命令式
编程的意义所在。在命令式编程中,我们立即进行所有计算并返回完全准备好的结果。在声明式编程中,我们试图尽可能长时间地延迟计算。
让我们尝试使用我们的
englishname
类:
interface name {
string first();
}
在此代码段的第一行中,我们只是创建一个对象的实例并将其标记为
name
。我们还不想访问数据库并从那里获取全名,将其拆分为多个部分,然后将它们封装在
name
中。我们只想创建一个对象的实例。这样的解析行为会对我们产生副作用,在这种情况下,会减慢应用程序的速度。如您所见,如果出现问题我们可能只需要
name.first()
并且我们需要构造一个异常对象。
我的观点是,在构造函数内部进行 任何 计算都是一种不好的做法,必须避免,因为它们是副作用,并且不是对象所有者所要求的。
您可能会问,在重新使用
name
期间的性能如何。如果我们创建一个
englishname
的实例,然后调用
name.first()
五次,我们最终将调用
string.split()
方法五次。
为了解决这个问题,我们创建了另一个类 a ,它将帮助我们解决这个“重用”问题:
interface name {
string first();
}
我正在使用
jcabi-aspects
中的
cacheable
注释,但您可以使用 java(或其他语言)中可用的任何其他缓存工具,例如
番石榴缓存
:
interface name {
string first();
}
但请不要让
cachedname
可变和延迟加载 — 这是一种反模式,我之前在 .
这就是我们的代码现在的样子:
interface name {
string first();
}
这是一个非常原始的例子,但我希望你明白了。
在这个设计中,我们基本上将对象分成两部分。第一个知道如何从英文名中获取名字。第二个知道如何在内存中缓存此计算的结果。现在,作为这些类的用户,我决定如何使用它们。我将决定是否需要缓存。这就是对象组合的全部内容。
让我重申,构造函数中唯一允许的语句是赋值。如果你需要在那里放其他东西,开始考虑重构——你的类肯定需要重新设计。