How Immutability Helps

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

在最近的几篇文章中,包括《Getters/Setters. Evil. Period.》,《Objects Should Be Immutable》,以及《Dependency Injection Containers are Code Polluters》,我普遍将所有可变对象的“setter”(以set开头的对象方法)标记为邪恶的。我的论证主要基于隐喻和抽象的例子。显然,这对于你们许多人来说并不令人信服—我收到了一些要求提供更具体和实际的例子的请求。

因此,为了说明我对“通过setter进行可变性”的强烈负面态度,我拿了一个现有的commons-email Java库,并根据我的思路重新设计了它,不使用setter,而是考虑“对象思维”。我将我的库作为jcabi家族的一部分发布—jcabi-email。让我们看看在没有getter的“纯”面向对象和不可变方法下,我们可以得到哪些好处。

如果你使用commons-email发送电子邮件,你的代码将如下所示:

这是如何使用jcabi-email进行相同操作的方法。

我认为差异很明显。

在第一个例子中,你正在处理一个怪兽类,它可以为你完成一切,包括通过SMTP发送你的MIME消息,创建消息,配置其参数,添加MIME部件等等。commons-email中的Email类确实是一个庞大的类—有33个私有属性,一百多个方法,大约两千行代码。首先,你通过一系列的setter方法配置该类,然后要求它为你send()一封电子邮件。

在第二个例子中,我们通过七个new调用实例化了七个对象。Postman负责打包MIME消息;SMTP负责通过SMTP发送消息;邮戳(StSenderStRecipientStSubject)负责在交付前配置MIME消息;附件EnPlain负责为我们将要发送的消息创建一个MIME部件。我们将这七个对象封装在一起,然后要求邮递员为我们send()信封。

从用户角度来看,几乎没有什么问题。Email 是一个功能强大的类,具有多个控制项—只需选择正确的控制项,工作就能完成。然而,从开发者角度来看,Email 类是一场噩梦。主要是因为这个类非常庞大且难以维护。

由于这个类太大,每当你想通过引入一个新方法来扩展它时,你都会面临一个事实,即你正在使这个类变得更糟糕—更长、缺乏凝聚性、难以阅读、难以维护等等。你会感觉自己在深入挖掘一些肮脏的东西,而且永远没有希望使它变得更干净。我相信你对这种感觉很熟悉—大多数遗留应用程序都是这样的。它们有巨大的多行“类”(实际上是用 Java 编写的 COBOL 程序),这些“类”是由你之前的几代程序员继承而来的。当你开始时,你充满了活力,但是在滚动这样一个“类”的几分钟后,你会说—“算了吧,都快到周六了。”

由于这个类太大,不再有数据隐藏或封装—33个变量可以被100多个方法访问。有什么是隐藏的吗?这个 Email.java 文件实际上是一个大型的过程化2000行脚本,错误地称为“类”。一旦你通过调用其中一个方法穿越了类的边界,就没有什么是隐藏的了。在那之后,你可以完全访问你可能需要的所有数据。这为什么不好?好吧,我们为什么需要封装呢?为了保护一个程序员免受另一个程序员的干扰,也就是所谓的防御性编程。当我忙于改变 MIME 消息的主题时,我希望确保我不会被其他方法的活动干扰,这些活动会错误地更改发送者并触及我的主题。封装帮助我们缩小问题的范围,而这个 Email 类却完全相反。

由于这个类太大,它的单元测试比类本身还要复杂。为什么?因为它的方法和属性之间存在多重依赖关系。为了测试 setCharset() 方法,你必须通过调用其他几个方法来准备整个对象,然后你必须调用 send() 方法来确保发送的消息实际上使用了你指定的编码。因此,为了测试一个一行代码的方法 setCharset(),你要运行整个集成测试场景,通过 SMTP 发送一个完整的 MIME 消息。显然,如果某个方法发生了变化,几乎每个测试方法都会受到影响。换句话说,测试非常脆弱、不可靠且过于复杂。

我可以继续谈论这个“由于这个类太大”,但我认为一个小而凝聚的类总是比一个大类好是显而易见的。对我来说、对你来说,对任何面向对象的程序员来说都是显而易见的。但是为什么对 Apache Commons Email 的开发者来说却不是那么明显呢?我不认为他们愚蠢或者没受过教育。那么是什么原因呢?

这就是经常发生的事情。一开始,你将一个类设计成一个紧密、坚固且小型的整体。你的意图非常积极。很快你就会意识到这个类还有其他任务要完成。然后,又有其他任务。然后,还有更多任务。

让类变得越来越强大的最好方法是通过添加setter将配置参数注入类中,以便在内部进行处理,不是吗?

这就是问题的根源!问题的根本原因是我们通过配置方法(也称为”setter”)将数据插入可变对象的能力。当一个对象是可变的并且允许我们随时添加setter时,我们会毫不限制地这样做。

让我这样说——可变类倾向于变得越来越庞大,失去紧密性。

如果commons-email的作者在一开始就使这个”Email”类是不可变的,他们就无法添加那么多方法和封装那么多属性。他们也无法将其变成一个庞然大物。为什么?因为不可变对象只能通过构造函数接受状态。你能想象一个有33个参数的构造函数吗?当然不能。

当你一开始就使你的类不可变时,你被迫保持紧密、小型、坚固和稳定。因为你不能封装太多内容,也不能修改封装的内容。只需两个或三个构造函数参数,就完成了。

当我设计 jcabi-email 时,我从一个小而简单的类开始:Postman。嗯,它是一个接口,因为我从不创建没有接口的类。所以,Postman 是……一名邮递员。他将消息递送给其他人。首先,我创建了一个默认版本(为了简洁起见,我省略了构造函数):

不错的开始,它可以工作。现在怎么办?嗯,Message很难构建。它是一个复杂的JDK类,需要一些操作才能变成一个漂亮的HTML电子邮件。所以我创建了一个信封,它将为我构建这个复杂的对象(请注意,PostmanEnvelope都是不可变的,并且用@Immutable注释,来自jcabi-aspects)。

我还重构了 Postman,使其接受一个信封而不是一个消息:

到目前为止,一切顺利。现在让我们尝试创建一个简单的“Envelope”实现。

它可以工作,但目前还没有任何有用的功能。它只会创建一个完全空的MIME消息并返回它。要不要给它添加一个主题,以及To:From:地址(请注意,MIME类也是不可变的)?

看起来正确并且能工作。但还是太简陋了。CC:BCC:怎么办?电子邮件正文呢?PDF附件怎么办?如果我想指定消息的编码方式怎么办?那Reply-To怎么办?

我能把所有这些参数都添加到构造函数里吗?记住,这个类是不可变的,我不能引入setReplyTo()方法。我必须将replyTo参数传递给它的构造函数。但这是不可能的,因为构造函数会有太多的参数,没有人能够使用它。

嗯,我开始思考:我们如何将“信封”的概念分解为更小的概念-这就是我发明的。就像现实中的信封一样,我的MIME对象将有邮票。邮票将负责配置一个Message对象(再次,Stamp是不可变的,以及其所有的实现者)。

现在,我可以将我的MIME类简化为以下内容:

现在,我将为主题、To:From:CC:BCC:等创建邮戳。任意数量的邮戳。MIME类将保持不变——小巧、内聚性强、易读、稳定等等。

重要的是,我为什么决定在类相对较小的时候进行重构。事实上,在MIME类只有25行代码时,我就开始担心这些邮戳类了。

这正是本文章的重点——不可变性强迫你设计小巧且内聚的对象

如果没有不可变性,我会像commons-email一样走相同的路。我的MIME类会不断增加,最终变得和commons-email中的Email类一样大。唯一阻止我这样做的是重构的必要性,因为我无法通过构造函数传递所有参数。

如果没有不可变性,我就没有这个动力,就会像Apache开发者在commons-email中所做的那样,使类变得臃肿,变成一个难以维护的怪兽。

那就是jcabi-email。希望这个例子足够说明问题,并且你将开始使用不可变对象编写更干净的代码。

Translated by ChatGPT gpt-3.5-turbo/42 on 2023-11-28 at 15:35

sixnines availability badge   GitHub stars