领域驱动设计:避免贫血领域和建模现实世界的角色

时间:2011-07-26 00:34:36

标签: domain-driven-design domain-model anemic-domain-model

我正在寻找一些关于我应该关注多少避免贫血领域模型的建议。我们刚刚开始使用DDD,并且正在努力解决有关简单设计决策的分析瘫痪问题。我们坚持的最新观点是某些业务逻辑所属的位置,例如我们有一个Order对象,其中包含Status等属性。现在说我必须执行像{{1}这样的命令因为有人在订单时犯了错误,这并不像更改UndoLastStatus那么简单,因为必须记录其他信息并更改属性。现在在现实世界中,这是一项纯粹的管理任务。所以我看待它的方式我有两个我能想到的选择:

  • 选项1:添加方法以便订购Status之类的东西,虽然这有点意义,但它并不真正反映域。此外,Order.UndoLastStatus()是系统中的主要对象,如果涉及订单的所有内容都放在订单类中,事情就会失控。

  • 选项2:创建一个Order对象,并且具有代表不同角色的不同服务。所以我可能会ShopShop.AdminServiceShop.DispatchService。所以在这种情况下,我会Shop.InventoryService

现在第二个选项我们有更多反映域的东西,并允许开发人员与业务专家讨论实际存在的类似角色。但它也走向了贫血的模式。一般来说哪种方式更好?

4 个答案:

答案 0 :(得分:6)

选项2肯定会导致程序代码 可能更容易开发,但更难维护。

  

现在,在现实世界中,这是一项纯粹的管理任务

“管理”任务应该是私有的,并通过公共的,完全“域名”行动来调用。最好 - 仍然以易于理解的代码编写,由域驱动。

正如我所看到的那样 - 问题是UndoLastStatus对域专家来说没什么意义 他们更有可能在谈论制作,取消和填写订单。

这些方面的某些内容可能更合适:

class Order{
  void CancelOrder(){
    Status=Status.Canceled;
  }
  void FillOrder(){
    if(Status==Status.Canceled)
      throw Exception();
    Status=Status.Filled;
  }
  static void Make(){
    return new Order();
  }
  void Order(){
    Status=Status.Pending;
  }
}

我个人不喜欢使用“状态”,它们会自动与使用它们的所有内容共享 - 我将其视为unnecessary coupling

所以我会有这样的事情:

class Order{
  void CancelOrder(){
    IsCanceled=true;
  }
  void FillOrder(){
    if(IsCanceled) throw Exception();
    IsFilled=true;
  }
  static Order Make(){
    return new Order();
  }
  void Order(){
    IsPending=true;
  }
}

如果在订单状态发生变化时更改相关内容,最好的办法是使用所谓的domain events 我的代码将按以下方式查看:

class Order{
  void CancelOrder(){
    IsCanceled=true;
    Raise(new Canceled(this));
  }
  //usage of nested classes for events is my homemade convention
  class Canceled:Event<Order>{
    void Canceled(Order order):base(order){}
  }     
}

class Customer{
  private void BeHappy(){
    Console.WriteLine("hooraay!");
  }
  //nb: nested class can see privates of Customer
  class OnOrderCanceled:IEventHandler<Order.Canceled>{
   void Handle(Order.Canceled e){
    //caveat: this approach needs order->customer association
    var order=e.Source;
    order.Customer.BeHappy();
   }
  }
}

如果订单增长太大,你可能想看看bounded contexts是什么(正如埃里克埃文斯所说 - 如果他有机会再次写下他的书,他会在一开始就将有限的背景移动起来)。

简而言之 - 它是由域驱动的一种分解形式。

理念相对简单 - 可以从不同的视点(也称为上下文)获得多个订单。

E.g。 - 从购物上下文订购,从会计上下文订购。

namespace Shopping{
 class Order{
  //association with shopping cart
  //might be vital for shopping but completely irrelevant for accounting
  ShoppingCart Cart;
 }
}
namespace Accounting{
 class Order{
  //something specific only to accounting
 }
}

但通常足够的域本身可以避免复杂性,并且如果你足够仔细地听它就很容易分解。例如。您可能会听到OrderLifeCycle,OrderHistory,OrderDescription这样的专家术语,您可以利用这些术语作为分解的锚点。

注意:请记住 - 我对你的域名一无所知 我使用的那些动词很可能对它完全陌生。

答案 1 :(得分:0)

我会遵循GRASP原则。应用信息专家设计原则,即您应该将责任分配给自然拥有完成更改所需的最多信息的类。

在这种情况下,由于更改订单状态涉及其他实体,我会让这些低级域对象中的每一个都支持一种方法来应用相对于自身的更改。然后还使用选项2中描述的域服务层,它抽象整个操作,根据需要跨越多个域对象。

另见Facade模式。

答案 2 :(得分:0)

我认为在Order类上使用类似UndoLastStatus的方法感觉有点不对,因为它存在的原因在某种意义上超出了订单的范围。另一方面,有一个负责改变订单状态的方法,Order.ChangeStatus,非常适合作为域模型。订单的状态是一个正确的域概念,并且应该通过Order类更改该状态,因为它拥有与订单状态相关联的数据 - Order类负责保持自身的一致性并处于适当的状态

另一种思考方式是Order对象是持久存储到数据库的,它是应用于Order的所有更改的“最后一站”。从订单的角度而不是从外部组件的角度来看,更容易推断订单的有效状态。这就是DDD和OOP的全部内容,使人们更容易推理代码。此外,可能需要访问私有或受保护的成员来执行状态更改,在这种情况下,将方法放在订单类上是更好的选择。这就是为什么贫血领域模型不受欢迎的原因之一 - 他们将保持国家一致性的责任转移到拥有阶级,从而打破了其他事物的封装。

实现更具体的操作(如UndoLastStatus)的一种方法是创建一个公开域的OrderService,以及外部组件如何在域上运行。然后你可以创建一个简单的命令对象:

class UndoLastStatusCommand {
  public Guid OrderId { get; set; }
}

OrderService将有一个处理该命令的方法:

public void Process(UndoLastStatusCommand command) {
  using (var unitOfWork = UowManager.Start()) {
    var order = this.orderRepository.Get(command.OrderId);
    if (order == null)
      throw some exception

    // operate on domain to undo last status

    unitOfWork.Commit();
  }
}

现在,Order的域模型公开了与Order对应的所有数据和行为,但OrderService和服务层通常声明对订单执行的不同类型的操作并公开域供外部组件使用,例如表示层。

还要考虑研究考虑贫血领域模型的domain events概念及其改进方法。

答案 3 :(得分:0)

听起来你并没有从测试中推动这个领域。看一下Rob Vens的工作,特别是他在探索性建模,时间反演和主动 - 被动方面的工作。