实体与价值对象的比较


为了定义实体和值对象之间的差异,我们需要引入三种类型的相等性,当我们需要将对象相互比较时,它们会起作用。

  1. 引用相等意味着如果两个对象引用内存中的相同地址,则认为它们是相等的。
  2. 标识符相等性意味着类具有id字段。如果具有相同的标识符,则此类的两个实例将是相等。
  3. 结构相等的情况下,如果所有成员都匹配,我们认为两个对象相等。

实体和值对象之间的主要区别在于我们将它们的实例相互比较的方式。标识符平等的概念指的是实体,而结构平等的概念指的是价值对象。换句话说,实体具有固有的标识,而价值对象则没有。
实际上,这意味着值对象没有标识符字段,如果两个值对象具有相同的属性集,我们可以互换地处理它们。同时,如果两个实体实例中的数据相同(Id属性除外),我们认为它们不等同。
您可以通过类似的方式来考虑它,您会想到两个同名的人。因此,您不会将他们视为同一个人。两个人都有自己的固有标识。然而,如果一个人有1美元的钞票,他们不在乎这张实物纸是否和昨天一样。直到它仍然是1美元,他们可以用另一个替换这个音符。在这种情况下,货币的概念将是一个价值对象。

实体与值对象:生命周期
这两个概念之间的另一个区别是它们实例的寿命。实体生活在连续体中,可以这么说。他们有发生在他们身上的历史以及他们在一生中如何改变的历史(即使我们不存储它)。
值对象同时具有零生命周期。我们轻松创造并摧毁它们。这是可互换的必然结果。如果这1美元的钞票与另一个账单相同,为什么还要费心呢?我们可以用我们刚刚实例化的对象替换现有对象,完全忘记它。
从这种区别流出的指导原则是价值对象不能靠自己生活,它们应该总是属于一个或多个实体。值对象表示的数据仅在其引用的实体的上下文中具有含义。在上面的人和金钱的例子中,“多少钱?”这个问题没有任何意义,因为它没有传达适当的背景。与此同时,“彼得有多少钱?”或“我们所有用户拥有多少钱?”这些问题完全有效。
这里的另一个推论是我们不单独存储值对象。我们持久保存值对象的唯一方法是将它附加到实体(一分钟内更多关于它)。

实体与值对象:不变性
下一个区别是不变性。值对象应该是不可变的,如果我们需要更改这样的对象,我们构建一个基于现有对象的新实例而不是更改它。相反,实体几乎总是可变的。
值对象是否应该永远是不可改变的问题是争议的主题。一些程序员认为这个规则不像前一个规则那么严格,并且在某些情况下,值对象确实是可变的。我过去也坚持这种观点。
如今,我发现不变性与用另一个值替换价值对象的能力之间的联系更深刻。通过改变值对象的实例,您可以假设它具有自己的生命周期。反过来,这个假设得出结论,值对象有其固有的标识,这与DDD概念的定义相矛盾。
这种简单的心理练习使不变性成为值对象的内在组成部分。如果我们接受值对象的生命周期为零,这意味着它们只是某些状态的快照,而不是更多,那么我们必须承认它们只能代表该状态的单个变体。
这导致我们遵循以下经验法则:如果您不能使值对象不可变,那么它不是值对象。

如何识别域模型中的值对象?
您的域模型中的概念是实体还是值对象并不总是很清楚。不幸的是,没有客观的属性可以用来了解它。概念是否是值对象完全取决于问题域:概念可以是一个域模型中的实体,在另外一个上下文中则是一个值对象。
在上面的例子中,我们可以互换地对待金钱,这使得这个概念成为一个值对象。同时,如果我们建立一个跟踪整个国家现金流量的软件,我们需要分别处理每一个账单,以收集每个账单的统计数据。在这种情况下,货币的概念将是一个实体,虽然我们可能会将其命名为Note或Bill。
尽管缺乏客观特征,您仍然可以使用一些技术将概念归因于实体或值对象。我们讨论了标识的概念:如果你可以安全地将一个类的实例替换为另一个具有相同属性集的实例,那么这个概念就是一个值对象。
该技术的更简单版本是将值对象与整数进行比较。你是否真的关心整数5是否与你在另一种方法中使用的5相同?绝对不是,你的应用程序中的所有五个都是相同的,无论它们如何被实例化。这使得整数本质上是一个值对象。现在,问问自己,你域中的这个概念看起来像整数吗?如果答案是肯定的,那么它就是一个值对象。


如何在数据库中存储值对象?
假设我们的域模型中有两个类:Person实体和Address 值对象:

// Entity
public class Person
{
    public int Id { get; set; }
    public string Name { get; set; }
    public Address Address { get; set; }
}

// Value Object
public class Address
{
    public string City { get; set; }
    public string ZipCode { get; set; }

}

在这种情况下,数据库结构将如何?想到的一个选项是为每个选项创建单独的表。
尽管从数据库的角度来看这种设计完全有效,但这种设计有两个主要缺点。首先,Address表包含一个标识符。这意味着我们必须在Address值对象中引入一个单独的Id字段才能正确使用这样的表。反过来,这意味着我们为Address类提供了一些标识。这违反了值对象的定义。
另一个缺点是,使用此解决方案,我们可能会从实体中分离值对象。Address值对象现在可以独立生存,因为我们可以从数据库中删除Person行而不删除相应的Address行。这将违反另一条规则,该规则规定值对象的生命周期应完全取决于其父实体的生命周期。
事实证明,最好的解决方案是将Address表中的字段内联到Person表中。

这将解决我之前提到的所有问题:地址不再具有身份,其生命周期现在完全取决于Person实体的生命周期。
如果您在心理上用我之前建议的单个整数替换关于Address的字段,这个设计也是有意义的。你是否为整数创建了一个单独的表?当然不是,您只需将该行的整数内联到您希望它所在的表中。这同样适用于值对象。不要为值对象引入单独的表,只需将它们内联到父实体的表中即可。

首选值对象
在处理实体和值对象时,一个重要的指导方针发挥作用:总是更喜欢值对象而不是实体。值对象是不可变的,比实体更轻量级。因此,它们非常容易使用。理想情况下,您应该始终将大多数业务逻辑放入值对象中。在这种情况下,实体将充当它们的包装并代表更高级别的功能。
此外,您最初看作实体的概念可能基本上是一个值对象。例如,代码库中的Address类最初可以作为实体引入。它可能有自己的Id字段和数据库中的单独表。重新访问后,您可能会注意到,在您的域中,地址实际上并没有自己的固有标识,可以互换使用。在这种情况下,请不要犹豫重构您的域模型并将实体转换为值对象。

总结
好吧,我想我涵盖了与实体和价值对象相关的所有方面。我们用以下内容总结一下:

  • 实体有自己的内在标识,值对象没有。
  • 标识平等的概念是指实体; 结构平等的概念指的是价值客体;引用平等的概念指的是两者。
  • 实体有历史; 值对象的生命周期为零。
  • 值对象应始终属于一个或多个实体,它不能独立存在。
  • 值对象应该是不可变的; 实体几乎总是可变的。
  • 要识别域模型中的值对象,请在心理上将其替换为整数。
  • 值对象不应在数据库中拥有自己的表。
  • 始终优先考虑域模型中实体的值对象。