Java:如何处理“几乎”不可变的数据结构?

5

不可变对象非常好,因为它们在多线程应用程序中不需要特别的关心或维护。然而,许多对象几乎不能自然地成为不可变对象。例如,一个订单被提交,处理后,在订单填写时分配一个永久ID。该ID无法在订单创建和提交时给出,但稍后(或者可能永远)到达。

可能的解决方案:

  1. 在订单创建时分配另一个唯一ID。然后,当永久(订单已满)ID到来时,将其存储在Map中。因此,订单类将是不可变的。然后,如果Map键不存在,则我们会知道订单尚未填写。(Map应该是静态类字段吗?)
  2. 另一种解决方案是使永久ID字段可变,并应用适当的同步。此外,我们可以限制ID仅在对象的生命周期内设置一次。

这些解决方案合理吗?还有其他想法吗?谢谢。


1
在我的工作中,我们的ERP系统每年处理超过10亿美元的销售额,因此销售量可能是一个因素,但我几乎从未见过不改变的订单。你确定将订单设置为不可变是个好主意吗? - corsiKa
我的订单的其他部分也会发生变化,但同样可以用新的不可变订单替换。我不确定是按照ColinD建议的使用withX(..)还是像Stephen C建议的那样进行同步更好。还涉及到一些其他问题,例如应用程序显示(GUI或控制台)。我认为选择哪种方式取决于它将如何使用。在我的情况下,我仍在分析中。 - Pete
5个回答

9

将类设置为不可变,并将null设置为ID的有效值。当您有要分配的ID时,用一个新的对象替换现有的不可变对象,该对象与旧对象完全相同,只是具有新的ID而不是旧的ID。我喜欢使用名为withX的方法来实现这个目的。

Foo foo = new Foo("bar");
...
foo = foo.withId(12345); // replace foo with new derived object

2
问题在于找到所有引用“旧”Foo的地方,并将它们替换为带有ID的“新”版本。 - Stephen C
@Stephen:这是不可变对象的经典问题。然而,在多线程环境中,通常更喜欢对对象的更改是显式的,并且由所有者控制,而不是在每个线程中立即发生。 - Russ Hayward
2
@Stephen C:这只意味着您必须小心地控制引用的位置。您通常需要通知应用程序的某些部分实例已经发生了变化。 - ColinD

3

对我来说,它们听起来像是两个不同的对象,一个是“订单”,另一个是“已完成订单”。其中一个是另一个的副本,并带有一个id...

你对不可变性感兴趣的原因是什么?它将如何影响您的领域模型?例如,我曾经看到过“领域”对象具有id字段,只是为了取悦Hibernate,但它们在Hibernate给它们分配id值之前并不具备id值。在这种情况下,“领域”对象很弱。

从“业务透视”的对象模型(在我的学术示例中)来看,似乎不需要id。然而,从“技术透视”的模型来看,需要一个id(更具体地说,Hibernate需要一个id)。显然存在紧张关系,所以我喜欢清楚地表达我试图建模的内容(业务或技术)。

顺便问一下,在您的示例中,id代表什么?

因此,当我们考虑“身份”(以Eric Evans的意义)的概念时,为了使一个对象或实体存在,它必须具有一个身份(如果它们的身份相等,则实体相等,无论其内容是否相等)。对我来说,这意味着

新建一个没有身份(在这种情况下是id)的实体是没有意义的

我还建议

不存在“几乎”不可变的对象,它要么是不可变的,要么不是

如果您使用上述解决方法,则应明确指出您的对象不再是不可变的。这可能没问题(再次问问自己为什么需要不可变性?)。我认为,拥有可以具有null id的领域对象(稍后将其替换为副本)并不是一个好主意。这会产生“特殊情况”,可以通过不同的建模方式避免,并使您在许多地方处理特殊情况(可能性)。


2

我认为第二种方法最好,必要时使用同步。延迟初始化和同步对象getter的开销可能是微不足道的。

我还怀疑,如果比较完整的实现,第一种方法将具有等效(或更差)的开销。例如,它将需要同步Map,或者使用一个具有自身开销的并发映射。而且,该映射是一个共享数据结构,这意味着争用的机会比通常未共享对象上的getter更高。


完全同意。当你正确使用它时,同步是一个强大的工具。但像任何工具一样,如果你使用不当,非常非常糟糕的事情就会发生。 - corsiKa

1

定义一个ID持有者类,其中包含一个ID字段。当创建订单时,使用空白ID创建一个新的ID持有者对象,并将其分配给订单。订单本身在技术上是浅不可变的,但只要不尝试为不同的订单“重复使用”ID持有者对象,就可以对ID持有者对象进行的唯一更改是应该应用于订单或其任何副本的更改。


-1
通过以下准则使类成为不可变的:
a)确保该类无法被覆盖 - 使该类为final,或使用静态工厂并保持构造函数私有
b)将字段设置为private和final
c)强制调用者在单个步骤中完全构建对象,而不是使用无参数构造函数结合后续调用setXXX方法(即避免Java Beans约定)
d)不提供任何可以以任何方式更改对象状态的方法 - 不仅是setXXX方法,还包括任何可以更改状态的方法
e)如果该类具有任何可变对象字段,则在类与其调用方之间传递时必须进行防御性复制
参考链接:http://www.javapractices.com/topic/TopicAction.do?Id=29

问题并不要求提供使事物不可变的指南。 - Toby

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接