【问题标题】:Rich domain model with behaviours and ORM具有行为和 ORM 的丰富领域模型
【发布时间】:2023-04-05 20:47:01
【问题描述】:

在观看了 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 架构可能并不适合每个项目,尤其是棕地。还是不行?

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

【问题讨论】:

  • 我刚刚看了同样的视频,我也在想同样的事情。您如何看待在构造函数中传递一个 poco,并且在 Member 类上具有一个只读属性以返回该 poco 的克隆?这样您就可以将数据传入和传出域对象,以便将其持久化或传递。
  • 对象快照之类的东西?它可能会起作用,但也需要一些黑客攻击才能使其与 ORM 工具一起使用。我个人认为没有任何简单的方法,它会带来很多抽象和概括,你必须在整个应用程序开发过程中与之抗争。事件溯源是 IMO 的唯一途径
  • 其实我刚刚看了这个视频,也在想同样的事情;这是否意味着您需要一组 DTO/POCO 对象用于 ORM 水合的数据/持久层,然后使用 AutoMapper 之类的映射器映射到域对象?类似的事情发生在存储库中吗?看起来像 EF Code First 这样的 ORM 需要一个带有 getter 和 setter 的 POCO。
  • @Abe 是的,在我看来 EF 模型应该低于域模型,并在保存或加载内容时使用 AutoMapper 魔法。此外,对于 UI,还有另一组 ViewModel,再次从域模型映射,而不是 EF 模型。业务逻辑应该在那些领域模型中,而不是在你可以随处可见的SomethingManager 类中。恕我直言:)
  • 酷!您是否觉得编写这么多映射代码很乏味/繁重? EF 到域模型,然后域到 MVC 视图模型......也可能有域到 WCF 合同等......看起来你可以 AutoMapper 有一个 ConstructUsing 方法,可以让你创建一个新对象而不是使用 getter 和 setter领域模型如stackoverflow.com/questions/2239143/…

标签: c# .net architecture orm domain-driven-design


【解决方案1】:

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

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

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

【讨论】:

    【解决方案2】:

    旧线程。但是 Vaughn Vernon 的 more recent post(2014 年末)解决了这种情况,特别提到了实体框架。鉴于我不知何故难以找到此类信息,也许在这里发布它也会有所帮助。

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

    直接从帖子中复制sn-p:

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

    【讨论】:

      【解决方案3】:

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

      同意使用 CQS。

      【讨论】:

      • 我在您的示例中没有看到私有/受保护或内部构造函数。所以 NHibernate 将使用默认的无参数的。我会使用这种类型的构造函数,只是为了确保对象在代码中填充,而不是通过 orm。
      • 查看 NHibernate 和默认构造函数的这篇文章:kozmic.pl/category/nhibernate
      【解决方案4】:

      当首先做 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 实体所需的数据,仅此而已。

      关于事件溯源,我认为你必须看看它是否适合你的域,我认为事件溯源仅用于存储域聚合根没有任何问题,而其余的(主要是基础设施)可以存储在普通的方式(关系表)。我认为 CQRS 在这件事上为您提供了极大的灵活性。

      【讨论】:

      • 感谢回答,但存储库应如何加载会员提供的服务?
      • 加载它们的目的是什么? :)
      • Member 暴露了 IEnumerable,所以用于服务类或类似的东西。
      猜你喜欢
      • 2011-12-03
      • 2014-01-05
      • 1970-01-01
      • 2012-12-31
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多