13

这是一个很长的问题,所以我要直奔主题。这是为了更好地说明问题的伪代码

数据库结构

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

地址(AddressID, UserID, Street, City, State, ZipCode) =>多对一用户关系

Phone (PhoneID, UserID, Number, IsPrimary) =>多对一用户关系

领域类

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;}
}

所以,到目前为止,我们对这个领域及其模型有了一个非常基本的表示。

我的问题如下。假设我想更新其中一个地址或修复其中一个数字的区号,因为最初输入的 wnen 拼写错误。

如果我遵循 Evan 关于 DDD 的圣经,值对象应该是不可变的。意思是,在创建后不会更改其属性或字段。如果是这种情况,那么我想,我的所有类都不是 ValueObject,因为我不能仅仅因为电话号码中字符串的一部分错误就重新创建整个 ContactInfo 类。所以,我想这使我所有的类实体?

请记住,我对每个此类都有一个“持久性 ID”,因为它们存储在数据库中。

假设我决定让 Phone 成为一个值对象,因为它很容易在构造函数中重新创建

public Phone(string newNumber)

所以,这就像向用户(agg root)和联系人信息添加一个方法?(得墨忒耳定律)

像...

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 专家”认为价值对象被过度使用,或者我会说被滥用。

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

4

3 回答 3

11

如果我遵循 Evan 关于 DDD 的圣经,值对象应该是不可变的。意思是,在创建后不会更改其属性或字段。如果是这种情况,那么我想,我的所有类都不是 ValueObject,因为我不能仅仅因为电话号码中字符串的一部分错误就重新创建整个 ContactInfo 类。所以,我想这使我所有的类实体?

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

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

接下来,“Persistence ids”不应该在域代码的任何地方表达。它们的存在只是为了满足关系数据库的需求,而 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 无关紧要。

于 2012-12-09T22:46:18.887 回答
10

一个实体有一个相当独特和独立的生命周期。当它独立存在时,它才有意义。

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

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

它将是特定于上下文的。但是问问自己,你是否有(或需要)一个独立的Addresss 列表,然后只需要Address在你的User. 如果是这种情况,那么它就是一个实体。但是,如果您仅在它您的一部分Address时才有意义,那么它就是一个值对象。User

值对象是不可变的这一事实并不意味着您需要替换的不仅仅是特定的值对象。我不知道我是否会ContactInfo在您当前的设计中有一个类,因为它只包含两个集合(Address/ PhoneNumber),但如果有更多内容(可能是),我会保留它。所以干脆替换掉相关的PhoneNumber。如果您有主/次之类的东西,那么它很简单:

AR.ReplacePrimaryPhoneNumber(new PhoneNumber('...'))

如果它是任意数字的列表,则Remove/Add将是合适的。

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

我希望这一切都有意义。摆脱对事物数据方面的关注是相当困难但必要的。专注于域,就好像您没有数据库一样。当您发现摩擦时,请尽最大努力不要将数据库思维引入您的域,而是尝试考虑如何保持域清洁并仍然使用您选择的数据库。

于 2012-12-09T06:58:23.243 回答
0

我将创建一个包含当前 Phone 类的字符串编号的类 PhoneNumber,并将其用作您的 Phone 类中的 Value 对象:

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;};
}

稍后当您的代码发展时,您将需要(例如)电话号码验证,您可以将其放在 PhoneNumber 类中。然后可以在不同的地方在整个应用程序中重用这个类。

在我看来,地址是一个值对象,您可以将其视为一个整体。尽管您可以对通常为实体的 Street、City 等进行建模,但这可能是过度建模。地址的任何部分都不能更改,在初始创建后更改时始终替换整个对象。

User 类在这个例子中,这些边界是一个聚合根(因此也是一个实体)。ContactInfo 类不是 ValueObject(不是不可变的),也不是 Entity(没有真实身份),而是一个 Aggregate。它包含多个类,应将其视为一个整体。

有关http://martinfowler.com/bliki/DDD_Aggregate.html的更多信息

通常,只要有持久性 ID,您就应该考虑一个实体。但是,如果您想添加持久性 ID,我会像 Phone 和 PhoneNumber 类一样开始拆分。例如,包含所有其他字段(以及有关地址值的逻辑)的地址(包含 id 的实体)和地址值。

这也应该解决管理持久性标识的难题,因为您替换了整个值对象,并且在 updatePrimaryPhoneNumber 的情况下持久性标识保持不变。

于 2015-06-10T13:04:19.543 回答