如果您被迫使用Anemic域模型,那么您将业务逻辑和计算字段放在何处?

时间:2009-12-19 16:33:07

标签: c# architecture dns domain-driven-design anemic-domain-model

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

有什么想法吗?

8 个答案:

答案 0 :(得分:24)

让我们回到基础:

服务

服务有三种形式:域服务应用服务基础架构服务

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

存储库

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


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

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

使用应用程序服务公开必要的提取方法(FetchOpenOrdersWithLines),将请求转发到存储库(并使用泛型,如Jeremy建议的那样) )。您还可以考虑使用查询规范来包装查询。

存储库中,使用域服务层中的规范来检查对象一致性等,然后再保留对象。

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

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

答案 1 :(得分:11)

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

域模型的目的是成为一个丰富的面向对象的API,为域建模。要遵循真Domain-Driven Design,必须定义域模型不受技术限制

换句话说,域模型首先,所有特定于技术的实现随后由映射器解决,该映射在域模型和相关技术之间进行映射。这通常包括两种方式: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的Entity Framework)要求您从某些基类派生数据类,因此这与域对象作为POCO完全不适合。因此,需要进行映射。

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

这可能是一项繁琐的工作,但你可以使用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);
}

答案 2 :(得分:5)

这正是服务层的用途 - 我也看到了应用程序,它被称为BusinessLogic层。

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

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

答案 3 :(得分:4)

根据您的说法,您可能会过于严格地考虑您的服务和存储库层。听起来您不希望您的表示层直接依赖于Repository层,为此,您需要在Service层中复制存储库中的方法(传递方法)。

我会质疑。你可以放松一下,并允许在你的表现层中使用它们,让你的生活更简单。也许通过隐藏这样的存储库来问自己你的成就。您已经抽象了持久性并使用它们查询IMPLEMENTATION。这很棒,它们的设计目的也是如此。看起来好像你正在尝试创建一个服务层来隐藏你的实体根本就存在的事实。我问为什么?

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

如果所有这些听起来都不可行,那么您可能需要更深入地思考您的抽象实现的内容,它们之间的关系以及Single Responsibility Principle。存储库是一种基础结构抽象(隐藏实体的保存和检索)。服务抽象出业务操作或规则的实现,并允许更好的版本控制或差异结构。它们通常不会按照您描述的方式分层。如果您的服务中有复杂的安全规则,您的存储库可能是更好的家。在典型的DDD样式模型中,存储库,实体,值对象和服务将在同一层中作为相同模型的一部分彼此并排使用。因此,上面的层(通常是表示)将被这些抽象隔离。在模型中,一个服务的实现可以使用另一个服务的抽象。进一步的改进将规则添加到谁持有对哪些实体或值对象实施更正式的生命周期上下文的引用。有关此问题的更多信息,我建议您学习Eric Evans bookDomain Driven Design Quickly

答案 4 :(得分: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);
   }

}

答案 5 :(得分:3)

我发现Dino Esposito的新书Microsoft® .NET: Architecting Applications for the Enterprise是这类问题和问题的重要知识库。

答案 6 :(得分:1)

服务层。

答案 7 :(得分: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解决方案并且不想弄乱这些部分。我也没有很多扩展,因为解决方案很小。我更喜欢更好地使用完整的域名服务层。