了解最新技术文章
当第一次面对不可变类型的想法时,人们可能会说:“这永远行不通!不仅这个概念过于复杂,而且还会导致性能下降”。我们可以同情这种情绪。当被迫编写额外的代码来复制对象的值以引入单个更改时,人们应该认真质疑他们的目标。如果你发现自己在问这个问题,那么这篇文章就是给你的。
首先,让我们回顾一下禁止改变事物背后的动机,以及为什么这个概念最近越来越受欢迎。
稳定感觉不错。我们在众所周知的环境中运作得更好。因此,我们使用抽象和类比,使对象模型尽可能接近我们习惯的真实世界。与可变对象相比,不可变对象与现实生活中的项目有更多共同点。想象你拿着一本书。在地球的另一边,作者决定他们不再喜欢第 5 章,所以他们删除了它。你的书变得更薄了。这充其量是一件麻烦事。起诉作者抢了你这本书的一部分是公平的。即使是电子副本也永远不会像那样更新。相反,一本书只能以一种简单的方式更新:制作一个副本,并在分发之前对新副本进行更改。这称为新版本。为什么我们希望我们的书保持不变而不“升级”它们,即使,就像电子书一样,这样做相对容易?嗯,方便以后参考这本书。如果我们想与我们的同事讨论第 5 章,我们宁愿谈论同一件事。否则,我们会得到一个别名错误,在Martin Fowler 的这篇博文。
不可变类型的另一个特性——在异步环境中工作时的安全性。这在许多技术文章以及软件开发人员的工作面试中被大量提及。要点如下:对引用而不是对象字段的原子操作是确保共享数据完整性的唯一方法。这使我们能够控制数据的更改方式以及如何解决冲突的更改。
通常情况下,服务器端和客户端的软件都使用某种内存存储来存储数据。最有可能的是,用于此的数据结构依赖于对象的哈希码和相等性。这种结构是大多数平台的黄金标准。例如,在 Java 中,如果放置在地图中的元素没有一致的实现HashMap
,HashSet
则将无法正常运行。hashCode()
equals()
对于可变类型,不可能提供这样的实现。考虑一个可变User
类,它存储在HashSet
. 在某些时候,我们可能需要遍历集合中的所有用户并改变每一秒。在这样的突变之后,集合中充满了该方法无法再发现的对象contains()
,因为原始对象的哈希值与新对象的哈希值不同。
不可变对象永远不会出现这样的问题。如果字段永远不会改变,哈希码也不会改变。
绝对地。在很多情况下,不变性的代价太高了。
使用可变类型而不是不可变类型的最大原因是复制数据的成本。归根结底,系统中的数据需要随着时间的推移而改变。有时,这种变化经常发生。例如,创建一个额外的Builder
对象和原始对象的新副本弥补了内存中对单个常规可变对象的三次对象引用。还应该考虑创建原始对象的新副本所花费的 CPU 时间。
一些系统不允许这种资源使用。这种系统的一个很好的例子是聚合来自多个传感器的数据的软件。传感器是一个小型设备,本身没有太多内存,因此它会立即推送收集到的数据,希望接收端能够处理它。此类软件通常在不是很强大的硬件上运行,这对性能造成了严格的限制。
另一个可以从数据可变性中受益的系统示例是自动化股票市场经纪人,其设计目的是比竞争对手更快地处理来自市场的请求。这样的系统是专门为优化速度而编写的。
开发人员在考虑不变性时应该使用他们的最佳判断。如果权衡太高,则可以不全神贯注于不变性,仅在性能限制不那么苛刻的情况下使用它。
在 Java 中,生成的 Protobuf 类始终是不可变的。每个类都有一个嵌套Builder
类,它具有所有相同的字段但是可变的。为了创建这样一个类的实例,我们创建一个构建器,分配字段,并构建消息。
任务 task = Task.newBuilder()
.setName(“…”)
.setDescription(“ …”) .build
();
为了更改值,我们基于现有消息创建新消息。
任务更新 = task.toBuilder()
.setDescription(“…”) .build
();
Protobuf 生成代码的方式代表了 Java 中典型的不可变 API,不包括一些 Protobuf 特有的额外内容,例如反射等。
但是,不可变数据类型可以是您想要的任何形状和形式,或者更准确地说,是 API 用户需要的任何形状和形式。例如,java.lang.String
, java.io.File
, java.time.LocalDateTime
等类型都是不可变的数据类型。
不幸的是,Protobuf 不会为 JavaScript 和 Dart 生成不可变代码。事实上,JS 中消息对象的某些方面是可变的,而其他方面则不是。
Java 和客户端语言之间的这种差异可以通过希望使特定于客户端的 Protobuf 对象尽可能轻量和高性能来解释。然而,随着计算机越来越高效,这种权衡真的值得吗?在我们看来,事实并非如此。
不过,在 Dart 中,有一些工具以及语言结构可以帮助您实现不变性。例如,built_value
和freezed
是两个允许用户创建不可变数据类型的库,几乎不需要额外的努力。但是为了实现源自 Protobuf 的数据类型的不可变性,我们必须安排从可变类型到不可变类型的转换,这是很多手动工作。最后,我们会为一种 Protobuf 类型获得两个类。至少可以说,这是一个次优的解决方案。
我们尝试在built_value
Protobuf 和 Protobuf 之间创建共生关系,以实现不可变的“前置”类型,这将依赖标准 Protobuf Dart 代码进行序列化。这种尝试失败了,因为我们发现自己在处理太多不连贯的抽象。在撰写本文时,我们计划实现自定义代码生成以支持 Dart、JavaScript 和 Java,从而解决我们面临的限制并提供不变性。
总而言之,不变性是一个有用的概念,原因有很多。首先,它通过与现实世界中的物品建立直接的类比来帮助人们理解物品的生命周期。其次,它具有技术优势,例如与数据结构的关系更清晰,异步安全。尽管在某些情况下,出于性能原因,可变类型仍然更可取,但大多数系统可以享受不可变的好处,而无需额外担心。
在 Java 中,Protobuf 通过生成构建和复制对象所需的所有代码,帮助我们轻松地创建不可变数据类型。还有一些工具可用于其他语言的此任务,例如 Dart。
除了简单性和技术优势之外,不可变数据类型之所以重要还有一个更大的原因。他们帮助我们构建基于领域驱动设计的项目。在本系列的以下部分中,我们将讨论 DDD 以及 Protobuf 如何帮助我们构建领域模型,从值对象开始,这是一种将领域概念转换为机器语言的简单而强大的模式。