如何解决松散耦合/dependency injection与富域模型之间的冲突?

编辑:这不是理论层面的冲突,而是实施层面的冲突。

另一个编辑:问题是没有域模型作为仅数据/ DTO与更丰富,更复杂的对象映射,其中Order具有OrderItems和一些calculateTotal逻辑。 具体问题是,例如,Order需要从中国的某些Web服务中获取OrderItem的最新批发价格(例如)。 因此,您运行了一些Spring Service,允许在中国调用此PriceQuery服务。 Order具有calculateTotal,它遍历每个OrderItem,获取最新价格,并将其添加到总数中。

那么您如何确保每个订单都引用此PriceQuery服务? 如何在反序列化,从DB加载和新实例化时恢复它? 这是我的确切问题。

简单的方法是传递对calculateTotal方法的引用,但是如果您的Object在其整个生命周期内部使用此服务该怎么办? 如果它在10种方法中使用怎么办? 每次传递引用都很麻烦。

另一种方法是将calculateTotal移出Order并进入OrderService,但这会打破OO设计,我们会转向旧的“事务脚本”方式。

原帖:

简短版本:富域对象需要引用许多组件,但这些对象会被持久化或序列化,因此它们对外部组件(在本例中为Spring bean:服务,存储库,任何东西)所持有的任何引用都是暂时的并且会被消除。 当对象被反序列化或从DB加载时,需要重新注入它们,但这非常难看,我看不到一种优雅的方法。

更长的版本:有一段时间了,我在Spring的帮助下练习了松耦合和DI。 这对我保持可管理性和可测试性有很大帮助。 不久前,我读了Domain-Driven Design和一些Martin Fowler。 因此,我一直在尝试将我的域模型从简单的DTO(通常是表行的简单表示,只是数据无逻辑)转换为更丰富的域模型。

随着我的域增长并承担新的职责,我的域对象开始需要我在Spring上下文中使用的一些bean(服务,存储库,组件)。 这已成为一场噩梦,也是转换为丰富域名设计最困难的部分之一。

基本上有些点我手动将应用程序上下文的引用注入到我的域中:

  • 当从Repository或其他负责实体加载对象时,因为组件引用是暂时的,显然不会持久化
  • 从Factory创建对象时,因为新创建的对象缺少组件引用
  • 当对象在Quartz作业或其他地方被反序列化时,因为瞬态组件引用被擦除

首先,它很难看,因为我将对象传递给应用程序上下文引用,并期望它通过名称引用它所需的组件。 这不是注射,而是直接拉动。

其次,它是丑陋的代码,因为在所有提到的地方我需要逻辑来注入appContext

第三,它容易出错,因为我必须记住在所有那些地方注入所有这些物体,这比它听起来更难。

必须有一个更好的方法,我希望你能够对它有所了解。

我冒昧地说,有一个“贫血域模型”和将所有服务塞进你的域对象之间有很多灰色阴影。 而且,通常,至少在业务领域和我的经验中,对象实际上可能只是数据; 例如,每当可以对该特定对象执行的操作依赖于大量其他对象和一些本地化上下文时,例如地址。

在我对网络上的域名驱动文献的评论中,我发现了许多模糊的想法和着作,但我无法找到一个正确的,非平凡的例子,说明方法和操作之间的界限应该在哪里,以及更重要的是,如何使用当前的技术堆栈实现它。 因此,为了这个答案的目的,我将举一个小例子来说明我的观点:

考虑Orders和OrderItems这个古老的例子。 “贫血”域模型看起来像:

class Order { Long orderId; Date orderDate; Long receivedById; // user which received the order } class OrderItem { Long orderId; // order to which this item belongs Long productId; // product id BigDecimal amount; BigDecimal price; } 

在我看来,域驱动设计的要点是使用类来更好地模拟实体之间的关系。 因此,非贫血模型看起来像:

 class Order { Long orderId; Date orderDate; User receivedBy; Set items; } class OrderItem { Order order; Product product; BigDecimal amount; BigDecimal price; } 

据说,您将使用ORM解决方案在此处进行映射。 在此模型中,您将能够编写Order.calculateTotal()等方法,该方法将汇总每个订单商品的所有amount*price

因此,模型将是丰富的,从某种意义上说,从业务角度来看有意义的操作(如calculateTotal )将被放置在Order域对象中。 但是,至少在我看来,域驱动设计并不意味着Order应该知道你的持久性服务。 这应该在一个单独的独立层中完成。 持久性操作不是业务领域的一部分,它们是实现的一部分。

即使在这个简单的例子中,也有许多陷阱需要考虑。 是否应该为每个OrderItem加载整个Product ? 如果有大量订单商品,并且您需要大量订单的摘要报告,您是否会使用Java,在内存中加载对象并在每个订单上调用calculateTotal() ? 或者从各个方面来看,SQL查询是一个更好的解决方案。 这就是为什么像Hibernate这样体面的ORM解决方案提供了解决这些实际问题的机制:前者的代理延迟加载和后者的HQL加载。 如果报告生成需要很长时间,那么理论上合理的模型会有什么用?

当然,整个问题非常复杂,而且我能够一次性编写或考虑。 而且我不是在权威的位置上发言,而是在部署业务应用程序时进行简单的日常实践。 希望你会得到一些答案。 随意提供一些额外的细节和你正在处理的例子……

编辑 :关于PriceQuery服务,以及在计算总数后发送电子邮件的示例,我将区分:

  1. 在计算价格后应发送电子邮件的事实
  2. 应该发送订单的哪一部分? (这也可能包括电子邮件模板)
  3. 发送电子邮件的实际方法

此外,人们不得不怀疑,发送电子邮件是Order的固有能力,还是可以用它完成的另一件事,比如坚持,序列化为不同的格式(XML,CSV,Excel)等。

我会做什么,以及我认为好的OOP方法如下。 定义封装准备和发送电子邮件的操作的接口:

  interface EmailSender { public void setSubject(String subject); public void addRecipient(String address, RecipientType type); public void setMessageBody(String body); public void send(); } 

现在,在Order类中,使用电子邮件发件人定义一个操作,通过该操作,订单“知道”如何将自己作为电子邮件发送:

 class Order { ... public void sendTotalEmail(EmailSender sender) { sender.setSubject("Order " + this.orderId); sender.addRecipient(receivedBy.getEmailAddress(), RecipientType.TO); sender.addRecipient(receivedBy.getSupervisor().getEmailAddress(), RecipientType.BCC); sender.setMessageBody("Order total is: " + calculateTotal()); sender.send(); } 

最后,您应该对应用程序操作有一个外观,即对用户操作的实际响应发生的一个点。 在我看来,这是你应该(通过Spring DI)获得服务的实际实现的地方。 例如,这可以是Spring MVC Controller类:

 public class OrderEmailController extends BaseFormController { // injected by Spring private OrderManager orderManager; // persistence private EmailSender emailSender; // actual sending of email public ModelAndView processFormSubmission(HttpServletRequest request, HttpServletResponse response, ...) { String id = request.getParameter("id"); Order order = orderManager.getOrder(id); order.sendTotalEmail(emailSender); return new ModelAndView(...); } 

以下是您使用此方法获得的信息:

  1. 域对象不包含服务,它们使用它们
  2. 根据接口机制的性质,域对象与实际服务实现(例如,SMTP,在单独的线程中发送等)分离
  3. 服务接口是通用的,可重用的,但不了解任何实际的域对象。 例如,如果订单获得额外字段,则只需更改Order类。
  4. 您可以轻松地模拟服务,并轻松地测试域对象
  5. 您可以轻松地测试实际的服务实现

我不知道这是否符合某些大师的标准,但它是一种脚踏实地的方法,在实践中运作得相当好。

Regardinig

如果您的订单需要在每次计算总额时发送电子邮件怎么办?

我会雇用活动。
如果在订单计算其总数时它对您有一些意义,那么让它将事件引发为eventDispatcher.raiseEvent(new ComputedTotalEvent(this))。
然后你监听这类事件,并按照之前的说法回调你的订单,让它格式化一个电子邮件模板,然后发送它。
您的域对象仍然精简,不了解您的要求。
简而言之,将您的问题分成两个要求:
– 我想知道订单何时计算总数;
– 我想在订单总数(新的和不同的)时发送电子邮件;

我找到了答案,至少对那些使用Spring的人来说:

6.8.1。 使用AspectJdependency injection域对象与Spring

我能想到的最简单的方法是在数据访问层中添加一些逻辑,这些逻辑会在将域对象返回到更高层(通常称为服务层)之前注入域对象及其依赖项。 您可以注释每个类的属性以指示需要连接的内容。 如果您不使用Java 5+,则可以为需要注入的每个组件实现一个接口,或者甚至将所有这些都用XML格式化,并将该数据提供给将要进行连接的上下文。 如果你想获得想象力,你可以将其推广到一个方面,并在数据访问层中全局应用它,这样所有拔出域对象的方法都会在返回之后将它们连接起来。

也许你想要的是一种类型的引用对象,它将序列化为全局引用(例如URI),并且能够在其他地方反序列化时作为代理复活。

身份映射模式可能有助于您的方案。 查看Jeremy Miller撰写的文章“ 模式实践” ,讨论这种模式。