18

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

public class Member
{
    List<Offer> _offers;

    public Member(string firstName, string lastName)
    {
        FirstName = firstName;
        LastName = lastName;
        _offers = new List<Offer>();
    }

    public string FirstName { get; set; }

    public string LastName { get; set; }

    public IEnumerable<Offer> 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 架构可能并不适合每个项目,尤其是棕地。或不?

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

4

4 回答 4

12

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

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

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

旧线程。但是Vaughn Vernon最近发表了一篇文章(2014 年末),专门针对这种情况,特别提到了实体框架。鉴于我不知何故难以找到此类信息,也许在这里发布它也会有所帮助。

基本上,该帖子提倡使用Product域(聚合)对象来包装ProductStateEF POCO 数据对象,以解决与“数据包”相关的问题。当然,域对象仍然会通过特定于域的方法/访问器添加其所有丰富的域行为,但是当它必须获取/设置其属性时,它会求助于内部数据对象。

直接从帖子复制片段:

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<ProductBacklogItem>();
  }

  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<ProductBacklogItemState> 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);
于 2016-07-20T13:00:08.033 回答
1

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

同意使用 CQS。

于 2012-06-27T09:09:53.960 回答
0

在做 DDD 第一件事时,您忽略了持久性问题。ORM 与 RDBMS 紧密耦合,因此这是一个持久性问题。

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

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

public interface IAssignOffer
{
    int OwnerId {get;}
    Offer AssignOffer(OfferType offerType, IOfferValueCalc valueCalc);
    IEnumerable<Offer> NewOffers {get; }
}

public class Member:IAssignOffer
{
    /* implementation */ 
 }

 public interface IDomainRepository
 {
    void Save(IAssignOffer member);    
 }

接下来,repo 将仅获取更改 NH 实体所需的数据,仅此而已。

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

于 2012-06-27T09:57:57.333 回答