这是一个很长的问题,所以我直接开门见山。这是为了更好地说明问题的伪代码
数据库结构
用户(UserID,Name,LastName)
地址(AddressID、UserID、街道、城市、州、ZipCode)=>多对一用户关系
电话(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;}
}
到目前为止,我们已经对这个领域及其模型有了一个非常基本的表示。
我的问题如下。假设我想更新其中一个地址,或者修复其中一个号码的区号,因为它最初输入时拼写错误。
如果我遵循Evan关于DDD的圣经,Value Object应该是不可变的。也就是说,在创建后不会更改其属性或字段。如果是这样的话,那么我想,我的类都不是ValueObject,因为我不能仅仅因为电话号码中字符串的一部分错误就重新创建整个ContactInfo类。所以,我想这让我所有的类都成为实体?
请记住,我为每个类都有一个"持久性id",因为它们存储在数据库中。
假设我决定将Phone作为一个值对象,因为在构造函数中很容易重新创建
public Phone(string newNumber)
那么,这将类似于向User(agg-root)AND 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专家"都被过度使用了,或者我会说被滥用了。
这种情况的最佳解决方案是什么?谢谢
如果我遵循Evan关于DDD的圣经,Value对象应该是不可变的。也就是说,在创建后不会更改其属性或字段。如果是这样的话,那么我想,我的课都不是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删除和随后的插入,以有效地替换所有电话号码。这是可以的,因为VOs的ID无关紧要。
实体有一个相当独特的生命周期。当它独自一人时,它就有意义了。
Order
/OrderItem
的经典示例可能对此有所帮助。如果OrderItem
成为一个实体,它将有自己的生命周期。然而,这没有太大意义,因为它是Order
的部分。当查看订单时,这似乎总是显而易见的,但当查看自己的类时,情况就不那么明显了,因为类之间可能存在一些引用。例如,OrderItem
代表我们正在销售的一些Product
。Product
有自己的生命周期。我们可以有一个独立的Product
s列表。我们如何对OrderItem
和Product
之间的链接进行建模可能是另一个讨论,但我会将我需要的Product
数据反规范化到OrderItem
中,并存储原始Product.Id
。
那么Address
类是实体还是值对象呢?这总是一个有趣的问题,因为我们有最喜欢的答案:这取决于情况。
它将根据具体情况而定。但问问自己,你是否有(或需要)一个独立的Address
列表,然后只需要User
中指向该Address
的链接。如果是这种情况,那么它就是一个实体。但是,如果您的Address
只有在是User
的一部分时才有意义,那么它就是一个Value对象。
Value对象是不可变的,这并不意味着您需要替换的不仅仅是特定的Value对象。我不知道在您当前的设计中我是否会有一个ContactInfo
类,因为它只包装了两个集合(Address
/PhoneNumber
),但如果有更多的内容(可能是),我会保留它。因此,只需更换相关的PhoneNumber
即可。如果你有像初级/次级这样的东西,那么它很简单:
AR.ReplacePrimaryPhoneNumber(new PhoneNumber('...'))
如果它是任意数字的列表,则Remove
/Add
将是合适的。
现在针对持久性 从数据库中删除不在我列表中的号码我希望这一切都有意义。摆脱对数据方面的关注是相当困难的,但也是必要的。将注意力集中在域上,就像没有数据库一样。当你发现摩擦时,尽最大努力而不是将数据库思维引入你的域,但试着思考如何保持域的清洁并仍然使用你选择的数据库。Id
。你不需要。当您有一个主要/次要场景时,您知道您的用例是什么,并且可以在DB中执行相关查询(例如,更新主要PhoneNumber
)。如果您有一个任意列表,可以选择在我的列表中添加所有新号码,并
我会创建一个类PhoneNumber,它包含当前Phone类的String编号,并将其用作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类中。然后,这个类可以在整个应用程序的不同位置重用。
在我看来,Address是一个Value对象,您可以将其视为一个整体。虽然你可以对街道、城市等进行建模,这些通常都是实体,但这可能是过度建模。地址的任何部分都不能更改,整个对象在初始创建后更改时总是被替换。
User类在这个例子中,这些边界是一个聚合根(因此也是一个实体)。ContactInfo类不是ValueObject(不是不可变的),也不是Entity(没有实际标识),而是Aggregate。它包含多个类,这些类应该被视为一个整体。
有关的更多信息http://martinfowler.com/bliki/DDD_Aggregate.html
通常,只要有一个持久性id,你就会想到一个实体。然而,如果你想添加持久性id,我会像Phone和PhoneNumber类一样开始拆分。例如,Address(包含id的实体)和AddressValue包含所有其他字段(以及关于地址值的逻辑)。
这也应该解决管理持久性标识的问题,因为在updatePrimaryPhoneNumber的情况下,替换了整个值对象,并且持久性标识保持不变。