我敢说,拥有“贫乏的域模型”和将所有服务塞进域对象之间存在许多灰色地带。通常,至少在业务领域和根据我的经验,对象实际上可能只不过是数据;例如,每当可以对该特定对象执行的操作依赖于大量其他对象和一些本地化上下文(例如地址)时。
在我对网上领域驱动文献的回顾中,我发现了很多模糊的想法和著作,但我并非无法找到一个适当的、重要的例子来说明方法和操作之间的界限应该在哪里,并且,更重要的是,如何用当前的技术栈来实现它。因此,为了回答这个问题,我将举一个小例子来说明我的观点:
考虑一下古老的 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。如果报告生成需要很长时间,那么理论上合理的模型有什么用呢?
当然,整个问题相当复杂,远非我能够一次性写出或考虑的。我并不是站在权威的立场上发言,而是部署业务应用程序的简单日常实践。希望您能从这个答案中得到一些东西。请随意提供一些额外的细节和您正在处理的示例......
Edit: 关于PriceQuery
服务,以及计算总数后发送电子邮件的示例,我会区分:
- 价格计算后应发送电子邮件的事实
- 应发送订单的哪一部分? (这还可能包括电子邮件模板)
- 发送电子邮件的实际方法
此外,人们不得不想知道,发送电子邮件是否是一个人固有的能力?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 MVCController
class:
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(...);
}
通过这种方法您可以获得以下结果:
- 域对象不包含服务,它们use them
- 根据接口机制的性质,域对象与实际服务实现(例如 SMTP、在单独线程中发送等)分离
- 服务接口是通用的、可重用的,但不知道任何实际的域对象。例如,如果订单有一个额外的字段,您只需更改
Order
class.
- 您可以轻松模拟服务,并轻松测试域对象
- 您可以轻松测试实际的服务实现
我不知道这是否符合某些大师的标准,但这是一种在实践中相当有效的脚踏实地的方法。