20

我们当前的 O/RM 工具并不能真正支持丰富的领域模型,因此我们不得不在任何地方都使用贫血 (DTO) 实体。这工作得很好,但我继续为将基本的基于对象的业务逻辑和计算字段放在哪里而苦苦挣扎。

当前层:

  • 介绍
  • 服务
  • 存储库
  • 数据/实体

我们的存储层具有大部分基本的获取/验证/保存逻辑,尽管服务层做了很多更复杂的验证和保存(因为保存操作也做日志记录、权限检查等)。问题是在哪里放置这样的代码:

Decimal CalculateTotal(LineItemEntity li)
{
  return li.Quantity * li.Price;
}

或者

Decimal CalculateOrderTotal(OrderEntity order)
{
  Decimal orderTotal = 0;
  foreach (LineItemEntity li in order.LineItems)
  {
    orderTotal += CalculateTotal(li);
  }
  return orderTotal;
}

有什么想法吗?

4

8 回答 8

26

让我们回到基础:

服务

服务有 3 种风格:领域服务应用服务基础设施服务

  • 领域服务:封装不适合领域对象的业务逻辑。在您的情况下,您的所有业务逻辑。
  • 应用程序服务:由外部消费者用来与您的系统对话
  • 基础设施服务:用于抽象技术问题(例如 MSMQ、电子邮件提供商等)

存储库

这是您的数据访问和一致性检查的地方。在纯 DDD 中,您的聚合根将负责检查一致性(在持久化任何对象之前)。在您的情况下,您将使用域服务层中的检查。


建议的解决方案:拆分现有服务

使用新的域服务层来封装 DTO 的所有逻辑,以及一致性检查(也许使用Specifications?)。

使用应用程序服务公开必要的FetchOpenOrdersWithLines获取方法(您还可以考虑使用查询规范来包装您的查询。

在您的Repository中,使用您的Domain Services层中的Specifications来检查对象一致性等,然后再持久化您的对象。

您可以在 Evans 的书中找到支持信息:

  • “服务和隔离域层” (第 106 页)
  • “规格” (第 224 页)
  • “查询规范” (第 229 页)
于 2009-12-22T09:03:54.960 回答
11

我很想回答Mu,但我想详细说明一下。总结:不要让您对 ORM 的选择决定您如何定义域模型。

域模型的目的是成为一个丰富的面向对象的 API,对域进行建模。要遵循真正的领域驱动设计,领域模型必须不受技术约束

换句话说,领域模型是第一位的,所有特定于技术的实现随后都由映射器解决,该映射器在领域模型和相关技术之间进行映射。这通常包括两种方式:对于 ORM 的选择可能会引入约束的数据访问层,以及对于 UI 技术施加额外要求的 UI 层。

如果实现与领域模型相距甚远,我们将讨论反腐败层

在您的情况下,您所说的贫血域模型实际上是数据访问层。您最好的办法是定义以技术中立的方式对您的实体的访问进行建模的存储库。

例如,让我们看看您的订单实体。对不受技术约束的订单进行建模可能会导致我们出现这样的情况:

public class Order
{
    // constructors and properties

    public decimal CalculateTotal()
    {
        return (from li in this.LineItems
                select li.CalculateTotal()).Sum();
    }
}

请注意,这是一个普通的旧 CLR 对象 ( POCO ),因此不受技术约束。现在的问题是如何将这些数据输入和输出数据存储?

这应该通过抽象的 IOrderRepository 来完成:

public interface IOrderRepository
{
    Order SelectSingle(int id);

    void Insert(Order order);

    void Update(Order order);

    void Delete(int id);

    // more, specialized methods can go here if need be
}

您现在可以使用您选择的 ORM 来实现 IOrderRepository。但是,某些 ORM(例如 Microsoft 的实体框架)要求您从某些基类派生数据类,因此这根本不适合作为 POCO 的域对象。因此,需要映射。

要意识到的重要一点是,您可能拥有在语义上类似于您的域实体的强类型数据类。然而,这是一个纯粹的实现细节,所以不要对此感到困惑。派生自例如EntityObject 的 Order 类不是 Domain Class - 它是一个实现细节,因此当您实现 IOrderRepository 时,您需要将 Order Data Class映射到 Order Doman Class

这可能是一项乏味的工作,但您可以使用AutoMapper为您完成。

下面是 SelectSingle 方法的实现可能的样子:

public Order SelectSinge(int id)
{
    var oe = (from o in this.objectContext.Orders
              where o.Id == id
              select o).First();
    return this.mapper.Map<OrderEntity, Order>(oe);
}
于 2009-12-25T13:04:37.963 回答
5

这正是服务层的用途——我还看到了称为业务逻辑层的应用程序。

这些是您希望花费大部分时间进行测试的例程,如果它们在自己的层中,那么模拟存储库层应该很简单。

存储库层应尽可能通用化,因此它不适合用于特定类的业务逻辑。

于 2009-12-19T17:31:56.557 回答
4

从您所说的来看,您可能对 Service 和 Repository 层的考虑过于严格。听起来您不希望您的 Presentation 层直接依赖于 Repository 层,并且要实现这一点,您需要从 Service 层中的 Repositories(您的传递方法)中复制方法。

我会质疑这一点。您可以放松一下,并允许两者都在您的表示层中使用,并让您的生活一开始就更简单。也许通过隐藏这样的存储库来问你自己你取得了什么成就。您已经在抽象持久性并使用它们查询 IMPLEMENTATION。这很棒,它们的设计目的是什么。似乎您正在尝试创建一个服务层来隐藏您的实体被持久化的事实。我要问为什么?

至于计算订单总数等。您的服务层将是自然的家。具有 LineTotal(LineItem lineItem) 和 OrderTotal(Order order) 方法的 SalesOrderCalculator 类就可以了。您可能还希望考虑创建一个适当的工厂,例如 OrderServices.CreateOrderCalculator() 以在需要时切换实现(例如,订单折扣税具有特定于国家/地区的规则)。这也可以形成 Order 服务的单一入口点,并通过 IntelliSense 轻松查找事物。

如果所有这些听起来都不可行,那么您可能需要更深入地思考您的抽象正在实现什么,它们如何相互关联以及单一职责原则. 存储库是一种基础架构抽象(隐藏如何保存和检索实体)。服务抽象出业务操作或规则的实现,并为版本控制或变化提供更好的结构。它们通常不像您描述的那样分层。如果您的服务中有复杂的安全规则,那么您的存储库可能是更好的选择。在典型的 DDD 样式模型中,存储库、实体、值对象和服务都将在同一层中并排使用,并作为同一模型的一部分。因此,上面的层(通常是表示)将被这些抽象隔离。在模型中,一个服务的实现可以使用另一个服务的抽象。进一步的改进为谁持有对哪些实体或值对象的引用添加了规则,以强制执行更正式的生命周期上下文。Eric Evans 的书领域驱动设计快速

于 2009-12-22T15:53:13.087 回答
4

如果您的 ORM 技术只能很好地处理 DTO 对象,那并不意味着您必须丢弃丰富的实体对象。你仍然可以用实体对象包装你的 DTO 对象:

public class MonkeyData
{
   public string Name { get; set; }
   public List<Food> FavoriteFood { get; set; }
}

public interface IMonkeyRepository
{
   Monkey GetMonkey(string name) // fetches DTO, uses entity constructor
   void SaveMonkey(Monkey monkey) // uses entity GetData(), stores DTO
}


public class Monkey
{
   private readonly MonkeyData monkeyData;

   public Monkey(MonkeyData monkeyData)
   {
      this.monkeyData = monkeyData;
   }

   public Name { get { return this.monkeyData.Name; } }

   public bool IsYummy(Food food)
   {
      return this.monkeyData.FavoriteFood.Contains(food);
   }

   public MonkeyData GetData()
   {
      // CLONE the DTO here to avoid giving write access to the
      // entity innards without business rule enforcement
      return CloneData(this.monkeyData);
   }

}
于 2009-12-22T17:29:45.947 回答
3

我发现 Dino Esposito 的新书Microsoft® .NE​​T:为企业构建应用程序是解决此类问题和问题的重要知识库。

于 2009-12-24T04:43:48.590 回答
1

服务层。

于 2009-12-19T16:35:33.367 回答
1

如果您想为您的实体添加一些行为,但不能修改您的实体,请尝试扩展方法。不过,我只会在您的示例中这样的简单场景中执行此操作。任何更复杂的东西或在多个实体和/或服务、层或任何应该在域服务中的东西之间的协调,正如已经建议的那样。

例如(从你的例子):

public static class LineItemEntityExtensions
{
  public static decimal CalculateTotal(this LineItemEntity li)
  {
    return li.Quantity * li.Price;
  }
}

public static class OrderEntityExtensions
{
  public static decimal CalculateOrderTotal(this OrderEntity order)
  {
    decimal orderTotal = 0;
    foreach (LineItemEntity li in order.LineItems)
      orderTotal += li.CalculateTotal();
    return orderTotal;
  }
}

public class SomewhereElse
{
  public void DoSomething(OrderEntity order)
  {
    decimal total = order.CalculateOrderTotal();
    ...
  }
}

如果您想要的这些添加很少,您可以将它们全部放在“DomainExtensions”类中,但我建议以常规方式对待它们,并将实体的所有扩展保留在一个类中的一个类中自己的文件.

仅供参考:我唯一一次这样做是当我有一个 L2S 解决方案并且不想弄乱部分内容时。我也没有很多扩展,因为解决方案很小。我更喜欢使用完整的域服务层的想法。

于 2009-12-28T21:10:19.347 回答