具有行为和ORM的丰富域模型

在观看了Jimmy Bogard( http://ndcoslo.oktaset.com/Agenda )的NDC12演示文稿“Crafting Wicked Domain Models”之后,我正在徘徊如何坚持这种域模型。
这是演示文稿中的示例类:

public class Member { List _offers; public Member(string firstName, string lastName) { FirstName = firstName; LastName = lastName; _offers = new List(); } public string FirstName { get; set; } public string LastName { get; set; } public IEnumerable AssignedOffers { get { return _offers; } } public int NumberOfOffers { get; private set; } public Offer AssignOffer(OfferType offerType, IOfferValueCalc valueCalc) { var value = valueCalc.CalculateValue(this, offerType); var expiration = offerType.CalculateExpiration(); var offer = new Offer(this, offerType, expiration, value); _offers.Add(offer); NumberOfOffers++; return offer; } } 

所以这个域模型中包含一些规则:
– 会员必须拥有名字和姓氏
– 优惠数量无法在外面更改
– 会员负责创建新优惠,计算其价值和分配

如果尝试将此映射到某些ORM(如Entity Framework或NHibernate),则无法正常工作。 那么,使用ORM将这种模型映射到数据库的最佳方法是什么?
例如,如果没有setter,如何从DB加载AssignedOffers?

只有对我有意义的是使用命令/查询架构:查询总是使用DTO作为结果,而不是域实体,并且命令在域模型上完成。 此外,事件采购非常适合域模型上的行为。 但是这种CQS架构并不适合每个项目,特别是棕色区域。 或不?

我在这里知道类似的问题,但找不到具体的例子和解决方案。

这实际上是一个非常好的问题,我已经考虑过了。 创建完全封装的正确域对象(即没有属性设置器)并使用ORM直接构建域对象可能很困难。

根据我的经验,有3种方法可以解决这个问题:

  • 正如Luka已经提到的,NHibernate支持映射到私有字段,而不是属性设置器。
  • 如果使用EF(我认为不支持上述内容),您可以使用memento模式将状态恢复到域对象。 例如,您使用entity framework填充您的域实体接受的“memento”对象来设置其私有字段。
  • 正如您所指出的,将CQRS与事件源一起使用可以消除此问题。 这是我制作完美封装的域对象的首选方法,它还具有事件源的所有附加好处。

旧线程。 但是Vaughn Vernon 最近的一篇文章 (2014年末)解决了这个问题,特别是entity framework。 鉴于我在某种程度上难以找到这样的信息,也许在这里发布它也会有所帮助。

基本上,post提倡Product域(聚合)对象包装ProductState EF POCO数据对象,以解决与“数据包”方面有关的问题。 当然,域对象仍然会通过特定于域的方法/访问器添加其所有丰富的域行为,但是当它必须获取/设置其属性时,它将诉诸内部数据对象。

从post直接复制片段:

 public class Product { public Product( TenantId tenantId, ProductId productId, ProductOwnerId productOwnerId, string name, string description) { State = new ProductState(); State.ProductKey = tenantId.Id + ":" + productId.Id; State.ProductOwnerId = productOwnerId; State.Name = name; State.Description = description; State.BacklogItems = new List(); } internal Product(ProductState state) { State = state; } //... private readonly ProductState State; } public class ProductState { [Key] public string ProductKey { get; set; } public ProductOwnerId ProductOwnerId { get; set; } public string Name { get; set; } public string Description { get; set; } public List BacklogItems { get; set; } ... } 

存储库将使用内部构造函数,以便从其DB持久化版本实例化(加载)实体实例。

我可以添加的一点是,可能Product域对象应该被另外一个访问者弄脏 ,只是为了通过EF持久化:同样是new Product(productState)允许从数据库加载域实体,应该通过以下方式允许相反的方式:

 public class Product { // ... internal ProductState State { get { // return this.State as is, if you trust the caller (repository), // or deep clone it and return it } } } // inside repository.Add(Product product): dbContext.Add(product.State); 

对于AssignedOffers:如果查看代码,您将看到AssignedOffers从字段返回值。 NHibernate可以像这样填充该字段:Map(x => x.AssignedOffers).Access.Field()。

同意使用CQS。

在做DDD时,首先要忽略持久性问题。 ORM与RDBMS紧密相关,因此它是一个持久性问题。

ORM模型持久性结构而不是域。 基本上,存储库必须将接收到的聚合根“转换”为一个或多个持久性实体。 有界上下文非常重要,因为聚合根会根据您想要完成的内容而发生变化。

假设您要在分配的新要约的上下文中保存成员。 然后你会有这样的事情(当然这只是一种可能的情况)

 public interface IAssignOffer { int OwnerId {get;} Offer AssignOffer(OfferType offerType, IOfferValueCalc valueCalc); IEnumerable NewOffers {get; } } public class Member:IAssignOffer { /* implementation */ } public interface IDomainRepository { void Save(IAssignOffer member); } 

接下来,repo将只获得所需的数据,以便更改NH实体,这就是全部。

关于EVent Sourcing,我认为你必须看看它是否适合你的域,我认为使用Event Sourcing只存储域聚合根,而其余(主要是基础设施)可以以普通方式存储时没有任何问题(关系)表)。 我认为CQRS在这个问题上给你很大的灵活性。