6

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

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

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

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

4

4 回答 4

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

我个人不喜欢使用“状态”,它们会自动共享给所有使用它们的东西——我认为这是不必要的耦合

所以我会有这样的事情:

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

为了在订单状态更改时更改相关内容,最好的办法是使用所谓的域事件
我的代码看起来像这样:

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 变得太大,您可能想看看有界上下文是什么(正如 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 之类的术语,您可以利用这些术语作为分解的锚。

注意:请记住 - 我对您的域的了解为零。
我使用的那些动词很可能对它完全陌生。

于 2011-07-27T09:10:21.250 回答
0

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

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

另请参阅外观模式。

于 2011-07-26T01:03:53.717 回答
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 和一般的服务层声明了对订单执行的不同类型的操作并公开域以供使用外部组件,例如表示层。

还要考虑研究领域事件的概念,它考虑了贫血的领域模型和改进它们的方法。

于 2011-07-27T03:02:47.227 回答
0

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

于 2012-03-12T22:10:51.303 回答