我敢说,在拥有“贫血的域模型”和将所有服务塞入域对象之间存在许多灰色阴影。而且很多时候,至少在业务领域和我的经验中,一个对象实际上可能只是数据而已。例如,只要可以对特定对象执行的操作取决于大量其他对象和一些本地化上下文,例如地址。
在我对网上领域驱动文献的回顾中,我发现了很多模糊的想法和著作,但我并不是找不到一个合适的、非平凡的例子来说明方法和操作之间的界限应该在哪里谎言,更重要的是,如何使用当前的技术堆栈来实现它。因此,为了回答这个问题,我将举一个小例子来说明我的观点:
考虑 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服务,以及计算总数后发送电子邮件的例子,我会区分:
- 价格计算后应发送电子邮件的事实
- 应发送订单的哪一部分? (这也可能包括电子邮件模板)
- 发送电子邮件的实际方法
此外,人们不得不怀疑,发送电子邮件是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(...);
}
使用这种方法可以得到以下结果:
- 域对象不包含服务,它们使用服务
- 根据接口机制的性质,域对象与实际服务实现(例如 SMTP、在单独的线程中发送等)分离
- 服务接口是通用的、可重用的,但不知道任何实际的域对象。例如,如果 order 有一个额外的字段,您只需更改
Order 类。
- 您可以轻松模拟服务,轻松测试域对象
- 您可以轻松测试实际的服务实现
我不知道这是否符合某些大师的标准,但这是一种脚踏实地的方法,在实践中效果相当不错。