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

我们当前的O / RM工具并不真正允许丰富的域模型,因此我们不得不在各地使用贫血(DTO)实体。 这工作得很好,但我仍然在努力放置基于对象的基本业务逻辑和计算字段。

当前图层:

  • 介绍
  • 服务
  • 知识库
  • 数据/实体

我们的存储库层具有大多数基本的提取/validation/保存逻辑,尽管服务层执行了许多更复杂的validation和保存(因为保存操作也执行日志记录,权限检查等)。 问题是在哪里放置这样的代码:

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

有什么想法吗?

让我们回到基础:

服务

服务有三种forms: 域服务应用服务基础设施服务

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

知识库

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


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

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

使用Application Service公开必要的提取方法( FetchOpenOrdersWithLines ),它将请求转发到您的Repository (并使用generics,如Jeremy建议的那样)。 您还可以考虑使用查询规范来包装查询。

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

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

  • “服务和隔离域层” (第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对象( 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完全不适合。 因此,需要映射。

要认识到的重要一点是,您可能拥有强类型的数据类,这些类在语义上类似于您的域实体。 但是,这是一个纯粹的实现细节,所以不要对此感到困惑。 从例如EntityObject派生的Order类不是域类 – 它是一个实现细节,因此当您实现IOrderRepository时,您需要将Order Data Class映射到Order 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(oe); } 

这正是服务层的用途 – 我还看到了被称为BusinessLogic层的应用程序。

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

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

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

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

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

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

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

 public class MonkeyData { public string Name { get; set; } public List 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); } } 

我发现Dino Esposito的新书Microsoft®.NET:为企业架构应用程序是这些类型的问题和问题的知识库。

服务层。

如果要向实体添加一些行为,但无法修改实体,请尝试使用扩展方法。 我只会针对像你的例子中的简单场景这样做。 任何更复杂的或在若干实体和/或服务,层或任何内容之间进行协调的任何内容都应该在域服务中。

例如(来自您的示例):

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

Interesting Posts