首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >问答首页 >如何解决松散耦合/依赖注入与富域模型之间的冲突?

如何解决松散耦合/依赖注入与富域模型之间的冲突?
EN

Stack Overflow用户
提问于 2009-03-29 09:23:51
回答 6查看 2.7K关注 0票数 17

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

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

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

简单的方法是传递对calculateTotal方法的引用,但是如果对象在其生命周期内在内部使用该服务,怎么办?如果在10种方法中使用呢?每次传递引用都会变得很麻烦。

另一种方法是将calculateTotal从顺序移到OrderService中,但这违背了OO设计,我们转向了旧的“事务脚本”方式。

原始员额:

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

更长的版本:已经有一段时间了,我在Spring的帮助下练习了松耦合和DI。它帮助我保持了一些可管理和可测试的功能。然而,不久前,我读过领域驱动的设计和一些马丁·福勒。因此,我一直试图将我的域模型从简单的DTO(通常是表行的简单表示,只是数据没有逻辑)转换为更丰富的域模型。

随着域的增长并承担新的责任,我的域对象开始需要Spring上下文中的bean(服务、存储库、组件)。这很快就变成了噩梦,也是转换为丰富域设计的最困难的部分之一。

基本上,在某些点上,我手动地将对应用程序上下文的引用注入到我的域中:

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

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

第二,这是丑陋的代码,因为在所有提到的地方,我需要逻辑注入一个appContext

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

一定有更好的方法,我希望你能对此有所了解。

EN

回答 6

Stack Overflow用户

回答已采纳

发布于 2009-04-26 17:50:41

我找到了答案,至少对于那些使用Spring的人是这样的:

6.8.1.使用AspectJ用Spring注入域对象

票数 2
EN

Stack Overflow用户

发布于 2009-03-29 18:39:54

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

在我对网络上领域驱动的文献的回顾中,我发现了很多模糊的想法和著作,但我并没有找到一个恰当的、非平凡的例子,说明方法和操作之间的界限应该在哪里,而且,更重要的是,如何用当前的技术栈实现这一点。因此,为了回答这个问题,我将举出一个小例子来说明我的观点:

考虑一个古老的订单和OrderItems的例子。“贫血”域模型如下所示:

代码语言:javascript
运行
复制
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;
 }

在我看来,领域驱动设计的重点是使用类来更好地建模实体之间的关系。所以,一个非贫血的模型看起来应该是:

代码语言:javascript
运行
复制
class Order {
   Long orderId;
   Date orderDate;
   User receivedBy;
   Set<OrderItem> items;
}

class OrderItem {
   Order order;
   Product product;
   BigDecimal amount;
   BigDecimal price;
}

据推测,您将使用ORM解决方案在这里进行映射。在这个模型中,您可以编写一个方法(如Order.calculateTotal() ),它将对每个订单项的所有amount*price进行汇总。

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

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

当然,整个问题是相当复杂的,更多的是,我可以在一次会议上写或考虑。我说的不是权威,而是部署商业应用程序的简单日常实践。希望你能从这个答案中得到些什么。请提供一些额外的细节和你正在处理的例子.

编辑:关于PriceQuery服务,以及在计算完总计后发送电子邮件的示例,我将对以下内容进行区分:

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

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

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

代码语言:javascript
运行
复制
 interface EmailSender {
     public void setSubject(String subject);
     public void addRecipient(String address, RecipientType type);
     public void setMessageBody(String body);
     public void send();
 }

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

代码语言:javascript
运行
复制
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 )获得服务的实际实现的地方。例如,这可以是Spring Controller类:

代码语言:javascript
运行
复制
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获得了额外的字段,则只需要更改Order类。
  4. 您可以轻松地模拟服务,也可以轻松地测试域对象。
  5. 您可以轻松地测试实际的服务实现。

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

票数 4
EN

Stack Overflow用户

发布于 2009-04-24 17:21:35

雷格丁尼

如果每次计算总数时,您的订单都需要发送一封电子邮件,怎么办?

我会利用事件。

如果当订单计算其总数时,它对您有一定意义,那么让它以eventDispatcher.raiseEvent(新的ComputedTotalEvent(this))的形式引发一个事件。

然后,您监听这类事件,并按前面所说的回调您的订单,让它格式化电子邮件模板,然后发送它。

您的域对象仍然是精简的,对此您的需求一无所知。

简而言之,将您的问题分成两个需求:

  • 我想知道一个订单何时计算出它的总数;
  • 我想发送电子邮件时,一个订单有一个(新的和不同的)总数;
票数 2
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/694374

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档