我们当前的 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;
}

有什么想法吗?

有帮助吗?

解决方案

让我们回到基础知识:

服务

服务有 3 种风格: 领域服务, 申请服务, , 和 基础设施服务

  • 领域服务 :封装不自然的业务逻辑 适合域对象。就你而言, 全部 您的业​​务逻辑。
  • 申请服务 :供外部消费者用来与您的系统对话
  • 基础设施服务 :用于抽象技术问题(例如MSMQ、电子邮件提供商等)

存储库

这是您的数据访问和 一致性检查 去。在纯粹的 DDD 中,你的 聚合根 将负责检查一致性(在持久化任何对象之前)。就您而言,您将使用您的支票 领域服务 层。


建议的解决方案: 将您现有的服务分开

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

使用 申请服务 公开必要的获取方法(FetchOpenOrdersWithLines),将请求转发给您的 存储库 (并按照杰里米的建议使用泛型)。您也可以考虑使用 查询规格 包装您的查询。

从你的 存储库, , 使用 规格 在你的 领域服务 在持久化对象之前检查对象一致性等的层。

您可以在埃文斯的书中找到支持信息:

  • “服务和隔离域层” (第 106 页)
  • “规格” (第 224 页)
  • 《查询规范》 (第 229 页)

其他提示

我很想回答 , ,但我想详细说明一下。总之: 不要让您选择的 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 对象 ( 波科 ),因此不受技术限制。现在的问题是如何将其传入和传出数据存储?

这应该通过抽象 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 的域对象。因此,需要进行映射。

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

这可能是一项乏味的工作,但您可以使用 自动映射器 为你做这件事。

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

这正是服务层的用途 - 我还看到过一些应用程序,它被称为业务逻辑层。

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

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

从您的说法来看,您可能对服务层和存储库层的思考过于严格。听起来您不希望表示层直接依赖于存储库层,为了实现这一点,您需要在服务层中复制存储库中的方法(您的传递方法)。

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

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

如果这一切听起来不可行,那么您可能需要更深入地思考您的抽象正在实现什么,它们之间如何相互关联以及 单一责任原则. 。存储库是一种基础设施抽象(隐藏实体的保存和检索方式)。服务抽象了业务操作或规则的实现,并允许更好的版本控制或差异结构。它们通常不会按照您描述的方式分层。如果您的服务中有复杂的安全规则,那么您的存储库可能是更好的家。在典型的 DDD 风格模型中,存储库、实体、值对象和服务都将在同一层中并排使用,并作为同一模型的一部分。因此,上面的层(通常是表示层)将被这些抽象隔离。在模型中,一个服务的实现可以使用另一个服务的抽象。进一步的细化添加了规则,规定谁持有对哪些实体或值对象的引用,以强制执行更正式的生命周期上下文。有关这方面的更多信息,我建议您学习 埃里克·埃文斯的书 或者 快速领域驱动设计.

如果您的 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);
   }

}

我找到了迪诺·埃斯波西托的新书 Microsoft® .NE​​T:为企业构建应用程序 成为此类问题的重要知识库。

服务层。

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

例如(从你的例子):

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 解决方案并且不想弄乱部分时。我也没有太多扩展,因为解决方案很小。我更喜欢使用完整的域服务层的想法。

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top