DDD 中的值对象 - 为啥是不可变的?

Posted

技术标签:

【中文标题】DDD 中的值对象 - 为啥是不可变的?【英文标题】:Value objects in DDD - Why immutable?DDD 中的值对象 - 为什么是不可变的? 【发布时间】:2011-06-02 16:01:01 【问题描述】:

我不明白为什么 DDD 中的值对象应该是不可变的,我也不明白这是如何轻松完成的。 (如果这很重要,我将专注于 C# 和实体框架。)

例如,让我们考虑经典的 Address 值对象。如果您需要将“123 Main St”更改为“123 Main Street”,为什么我需要构造一个全新的对象而不是说 myCustomer.Address.AddressLine1 = “123大街”? (即使实体框架支持结构,这仍然是个问题,不是吗?)

我理解(我认为)值对象没有身份并且是域对象的一部分的想法,但是有人可以解释为什么不变性是一件好事吗?


编辑:我的最后一个问题真的应该是“有人可以解释为什么不变性是一件好事应用于值对象?”很抱歉造成混乱!


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

例如,这是一种为实体框架实现不可变值类型的 hack-ish 方法:http://rogeralsing.com/2009/05/21/entity-framework-4-immutable-value-objects。基本上,他只是将所有二传手设为私有。为什么要这么麻烦?

【问题讨论】:

在谈到地址(结构)值类型和字符串(类)属性值引用类型时,值和引用类型通常混合在一起,如您原来的问题所示。将您的问题仅限于值类型有什么好处吗?你能在不稀释你的意图的情况下打开它吗? 我不确定我是否遵循。我不是在谈论 CLR 值类型(结构、整数等)与引用类型(类等)。我说的是值对象(与实体对象相对)的 DDD 概念。 我浏览了这个问题,看到了 C#,我的思绪跳了起来。感谢您的澄清。 值对象实际上不需要是不可变的以避免别名问题。只需通过值而不是通过引用传递它们,你会没事的。见wiki.c2.com/?ValueObjectsCanBeMutable。 【参考方案1】:

忽略所有关于线程安全等的疯狂答案,这与 DDD 无关。 (我还没有看到线程安全的 O/R 映射器或其他对 DDD 友好的 dal)

想象一个权重的值对象。 假设我们有一个 KG 值对象。

示例(为清晰起见进行了编辑):

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

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

jimmy.Weight.Value = 82;

如果我们仍然使用相同的对象引用,那也会改变 joe 的权重。 请注意,我们为 joe 和 jimmy 分配了一个代表 75kg 的对象。 当jimmy变重的时候,不是kg75的物体变了,而是jimmy的体重变了,所以我们应该新建一个代表82kg的物体。

但是如果我们有一个新的会话并在一个干净的 UoW 中加载 joe 和 jimmy 呢?

 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 以及它们的权重而没有任何标识,我们将得到两个不同的权重对象,当我们更改 jimmys weight 时,joe 的重量仍然与以前相同。

因此,对于相同的代码,我们会有两种不同的行为。 如果对象引用仍然相同,那么 joe 和 jimmy 都会获得一个新的权重。 如果 joe 和 jimmy 以干净的 uow 加载,则只有其中一个会受到更改的影响。

这在 imo 中是相当不一致的。

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

【讨论】:

这个论点是有缺陷的。您是从技术角度为出于概念原因而选择的事物进行争论。这些值对象和实体的概念存在于任何特定实现之外。 它涵盖了为什么我们需要不可变的值对象,无论您使用特定技术还是什么,无论您使用 EF4 还是任何其他技术,75kg 的重量都是不可变的。如果某人的体重发生了变化,那么应该改变的不是代表 75 公斤的物体,而是应该为那个人分配一个新的重量.. 仅抛开理论方面的内容并不能完全说明如果忽略这些方面会遇到麻烦。 IMO,如果您能在更具体的场景中看到为什么某些事情会失败,就更容易理解为什么存在这些概念。 @Roger Alsing:你的论点只是说明了为什么可变性有时会给你带来麻烦。它没有争论为什么值对象应该是不可变的。您从未提及值对象的定义这一事实应该是有问题的线索。 +1。用一个很好的例子回答了这个问题。类似于 Martin Fowler 的解释 (c2.com/cgi/wiki?ValueObjectsShouldBeImmutable)。【参考方案2】:

很久以前就有人问过这个问题,但我决定用一个我觉得简单易记的例子来提供答案。此外,SO 可以作为许多开发人员的参考,我认为遇到这个问题的任何人都可以从中受益。

因为它们是由它们的属性定义的,所以值对象被视为不可变

价值对象的一个​​很好的例子是金钱。口袋里的五张一美元钞票你无法区分,这没关系。你不关心货币的身份——只关心它的价值和它代表什么。如果有人用你钱包里的一张五美元钞票换了一张,这不会改变你还有五美元的事实。

因此,例如,在 C# 中,您将 money 定义为不可变的值对象:

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;
    

【讨论】:

【参考方案3】:

值对象需要是不可变的。

在许多情况下,不可变对象确实让生活变得更简单。 ...而且他们可以让并发编程方式更安全、更干净for more info

让我们认为值对象是可变的。

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

假设你原来的名字是 Richard Thomas Cook

现在假设您只更改了名字(为 Martin)和姓氏(为 Bond), 如果它不是不可变对象,您将使用这些方法一一改变状态。名字为 Martin Thomas CookMartin Thomas Bond 的最终名字之前处于那种聚合状态是绝对不能接受的(这也给那些稍后查看代码,导致在进一步设计中产生不良的多米诺骨牌效应)。

可变值对象必须明确地对 1 个事务中给出的更改强制执行完整性约束,而在不可变对象中是免费给出的。 因此,使值对象不可变是有意义的。

【讨论】:

【参考方案4】:

我参加聚会很晚了,但我自己一直在想这件事。 (会感谢任何 cmets。)

我认为这里没有明确引用它,但我认为 Evans 提到的不变性主要是在共享的背景下:

为了安全共享对象,它必须是不可变的:它 除非完全更换,否则无法更改。 (埃文斯 p100)

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

如果室友都打电话订购电力服务[即如果两个客户有相同的地址],公司会 需要意识到它。 [所以] 地址是一个实体。 (埃文斯 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);

就一般优势而言,有些是在In DDD, what are the actual advantages of value objects? 获得的。值得注意的是,您只需在创建时验证 VO。如果你这样做,那么你知道它总是有效的。

【讨论】:

谢谢你。埃文的解释很有启发性,你的例子真的很重要。 这是迄今为止最简单和最好的答案。它使用了问题中的示例,并说明了为什么它会在没有 VO 的情况下失败。【参考方案5】:

为什么 6 是不可变的?

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

编辑:我现在将我们的对话提升到这个答案中。

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

【讨论】:

我见过这样的例子,它们看起来太简单了。 6 是不可变的,因为与 String 一样,您对其执行的任何操作都会为您提供一个新对象。 6+2 不是数字六变成了八;这是一个新号码。我明白了,但我不喜欢这个类比。在 Address 的情况下,类型更复杂,并且更难(并且感觉很傻)在其上定义返回新类型的操作。例如 newAddress = oldAddress.ChangeLine1("123 Main Street")? 你考虑得不够仔细。您可以创建一个模仿int 的类型,当您在其上调用实例级方法时,它会修改对象(调用类Integer,例如,实例级方法@ 987654328@ 接受 Integer 类型的参数)。我们为什么不这样做?为什么我们将数字建模为值对象? 嗯,六总是六。直观地说,Console.WriteLine(6) 应该总是“6”。这个例子很简单,但我很难将它应用回地址的想法。我从您那里了解到,说 jason.Address.Line1 = "123 Main" 有一些危险,最好说 jason.Address = new Address(...)。我还是不知道为什么。 @Hobbes:好的,但为什么6 总是6?这是因为6的身份是由它所代表的东西决定的,即拥有六个东西的状态。你不能改变 6 代表它。现在,这是值对象的基本概念。它们的价值取决于它们的状态。然而,一个实体不是由它的状态决定的。 Customer 可以更改他们的姓氏或地址,但仍然是相同的Customer。这就是为什么值对象应该是不可变的。他们的状态决定了他们的身份;如果他们的状态发生变化,他们的身份也应该改变。 最后一条评论完美。【参考方案6】:

这可能不是完整的答案。我只是在回答你关于不变性优势的问题。

    因为不可变对象是线程 安全的。因为他们无法改变 状态,它们不能被破坏 螺纹干涉或观察到 不一致的状态。 对不可变对象的引用 对象可以轻松共享或 缓存而无需复制或 克隆它们,因为它们的状态不能 施工后发生了翻天覆地的变化。 关于不变性的更多优势 你看这里(LBushkin的回答) What's the advantage of a String being Immutable?

This is an example Martin Fowler 关于为什么值对象应该是不可变的。

好吧,虽然将 VO 设为不可变并不是强制性的(即使 DDD 的书也没有说它们必须是不可变的),但在 DDD 中,使其成为 VO 的主要思想似乎不是处理生活像实体一样的循环复杂性。看这里for more details。

【讨论】:

从技术角度提出的任何论点都是错误的。您从未使用过我们在这里讨论值对象的事实。你对string 的类比是有缺陷的,因为string 是一个引用类型。您的论点实际上是关于不可变类型的好处,而不是为什么值对象应该是不可变的 这里不是隐含的值对象吗。此外,提出的问题是“有人可以解释为什么不变性是一件好事”。现在来到我提供的链接;我没有逐字复制优点,而是指定了讨论不变性优点的链接。那么你能告诉我为什么 Address 不是引用类型吗? Pangea:说得好。我问“为什么不可变性是一件好事”,但我真正的意思是“为什么不可变性是一件好事”对于价值对象?我将编辑我的问题以使其更清楚。 我已阅读您发布的 c2.com 链接。我认为他们不是在谈论领域驱动设计? @Hobbes - 我已经更新了我的答案。看最后一段。希望这能清除一些东西。

以上是关于DDD 中的值对象 - 为啥是不可变的?的主要内容,如果未能解决你的问题,请参考以下文章

Java中的String为什么是不可变的?

Java中的String为什么是不可变的? -- String源码分析

vue为啥method值不改

为啥有时 IEnumerable<T> 中的元素是可变的,而有时它们是不可变的? [复制]

为啥颤振小部件是不可变的?

UITableViewCell 中的最终 UIButton 是不可变的