【问题标题】:How can I resolve the conflict between loose coupling/dependency injection and a rich domain model?如何解决松耦合/依赖注入和富域模型之间的冲突?
【发布时间】:2010-10-16 05:08:35
【问题描述】:

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

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

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

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

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

原帖:

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

加长版: 一段时间以来,我在 Spring 的帮助下练习了松散耦合和 DI。它在保持事情的可管理性和可测试性方面帮助了我很多。然而,不久前,我阅读了领域驱动设计和一些 Martin Fowler。因此,我一直在尝试将我的域模型从简单的 DTO(通常是表行的简单表示,只是数据没有逻辑)转换为更丰富的域模型。

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

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

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

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

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

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

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

【问题讨论】:

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


    【解决方案1】:

    我敢说,在拥有“贫血的域模型”和将所有服务塞入域对象之间存在许多灰色阴影。而且很多时候,至少在业务领域和我的经验中,一个对象实际上可能只是数据而已。例如,只要可以对特定对象执行的操作取决于大量其他对象和一些本地化上下文,例如地址。

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

    考虑 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 应该知道您的持久性服务。这应该在一个单独的独立层中完成。持久性操作不是业务领域的一部分,它们是实现的一部分。

    即使在这个简单的例子中,也有很多陷阱需要考虑。是否应该用每个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 有一个额外的字段,您只需更改 Order 类。
    4. 您可以轻松模拟服务,轻松测试域对象
    5. 您可以轻松测试实际的服务实现

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

    【讨论】:

    • 你所描述的工作正常,但我在更复杂的例子上碰壁了。如果您的订单在每次计算总额时都需要发送一封电子邮件怎么办?当然你可以把它带入你的服务中,但这不是真正的 OOP,对吧? Martin F. 甚至不使用服务层
    • 请看我的“另一个编辑”。我真的很想听听你的想法
    【解决方案2】:

    注意

    如果您的订单需要寄出怎么办 每次总数是一封电子邮件 计算?

    我会使用活动。
    如果在订单计算其总数时它对您有意义,则让它引发一个事件作为 eventDispatcher.raiseEvent(new ComputedTotalEvent(this))。
    然后您侦听此类事件,并如前所述回调您的订单,使其格式化电子邮件模板,然后发送。
    您的域对象仍然很精简,但对您的需求一无所知。
    简而言之,将您的问题分为 2 个要求:
    - 我想知道订单何时计算其总数;
    - 当订单有(新的和不同的)总数时,我想发送一封电子邮件;

    【讨论】:

      【解决方案3】:

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

      6.8.1. Using AspectJ to dependency inject domain objects with Spring

      【讨论】:

        【解决方案4】:

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

        【讨论】:

        • 你的第一个建议(在 DAO 中注入)我现在正在做,但正如我所说的,序列化、工厂等存在问题。我真的喜欢你使用的建议方面+界面。这将分离注入逻辑并使其更清洁/更易于管理......
        • 处理其他两种情况具有挑战性。您可以在将类加载到 JVM 时对其进行检测。基本上是另一种 AOP 方法,但在实现方面非常不同。以下是关于他们如何在 Terracotta 中执行此操作的一些信息:tinyurl.com/cgx8ne
        【解决方案5】:

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

        【讨论】:

          【解决方案6】:

          Identity Map 模式可能对您的方案有所帮助。查看 Jeremy Miller 撰写的文章 Patterns In Practice,他在其中讨论了这种模式。

          【讨论】:

            猜你喜欢
            • 2013-05-26
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2017-11-21
            相关资源
            最近更新 更多