领域驱动设计中的值对象 - 为什么要是不可变的?

45
我不明白为什么DDD中的值对象应该是不可变的,也不知道如何轻松实现。(如果有影响,我关注的是C#和Entity Framework。)
例如,让我们考虑经典的地址值对象。如果您需要将“123 Main St”更改为“123 Main Street”,为什么我需要构建一个全新的对象,而不是说myCustomer.Address.AddressLine1 =“123 Main Street”?(即使Entity Framework支持结构体,这仍然是一个问题,不是吗?)
我理解(我想)值对象没有身份并且是域对象的一部分的想法,但是有人能解释为什么不可变性是一件好事吗?

编辑:我的最终问题应该是“为什么在值对象中应用不可变性是一件好事?”请有人解释一下。抱歉造成的困惑!


编辑:澄清一下,我不是在问CLR值类型(与引用类型相对)。我在问更高级别的DDD概念——值对象。

例如,这是一种实现Entity Framework不可变值类型的hack方法:http://rogeralsing.com/2009/05/21/entity-framework-4-immutable-value-objects。基本上,他只是将所有setter设置为私有的。为什么要费这个劲呢?


通常在讨论 Address (struct) 值类型和 String (class) 属性值引用类型时,值类型和引用类型会相互混合。限制问题只涉及值类型是否有优势?你能否开放一下而不削弱你的意图? - John K
我不确定我理解了。我不是在谈论CLR值类型(结构体,整数等)与引用类型(类等)之间的区别。我谈论的是DDD概念中的值对象(相对于实体对象)。 - Hobbes
我扫了一眼问题,看到C#,我的思维跳跃了。感谢澄清。 - John K
值对象不需要实际上是不可变的来避免别名问题。只需将它们作为值传递而不是引用,您就可以解决问题。请参见https://wiki.c2.com/?ValueObjectsCanBeMutable。 - Géry Ogam
6个回答

62

不要理会那些关于线程安全等与DDD无关的荒谬答案。 (我还没有看到过一个线程安全的O/R映射器或其他适合DDD的数据访问层)

想象一个重量值对象。 比如说我们有一个表示千克的值对象。

示例(已编辑以提高清晰度):

var kg75 = new Weight(75);
joe.Weight = kg75;
jimmy.Weight = kg75;

如果我们这样做会发生什么:

jimmy.Weight.Value = 82;
那将改变乔的体重,如果我们仍在使用相同的对象引用。请注意,我们将表示75公斤的对象分配给了乔和吉米。当吉米增重时,改变的不是kg75对象,而是吉米的体重,因此,我们应该创建一个表示82公斤的新对象。 但是,如果我们有一个新会话并且在干净的UoW中加载乔和吉米怎么办?
 var joe = context.People.Where(p => p.Name = "joe").First();
 var jimmy = context.People.Where(p => p.Name = "jimmy").First();
 jimmy.Weight.Value = 82;

那么会发生什么呢?在您的情况下,由于EF4将加载Joe和Jimmy及其重量而没有标识,我们将得到两个不同的重量对象,当我们更改Jimmy的重量时,Joe仍然与之前一样重。

因此,相同的代码会有两种不同的行为。如果对象引用仍然相同,则Joe和Jimmy都会得到一个新的重量。如果Joe和Jimmy在干净的UOW中被加载,只有其中一个会受到更改的影响。

我认为这非常不一致。

通过使用不可变的VO,您可以在这两种情况下获得相同的行为,并且在构建对象图时仍然可以重用对象引用以减少内存占用。


7
这个论点有缺陷。你从技术角度争辩某个概念的问题,而这个概念是基于概念性原因选择的。价值对象和实体这些概念存在于任何具体实现之外。 - jason
3
无论您使用哪种技术,都需要不可变的值对象。例如,体重为75公斤的对象是不可变的,无论您使用EF4或其他任何技术。如果某人的体重发生了变化,不应该改变代表75公斤的对象,而应该将新的体重分配给该人。 - Roger Johansson
8
仅仅讨论理论方面并不能很好地说明为什么如果忽略这些方面会陷入麻烦。在我看来,如果你能在更具体的场景中看到某些事情为什么会失败,那么理解这些概念就更容易了。 - Roger Johansson
1
@Roger Alsing:你的观点只是说为什么可变性有时会让你陷入麻烦,但它并没有论证为什么值对象应该是不可变的。事实上,你从未提到值对象的定义,这应该是一个暗示,说明有些事情出了问题。 - jason
2
+1。用一个很好的例子回答了这个问题,类似于Martin Fowler的解释(http://c2.com/cgi/wiki?ValueObjectsShouldBeImmutable)。 - trebormf
显示剩余5条评论

45

为什么数字6是不可变的?

理解这一点,你就会明白为什么值对象应该是不可变的。

编辑:我将我们的对话移到了这个回答中。

6 是不可变的,因为 6 的身份由它所代表的状态决定,也就是拥有六个东西的状态。你不能改变 6 代表这个状态的事实。这是值对象的基本概念。它们的值由它们的状态确定。而实体并不是由其状态确定的。一个 Customer 可以更改他们的姓氏或地址,但仍然是同一个 Customer。这就是为什么值对象应该是不可变的。它们的状态决定了它们的身份;如果它们的状态发生了改变,它们的身份也应该随之改变。


2
我看过像这样的例子,它们似乎太简单了。6是不可变的,因为像String一样,对它执行的任何操作都会给你一个新对象。6+2不是将数字6更改为8的数字6;它是一个新数字。我明白了,但我不喜欢这个比喻。在Address的情况下,类型更复杂,定义返回新类型的操作更困难(并且感觉很傻)。例如,newAddress = oldAddress.ChangeLine1("123 Main Street")? - Hobbes
1
六永远是六。直觉上,Console.WriteLine(6)应该总是“6”。这个例子足够简单,但我很难将其应用到地址的概念中。从你那里得到的感觉是,说jason.Address.Line1 =“123 Main”有些危险,最好说jason.Address = new Address(...)。我仍然不知道为什么。 - Hobbes
9
@Hobbes: 好的,但是为什么“6”始终是“6”?这是因为“6”的身份是由它所代表的东西决定的,即拥有六个某物的状态。你不能改变“6”代表这一点。现在,这就是价值对象的基本概念。它们的价值是由它们的状态确定的。然而,实体并不取决于它的状态。“客户”可以更改他们的姓氏或地址,仍然是同一个“客户”。这就是价值对象应该是不可变的原因。它们的状态确定了它们的身份;如果它们的状态发生了改变,它们的身份也应该发生改变。 - jason
2
最后一条评论完美地点破了它。 - Arnis Lapsa
1
@Hobbes:假设有人说 jason.Address = fred.Address; jason.Address.Line1 = "123 Main"; 那么这对 fred.Address 会产生什么影响(如果有的话)?它是否应该不受影响?对于某些类型,例如 Address,具有公开字段的结构体可能提供最合理的语义,其类型的每个变量或字段都与其他变量或字段“分离”(并且它是一个具有公开字段的结构体,这一事实很容易被注意到)。 - supercat
显示剩余3条评论

16

我来晚了,但我一直在思考这个问题。(欢迎任何评论。)

我认为Evans提到不变性的引用主要是在共享的上下文中:

为了安全地共享对象,它必须是不可变的:它只能通过完全替换来改变。(Evans p100)

在Evan的书中还有一个名为“地址是否是值对象?谁在问?” 的侧边栏。

如果室友都打电话订购电力服务[即两个客户具有相同的地址],公司将需要意识到这一点。因此,地址是一个实体。(Evans p98)

在您提供的示例中,假设客户的家庭地址和工作地址都是123 Main Street。当您进行所描述的更正时,两个地址都会更改吗?如果是这样,并且如果我正确理解Evans,那么听起来您真的拥有一个实体。

以另一个例子为例,假设我们有一个对象来表示客户的全名:

public class FullName
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

public class Customer
{
    public FullName Name { get; set; }
}

没有值对象,以下操作将失败:
[Test]
public void SomeTest() {
    var fullname = new FullName { FirstName = "Alice", LastName = "Jones" };
    var customer1 = new Customer { Name = fullname };
    var customer2 = new Customer { Name = fullname };

    // Customer 1 gets married.
    customer1.Name.LastName = "Smith";

    // Presumably Customer 2 shouldn't get their name changed.
    // However the following will fail.
    Assert.AreEqual("Jones", customer2.Name.LastName);
}

就一般而言,在DDD中,值对象有哪些实际优势被认为是其中之一。值得注意的是,您只需要在创建VO时验证一次。如果这样做,那么您就知道它始终是有效的。


谢谢您。Evan的解释很有启发性,您的例子真正让人理解了它。 - Justin J Stark
这绝对是最简单和最好的答案。它使用了问题中的示例,并展示了如果不是一个VO,它将会失败。 - Alisson Reinaldo Silva
为什么你一开始没有写出 var fullname1 = new FullName { FirstName = "Alice", LastName = "Jones" };var fullname2 = new FullName { FirstName = "Alice", LastName = "Jones" };,以避免遇到这个问题? - rahulaga-msft

5

这可能不是完整的答案。我只回答了您关于Immutability优点的问题。

  1. 因为Immutable对象是线程安全的。由于它们无法更改状态,因此它们不会受到线程干扰的破坏或在不一致状态下被观察。
  2. 对于Immutable对象的引用可以轻松共享或缓存,而无需复制或克隆它们,因为它们的状态在构建后永远不会改变。
  3. 有关Immutability的更多优点,请参见此处(LBushkin's answer) What's the advantage of a String being Immutable?

Martin Fowler给出了一个例子,说明为什么值对象应该是Immutable。

好的,尽管将VO设置为immutable并非强制性要求(即使DDD书籍也没有说必须是immutable),但在DDD中,主要思想似乎是将其作为VO以避免处理实体的生命周期复杂性。请参见此处了解更多详情。


任何从技术角度出发的论点都是错误的。你从来没有用到我们在这里谈论的是值对象的事实。你对string的类比是错误的,因为string是引用类型。你的论点实际上是关于不可变类型的好处,而不是为什么值对象应该是不可变的。 - jason
1
这里难道不是暗示使用值对象吗?此外,问题是“有人能解释为什么不变性是一件好事吗?”现在来看我提供的链接;我没有逐字复制优点,而是指定了一个链接,其中讨论了不变性的优点。那么你能告诉我为什么地址不是引用类型吗? - Aravind Yarram
Pangea:很好的观点。我问“为什么不变性是一件好事”,但我真正想知道的是,“为什么对于值对象来说,不变性是一件好事?”我会编辑我的问题,使其更加清晰明了。 - Hobbes
我已经阅读了您发布的c2.com链接。我认为他们并没有谈论领域驱动设计? - Hobbes
@Hobbes - 我已经更新了我的答案。请看最后一段。希望这能澄清一些问题。 - Aravind Yarram
我会这样重新表述@Pangea的观点:在大多数实际情况下(考虑您选择的PL和其他技术),使用不可变对象可以更容易地编写正确的程序,而值对象是它们的良好匹配。同时,这也是一个有效的观点,即使对于VO来说,使其不可变也并非强制要求。 - Alexey

2
很久以前就有人提出了这个问题,但我决定用一个例子来回答,我认为这个例子很直接简单易懂。此外,SO作为许多开发者的参考,我认为任何遇到这个问题的人都可以从中受益。
由于它们是通过其属性定义的,所以值对象被视为不可变的。一个好的例子是货币。你无法区分口袋里相同的五张一美元钞票,但你并不关心货币的身份,只关心它的价值和代表的意义。如果有人拿走了你钱包里的一张五美元钞票并换成另一张,这并不会改变你仍然拥有五美元的事实。
因此,例如在C#中,你将金钱定义为不可变的值对象:
public class Money
{
    protected readonly decimal Value;

    public Money(decimal value)
    {
        Value = value;
    }

    public Money Add(Money money)
    {
        return new Money(Value + money.Value);
    }

    // ...


    // Equality (operators, Equals etc) overrides (here or in a Value Object Base class). 
    // For example:

    public override bool Equals(object obj)
    {
        return Equals(obj as Money);
    }

    public bool Equals(Money money)
    {
        if (money == null) return false;
        return money.Value == Value;
    }
}

2

价值对象需要是不可变的。

在许多情况下,不可变对象确实可以使生活更简单。... 它们可以使并发编程更加安全和清洁。更多信息

让我们考虑将值对象视为可变的。

class Name{
string firstName,middleName,lastName
....
setters/getters
}

假设你的原名是 Richard Thomas Cook

现在假设你只改变了firstName(改为Martin)和lastName(改为Bond),如果它不是一个不可变对象,那么你将使用方法逐个更改状态。在最终名称为Martin Thomas Bond之前,在该聚合状态下拥有名称Martin Thomas Cook从不可接受(它也会给后来查看代码的人带来错误的思考,导致进一步设计中不良的多米诺骨牌效应)。

可变值对象必须显式地强制执行1个事务中给定的更改的完整性约束,这在不可变对象中是免费的。因此,将值对象设置为不可变是有意义的。


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