How an Immutable Object Can Have State and Behavior?

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

在对不可变对象的争论中,我经常听到这样的观点:“是的,在状态不变时,它们很有用。然而,在我们的情况下,我们处理的是经常变化的对象。我们简直无法负担每次只需要更改其title时就创建一个新的document。”在这一点上,我不同意:如果您需要频繁更改标题,那么标题不是文档的状态,而是文档的行为。如果一个对象是一个良好的对象,即使标题经常更改,它仍然可以并且必须是不可变的。让我解释一下。

基本上,每个对象都有三个要素:身份、状态和行为。身份是区别我们的document与其他对象的特征,状态是一个文档对自身了解的内容(也称为”封装的知识”),而行为是文档能够根据我们的请求为我们做的事情。例如,这是一个可变的文档:

让我们尝试使用这个可变对象:

在这里,我们正在创建两个对象,然后修改它们封装的状态。显然,first.equals(second)会返回false,因为这两个对象具有不同的身份,尽管它们封装了相同的状态。

toString()方法暴露了文档的行为—文档可以将自己转换为字符串。

为了修改文档的标题,我们只需要再次调用它的setTitle()方法。

简单来说,我们可以多次重用这个对象,并修改它的内部状态。这样做既快速又方便,不是吗?快速,是的。方便,其实并不完全如此。请继续阅读。

正如我之前提到的,不可变性是一个良好对象的美德之一,也是非常重要的一个。一个良好的对象是不可变的,而良好的软件只包含不可变的对象。不可变对象与可变对象的主要区别在于不可变对象没有身份,其状态永远不会改变。以下是同一文档的一个不可变变体:

这个文档是不可变的,它的状态(idtitle)就是它的身份。让我们看看如何使用这个不可变类(顺便说一下,我正在使用jcabi-aspects@Immutable注解)。

我们不能再修改一个文档了。当我们需要改变标题时,我们必须创建一个新的文档。

每当我们想要修改其封装状态时,我们也必须修改其身份,因为没有身份。状态就是身份。看一下上面的equals()方法的代码—它通过比较文档的ID和标题来比较文档。现在,文档的ID+标题就是它的身份!

现在我要谈到我们一开始的问题了:性能和便利性如何?我们不想每次修改标题时都要改变整个文档。如果文档足够大,那将是一个巨大的负担。此外,如果一个不可变对象封装了其他不可变对象,在修改其中一个对象中的单个字符串时,我们必须改变整个层次结构。

答案很简单。文档的标题不应该是其状态的一部分。相反,标题应该是其行为。例如,考虑以下情况:

从概念上讲,这个文档充当了一个真实文档的代理,该文档的标题存储在某个地方,比如一个文件中。这是一个好对象应该做的事情——成为真实实体的代理。该文档提供了两个功能:读取标题和保存标题。以下是它的接口示例:

title() reads the title of the document and returns it as a String, and title(String) saves it back into the document. Imagine a real paper document with a title. You ask an object to read that title from the paper or to erase an existing one and write new text over it. This paper is a “copy” utilized in these methods.

现在我们可以对不可变文档进行频繁更改,而文档仍然保持不变。它不会停止是不可变的,因为它的状态(id)没有改变。即使我们更改了它的标题,它仍然是同一份文档,因为标题不是文档的状态。它是现实世界中文档之外的东西。文档只是我们和那个“东西”之间的代理。阅读和写入标题是文档的行为,而不是它的状态。

我们唯一尚未回答的问题是什么是这个“副本”,如果我们需要将文档的标题保存在内存中会发生什么?

让我们从“对象思维”的角度来看待它。我们有一个document对象,它应该代表在面向对象的世界中的一个真实实体。如果这样的实体是一个文件,我们可以轻松实现title()方法。如果这样的实体是一个Amazon S3对象,我们也可以轻松实现标题的读写方法,保持对象不可变。如果这样的实体是一个HTTP页面,我们在标题的读写实现上没有任何问题,保持对象不可变。只要真实的世界文档存在并具有自己的身份,我们的标题读写方法就会与该真实世界文档进行通信,提取或更新其标题。

当这样的实体在真实世界中不存在时,问题就会出现。在这种情况下,我们需要创建一个可变的对象属性,称为title,通过title()方法读取它,并通过title(String)方法修改它。但是对象是不可变的,所以根据定义,我们不能在其中拥有可变的属性!我们该怎么办?

我们的对象怎么会不代表一个真实的实体呢?记住,“现实世界”是指对象周围的生活环境。一个对象怎么可能不代表任何人而自行行动呢?不可能。每个对象都是一个真实实体的代表。那么,如果我们想要在对象内部保留“标题”,又没有任何文件或HTTP页面与对象相关联,它代表的是谁呢?

它代表的是“计算机内存”。

不可变文档#50的标题“如何烤三明治”存储在内存中,占用了23个字节的空间。文档应该知道这些字节存储在哪里,应该能够读取它们并替换为其他内容。这23个字节是对象所代表的真实实体。这些字节与对象的状态无关。它们是一个可变的真实实体,类似于文件、HTTP页面或亚马逊S3对象。

不幸的是,Java(以及其他许多现代语言)不允许直接访问计算机内存。如果能够直接访问计算机内存,我们将如何设计我们的类:

Memory 类将由 JDK 本身实现,并且所有其他类都将是不可变的。Memory 类将直接访问内存堆,并负责操作操作系统级别的 mallocfree 操作。拥有这样的类将使我们能够使所有的 Java 类都变为不可变的,包括 StringBufferByteArrayOutputStream 等。

Memory 类将明确强调软件程序中对象的任务,即成为数据动画师。对象不是保存数据;它是将数据进行动画处理。数据存在于某个地方,它是贫血的、静态的、不动的、静止的等等。数据在对象活着的时候是死的。对象的角色是使一段数据活起来,将其进行动画处理,而不是成为一段数据。为了访问那段已经死了的数据,对象需要一些知识。对象可能需要一个数据库的唯一键、一个 HTTP 地址、一个文件名或者一个内存地址来找到数据并进行动画处理。但是对象不应该把自己看作是数据。

不幸的是,在Java、Ruby、JavaScript、Python、PHP和许多其他高级语言中,我们没有这样的内存表示类。看起来语言设计者没有理解活动对象与死数据的概念,这很遗憾。我们被迫使用相同的语言结构混合数据与对象状态:对象变量和属性。也许有一天我们会在Java和其他语言中拥有Memory类,但在那之前,我们有几个选择。

使用C++。在C++和类似的低级语言中,可以直接访问内存并处理内存中的数据,就像处理文件或HTTP数据一样。在C++中,我们可以创建那个Memory类,并且可以按照我们上面解释的方式使用它。

使用数组。在Java中,数组是一种具有独特属性的数据结构——它可以在声明为final的同时被修改。您可以将字节数组用作不可变对象内部的可变数据结构。这是一个类似于Memory类的概念解决方案,但更为原始。

避免内存中的数据。尽量避免使用内存中的数据。在某些领域,这很容易实现;例如,在Web应用程序、文件处理、I/O适配器等中。然而,在其他领域中,这说起来比做起来要困难得多。例如,在游戏、数据操作算法和GUI中,大多数对象都在内存中操纵数据,主要是因为内存是它们唯一拥有的资源。在这种情况下,如果没有Memory类,您最终会得到可变对象:( 没有变通的办法。

总之,不要忘记对象是数据的动画师。它使用封装的知识来访问数据。无论数据存储在文件、HTTP还是内存中,从概念上讲,它与对象状态非常不同,尽管它们看起来非常相似。

一个好的对象是可变数据的不可变动画师。尽管它是不可变的,数据是可变的,在对象的生存环境中,它是活的,而数据是死的。

Translated by ChatGPT gpt-3.5-turbo/42 on 2023-12-05 at 21:23

sixnines availability badge   GitHub stars