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

时间:2009-03-29 09:23:52

标签: java spring dependency-injection domain-driven-design

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

另一个编辑: 问题是没有域模型作为仅数据/ 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

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

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

6 个答案:

答案 0 :(得分:4)

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

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

考虑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<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. 发送电子邮件的实际方法
  4. 此外,人们不得不怀疑,发送一封电子邮件是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. 您可以轻松测试实际的服务实施
    6. 我不知道这是否符合某些大师的标准,但它是一种在实践中运作良好的脚踏实地的方法。

答案 1 :(得分:2)

Regardinig

  

如果您的订单需要发送,该怎么办?   每次总计都会收到一封电子邮件   如何计算?

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

答案 2 :(得分:2)

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

6.8.1. Using AspectJ to dependency inject domain objects with Spring

答案 3 :(得分:1)

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

答案 4 :(得分:0)

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

答案 5 :(得分:0)

Identity Map模式可能有助于您的方案。查看Jeremy Miller撰写的文章Patterns In Practice,他将讨论这种模式。