【问题标题】:Domain driven design: Avoiding anemic domains and modelling real world roles领域驱动设计:避免贫血领域和模拟现实世界的角色
【发布时间】:2011-10-13 00:49:31
【问题描述】:

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

  • 选项 1:添加订购方法,例如 Order.UndoLastStatus(),虽然这有点道理,但它并不能真正反映领域。 Order 也是系统中的主要对象,如果涉及订单的所有内容都放在订单类中,事情可能会失控。

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

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

【问题讨论】:

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


    【解决方案1】:

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

    如果 Order 变得太大,您可能想看看 bounded contexts 是什么(正如 Eric Evans 所说 - 如果他有机会再次写他的书,他会将有界上下文移到开头)。

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

    想法相对简单 - 可以从不同的角度(即上下文)拥有多个订单。

    例如- 购物环境下的订单,会计环境下的订单。

    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) 从购物车中删除订单,2) 删除商店中的预订,3) 补偿客户款项(如果已付款)?无法想象它们位于 Order.Cancel 方法中。
    【解决方案2】:

    我会以GRASP 原则为指导。应用 Information Expert 设计原则,即您应该将职责分配给自然拥有完成更改所需的最多信息的类。

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

    另请参阅Facade 模式。

    【讨论】:

    • 更改订单状态只会涉及订单聚合中的实体。所以基本上我会同时应用这两个选项,而 Shop.AdminService.UndoLastStatus(Order) 会简单地调用 Order.UndoLastStatus()?
    • 我从您的问题中了解到,当您撤消订单时还有其他必然的任务要做,这就是为什么我认为该任务应该属于上一级,因此它可以访问其他对象,如记录器等等。
    【解决方案3】:

    我认为在 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 的概念,它考虑了贫血的领域模型和改进它们的方法。

    【讨论】:

    • 为什么要创建 UndoLastStatusCommand,为什么不直接传递 OrderId 呢?
    • 命令对象在面向消息的服务层中充当一种特殊类型的消息。这只是构建服务层的一种方式,它基于异步集成模式,例如 CQRS。因此,将订单 id 直接传递给名为 UndoLastStatus 的服务方法也是有效的,但最终决定取决于服务层上设置的约束和要求。
    【解决方案4】:

    听起来您并没有通过测试来驱动这个域。看看Rob Vens的工作,尤其是他在探索性建模、时间反演和主动-被动方面的工作。

    【讨论】:

      猜你喜欢
      • 2014-07-24
      • 1970-01-01
      • 2011-12-19
      • 2012-02-04
      • 2010-12-20
      • 1970-01-01
      • 2010-12-26
      • 2011-02-20
      • 2014-01-05
      相关资源
      最近更新 更多