Domain driven design 具有DB id和实体的值对象的DDD类设计困境

Domain driven design 具有DB id和实体的值对象的DDD类设计困境,domain-driven-design,value-objects,Domain Driven Design,Value Objects,这是一个很长的问题,所以我要直截了当地说。这是用于更好地说明问题的伪代码 DB结构 用户(用户名、名称、姓氏) 地址(AddressID、UserID、街道、城市、州、ZipCode)=>多对一用户关系 电话(PhoneID、UserID、Number、iPrimary)=>多对一用户关系 域类 class User:IEntity { public string Name {get;set;} public string LastName {get;set;} public ContactIn

这是一个很长的问题,所以我要直截了当地说。这是用于更好地说明问题的伪代码

DB结构

用户(用户名、名称、姓氏)

地址(AddressID、UserID、街道、城市、州、ZipCode)=>多对一用户关系

电话(PhoneID、UserID、Number、iPrimary)=>多对一用户关系

域类

class User:IEntity
{
public string Name {get;set;}
public string LastName {get;set;}
public ContactInfo{get;set;}
}

class Phone: IValueObject or IEntity? will see later.
{
public int id; // persistence ID, not domain ID
public string Number {get;set;}
}

class Address: IValueObject or IEntity? will see later.
{
public string Line1 {get;set;}
public string City {get;set;}
public string State {get;set;}
public string ZipCode {get;set;}
}

class ContactInfo: IValueObject or IEntity? will see later.
{
List<Address> Addresses {get;set;}
List<Phone> PhoneNumbers {get;set;}
}
那么,这就像是给用户(agg root)和contactinfo添加了一个方法?(德米特尔法)

User....
public void UpdatePrimaryPhoneNumber(string number)
{
this.ContactInfo.UpdatePrimaryPhoneNumber(number);
}

ContactInfo....
public void UpdatePrimaryPhoneNumber(string number)
{
var oldPhone = Phones.Where(p=>p.IsPrimary).Single();
var newPhone = new Phone(number, oldPhone.persistenceid???-> this is not part of the domain)
oldPhone = newPhone;
}
但我仍然要处理持久性id。。。grrrrr。真头痛

有时候,当我读到那些博客时,我觉得大多数“ddd专家”认为价值对象被过度使用了,或者我会说被滥用了

这种情况的最佳解决方案是什么?
谢谢

一个实体有一个相当独特的个体生命周期。当它单独存在时,它就有了意义

Order
/
OrderItem
的经典示例可能会对此有所帮助。 如果一个
OrderItem
成为一个实体,它将有自己的生命周期。但是,这没有太大意义,因为它是
订单的一部分。在查看订单时,这一点似乎总是很明显,但在查看自己的类时,情况就不那么明显了,因为类之间可能存在一些引用。例如,
OrderItem
表示我们正在销售的一些
产品。
产品
有自己的生命周期。我们可以有一个独立的
产品列表。我们如何对
订单项
产品
之间的链接进行建模可能是另一个讨论,但我会将所需的
产品
数据反规范化到
订单项
中,并存储原始的
Product.Id

那么
Address
类是实体还是值对象呢?这总是一个有趣的问题,因为我们有最喜欢的答案:这取决于

它将是特定于上下文的。但是问问自己是否有(或需要)一个独立的
地址列表,然后只需要
用户中指向该
地址的链接。如果是这种情况,那么它就是一个实体。但是,如果您的
地址
仅当它是
用户
的一部分时才有意义,则它是一个值对象

值对象是不可变的这一事实并不意味着您需要替换的不仅仅是特定的值对象。我不知道在您当前的设计中是否会有一个
ContactInfo
类,因为它只包装了两个集合(
Address
/
PhoneNumber
),但如果有更多的,我会保留它(可能是)。因此,只需更换相关的
电话号码
。如果您有类似于主/辅的功能,那么它非常简单:

AR.ReplacePrimaryPhoneNumber(新电话号码(“…”)

如果它是一个任意数字的列表,则
删除
/
添加
是合适的

现在来看持久性
Id
。你不需要。当您有一个主要/次要场景时,您知道您的用例是什么,并且您可以在数据库中执行相关查询(例如,更新主要
电话号码
)。如果你有一个任意列表,你可以选择在我的列表中添加所有新的数字从数据库中删除那些不在我的列表中的数字;否则,只需删除所有数字,并添加所有您拥有的。如果这看起来像是一个沉重的运动:它是。事件源将把这一点转移到内存处理中,我将认真推进这一点

我希望这一切都有意义。摆脱对数据方面的关注是相当困难的,但也是必要的。关注域,就好像你没有数据库一样。当您发现冲突时,请尽最大努力不要将数据库思想引入您的领域,而是尝试思考如何保持您的领域干净并仍然使用您选择的数据库

如果我遵循埃文关于DDD的圣经,值对象应该是不可变的。 也就是说,在创建它之后,它的属性或字段不会发生任何更改。 如果是这样的话,那么我想,我的课程没有一门是优秀的 ValueObject,因为我不能重新创建整个ContactInfo类 仅仅因为电话号码中的字符串的一部分是错误的。 所以,我想这让我所有的类都成为实体

虽然VO本身可能是不可变的,但VO本身并不存在——它始终是聚合的一部分。因此,一个VO可以是不可变的,但是引用该VO的对象不必是不可变的。帮助我理解VOs的是将它们与原始Int32值进行比较。每个整数的值都是不可变的-5始终是5。但只要有Int32,就可以在那里设置另一个值

对于您的域,这意味着您可以拥有一个不可变的地址VO,但给定的使用实体可以引用地址VO的任何实例。这将允许进行更正和任何其他更改。您不会更改地址VO上的单个字段,而是将其替换为一个全新的VO实例

其次,“持久性ID”不应该在域代码中的任何地方表达。它们的存在只是为了满足关系数据库的需要,而NoSQL数据库根本不需要它们

主要电话场景应该更像这样:

public void UpdatePrimaryPhoneNumber(string number)
{
  var existingPrimaryNumber = this.Phones.FirstOrDefault(x => x.IsPrimary == true);
  if (existingPrimaryNumber != null)
      this.Phones.Remove(existingPrimaryNumber);
  this.Phones.Add(new Phone(phoneNumber: number, isPrimary = true));
}
此方法封装了更新现有主电话号码的思想。电话号码VO是不可变的这一事实意味着您必须删除现有值并用新值替换它。数据库端通常发生的情况,尤其是像NHibernate这样的ORM,是它将发出SQL删除和后续插入,以有效地替换所有电话号码。这没关系,因为VO的ID不重要

我会创建一个clas
public void UpdatePrimaryPhoneNumber(string number)
{
  var existingPrimaryNumber = this.Phones.FirstOrDefault(x => x.IsPrimary == true);
  if (existingPrimaryNumber != null)
      this.Phones.Remove(existingPrimaryNumber);
  this.Phones.Add(new Phone(phoneNumber: number, isPrimary = true));
}
class Phone implements IEntity
{
public int id; // persistence ID, not domain ID
public PhoneNumber number {get;set;}
}

class PhoneNumber implements IValueObject
{
public String number {get;set;};
}