Constructors Must Be Code-Free

The following text is a partial translation of the original English article, performed by ChatGPT (gpt-3.5-turbo) and this Jekyll plugin:

在构造函数中应该做多少工作?在构造函数内部进行一些计算并封装结果似乎是合理的。这样,当对象方法需要结果时,我们就能立即获得它们。听起来是一个好的方法吗?不,不是的。出于一个原因,这是个不好的主意:它阻止了对象的组合并使其无法扩展。

假设我们正在创建一个表示人名的接口:

很简单,对吧?现在,让我们试着实现它:

这有什么问题?这样做更快,对吧?它只会将名称分解成几个部分并封装起来。然后,无论我们调用first()方法多少次,它都会返回相同的值,而不需要再次进行分解。然而,这种思维是有缺陷的!让我展示给你正确的方法并解释一下:

这是正确的设计。我可以看到你在微笑,所以让我证明我的观点。

在我开始证明之前,请你先阅读这篇文章:Composable Decorators vs. Imperative Utility Methods。它解释了静态方法和组合装饰器之间的区别。上面的第一个代码片段非常接近一个命令式实用方法,尽管它看起来像一个对象。而第二个示例是一个真正的对象。

在第一个示例中,我们滥用了new运算符,并将其转化为一个静态方法,它会在此刻为我们进行所有的计算。这就是命令式编程的特点。在命令式编程中,我们立即进行所有的计算,并返回完全准备好的结果。而在声明式编程中,我们尽可能地推迟计算。

让我们尝试使用我们的EnglishName类:

在此代码片段的第一行中,我们只是创建了一个对象的实例并将其标记为name。我们不想立即访问数据库并从中获取完整的名称,将其分割成部分,并将它们封装在name中。我们只是想创建一个对象的实例。这样的解析行为对我们来说只是副作用,在这种情况下会减慢应用程序的速度。如你所见,如果出现问题并且需要构造异常对象,我们可能只需要name.first()

我的观点是,在构造函数内部执行任何计算都是一种不好的做法,必须避免,因为它们是副作用,并且不是对象所有者所请求的。

你可能会问,在重用name时性能如何。如果我们创建一个EnglishName的实例,然后调用name.first()五次,我们将得到对String.split()方法的五次调用。

为了解决这个问题,我们创建另一个类,一个可组合的装饰器,它将帮助我们解决这个“重用”问题:

我正在使用jcabi-aspects中的Cacheable注解,但您可以使用Java(或其他语言)中的任何其他可用的缓存工具,如Guava Cache

但请不要将CachedName设置为可变和延迟加载——这是一种反模式,我之前在《对象应该是不可变的》中已经讨论过。

现在我们的代码将如下所示:

这只是一个非常原始的例子,但我希望你能理解这个概念。

在这个设计中,我们基本上将对象分成了两部分。第一个部分知道如何从英文名中获取名字的第一部分。第二个部分知道如何将这个计算的结果缓存到内存中。现在,作为这些类的用户,我可以自己决定如何使用它们。我将决定是否需要缓存。这就是对象组合的全部意义。

请让我再强调一下,构造函数中唯一允许的语句是赋值语句。如果你需要放置其他内容,请开始考虑重构——你的类肯定需要重新设计。

Translated by ChatGPT gpt-3.5-turbo/42 on 2023-11-17 at 14:10

sixnines availability badge   GitHub stars