如果您被迫使用贫血领域模型,您将业务逻辑和计算字段放在哪里?

发布于 2024-08-15 15:16:44 字数 619 浏览 1 评论 0原文

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

有什么想法吗?

Our current O/RM tool does not really allow for rich domain models, so we are forced to utilize anemic (DTO) entities everywhere. This has worked fine, but I continue to struggle with where to put basic object-based business logic and calculated fields.

Current layers:

  • Presentation
  • Service
  • Repository
  • Data/Entity

Our repository layer has most of the basic fetch/validate/save logic, although the service layer does a lot of the more complex validation & saving (since save operations also do logging, checking of permissions, etc). The problem is where to put code like this:

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

or

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

Any thoughts?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(8

旧人 2024-08-22 15:16:44

让我们回到基础知识:

服务

服务有 3 种类型:域服务应用程序服务基础设施服务

  • 域服务 strong> :封装了自然不具备的业务逻辑
    适合域对象。就您而言,是您的所有业务逻辑。
  • 应用程序服务:由外部消费者用来与您的系统对话
  • 基础设施服务:用于抽象技术问题(例如
    MSMQ、电子邮件提供商等)

存储库

这是您的数据访问和一致性检查所在的位置。在纯 DDD 中,您的聚合根将负责检查一致性(在保留任何对象之前)。在您的情况下,您将使用来自域服务层的检查。


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

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

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

在保留对象之前,从您的存储库中,使用域服务层中的规范来检查对象一致性等。

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

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

Let's get back to basics:

Services

Services come in 3 flavours: Domain Services, Application Services, and Infrastructure Services

  • Domain Services : Encapsulates business logic that doesn't naturally
    fit within a domain object. In your case, all of your business logic.
  • Application Services : Used by external consumers to talk to your system
  • Infrastructure Services : Used to abstract technical concerns (e.g.
    MSMQ, email provider, etc)

Repository

This is where your data-access and consistency checks go. In pure DDD, your Aggregate Roots would be responsible for checking consistency (before persisting any objects). In your case, you would use checks from your Domain Services layer.


Proposed solution: Split your existing services apart

Use a new Domain Services layer to encapsulate all logic for your DTOs, and your consistency checks too (using Specifications, maybe?).

Use the Application Service to expose the necessary fetch methods (FetchOpenOrdersWithLines), which forward the requests to your Repository (and use generics, as Jeremy suggested). You might also consider using Query Specifications to wrap your queries.

From your Repository, use Specifications in your Domain Services layer to check object consistency etc before persisting your objects.

You can find supporting info in Evans' book:

  • "Services and the Isolated Domain Layer" (pg 106)
  • "Specifications" (pg 224)
  • "Query Specifications" (pg 229)
滿滿的愛 2024-08-22 15:16:44

我很想回答 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 不是一个域类 - 它是一个实现细节,因此当您实现 IOrderRepository 时,您需要将订单数据类映射到订单Doman 类。

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

I'm tempted to answer Mu, but I'd like to elaborate. In summary: Don't let your choice of ORM dictate how you define your Domain Model.

The purpose of the Domain Model is to be a rich object-oriented API that models the domain. To follow true Domain-Driven Design, the Domain Model must be defined unconstrained by technology.

In other words, the Domain Model comes first, and all technology-specific implementations are subsequently addressed by mappers that map between the Domain Model and the technology in question. This will often include both ways: to the Data Access Layer where the choice of ORM may introduce constraints, and to the UI layer where the UI technology imposes additional requirements.

If the implementation is extraordinarily far from the Domain Model, we talk about an Anti-Corruption Layer.

In your case, what you call an Anemic Domain Model is actually the Data Access Layer. Your best recourse would be to define Repositories that model access to your Entities in a technology-neutral way.

As an example, let's look at your Order Entity. Modeling an Order unconstrained by technology might lead us to something like this:

public class Order
{
    // constructors and properties

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

Notice that this a Plain Old CLR Object ( POCO ) and is thus unconstrained by technology. Now the question is how you get this in and out of your data store?

This should be done via an abstract 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
}

You can now implement IOrderRepository using your ORM of choice. However, some ORMs (such as Microsoft's Entity Framework) requires you to derive the data classes from certain base classes, so this doesn't fit at all with Domain Objects as POCOs. Therefor, mapping is required.

The important thing to realize is that you may have strongly typed data classes that semantically resemble your Domain Entities. However, this is a pure implementation detail, so don't get confused by that. An Order class that derives from e.g. EntityObject is not a Domain Class - it's an implementation detail, so when you implement IOrderRepository, you need to map the Order Data Class to the Order Doman Class.

This may be tedious work, but you can use AutoMapper to do it for you.

Here's how an implementation of the SelectSingle method might look:

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);
}
最终幸福 2024-08-22 15:16:44

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

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

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

This is exactly what the service layer is for - I've also seen applications where it's called the BusinessLogic layer.

These are the routines you'll want to spend most of your time testing, and if they're in their own layer then mocking out the repository layer should be straightforward.

The repository layer should be genericized as much as possible, so it's not an appropriate place for business logic that's individual to particular classes.

删除会话 2024-08-22 15:16:44

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

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

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

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

From what you say it may be that you’re thinking too rigidly about your Service and Repository layers. It sounds like you don’t want your Presentation layer to have a direct dependency on the Repository layer and to achieve this you are duplicating methods from your Repositories (your pass-through methods) in the Service layer.

I would question that. You could relax that and allow both to be used within your Presentation layer and make your life simpler for a start. Maybe ask your self what your achieving by hiding the Repositories like that. You’re already abstracting persistence and querying IMPLEMENTATION with them. This is great and what they are designed for. It seems as though you’re trying to create a service layer that hides the fact your entities are persisted at all. I’d ask why?

As for calculating Order totals etc. Your Service layer would be the natural home. A SalesOrderCalculator class with LineTotal(LineItem lineItem) and OrderTotal(Order order) methods would be fine. You may also wish to consider creating an appropriate Factory e.g. OrderServices.CreateOrderCalculator() to switch the implementation if required (tax on order discount has country specific rules for instance). This could also form a single entry point to Order services and make finding things easy through IntelliSense.

If all this sounds unworkable it may be you need to think more deeply about what your abstractions are achieving, how they relate to each other and the Single Responsibility Principle. A Repository is an infrastructure abstraction (hiding HOW entities are saved and retrieved). Services abstract away the implementation of business actions or rules and allow a better structure for versioning or variance. They are not generally layered in the way you describe. If you have complex security rules in your Services, your Repositories may be the better home. In a typical DDD style model, Repositories, Entities, Value Objects and Services would all be used along side each other in the same layer and as part of the same model. Layers above (typically presentation) would therefore be insulated by these abstractions. Within the model the implementation of one service may use the abstraction of another. A further refinement adds rules to who holds references to which entities or value objects enforcing a more formal lifecycle context. For more information on this I would recommend studying the Eric Evans book or Domain Driven Design Quickly.

久随 2024-08-22 15:16:44

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

}

If your ORM technology only handles DTO objects well, that doesn't mean you have to throw out rich entity objects. You can still wrap your DTO objects with entity objects:

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

}
瞳孔里扚悲伤 2024-08-22 15:16:44

I've found Dino Esposito's new book Microsoft® .NET: Architecting Applications for the Enterprise to be a great repository of knowledge for these types of questions and issues.

压抑⊿情绪 2024-08-22 15:16:44

服务层。

The service layer.

殤城〤 2024-08-22 15:16:44

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

例如(从您的示例中):

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

If you want to add a bit of behavior to your entities, but can't modify your entities, give extension methods a try. I'd only do this for simple scenarios like in your example though. Anything more complex or that coordinates between several entities and/or services, layers, or whatever should be in a domain service as suggested already.

For example (from your examples):

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

If there are very few of these additions that you want, you can just put them all in a "DomainExtensions" class, but I'd otherwise suggest treating them with regular respect and keep all of an entity's extensions in one class in its own file.

FYI: The only time I've done this is when I had a L2S solution and didn't want to mess with the partials. I also didn't have many extensions because the solution was small. I like the idea of going with a full blown domain services layer better.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文