一、前言

领域驱动(DDD:Domain-Driven Design)本质上是面向业务将代码结构划分清楚,易于开发维护,并没有什么神奇之处,软件开发没有银弹,不要神话某种技术。

领域是一个面向业务层面的软件设计概念,对计算机来讲本无领域,计算机所能处理的仅仅是一条条的机器指令。在程序设计语言的发展历程中,先后出现了面向过程、函数式、逻辑式、面向对象等多种不同风格的程序设计语言,这种风格被学术界称为程序设计语言范式。

image.png

面向过程、面向对象是目前最为主流的程序设计语言范式,回顾其发展历程,可以清晰的发现,程序设计语言提供了越来越丰富的抽象、封装能力,例如把一行行的汇编指令封装到函数中,然后在封装到类、包甚至模块中。程序设计语言的发展,本质上就是利用封装、抽象、组合等等方法降低代码的复杂度,但对于CPU来讲仍然是一条条的机器指令。

二、挑战

领域驱动在业界已经流行多年,经验丰富的程序员或多或少都在项目中引入了一些DDD的思想,但完全遵照 DDD构建的项目却很少。很多程序员对DDD的认知是看起来高大上但不知如何实际应用,除了领会DDD思想有一定难度外,面向对象与数据库实体模型间的阻抗也是一个非常重要的原因,而几乎所有讲解DDD的书籍、文章中都选择性的回避了这一问题。本着知行合一的态度,本文将讨论DDD实现过程中会遇到哪些具体的问题以及如何解决。

很多讲述DDD的文章中经常以订单的场景为例进行讲解,本文中我们也延续这种方式。首先设计领域对象及值对象。订单是一个独立存在的业务对象,因此将订单(Order)作为领域对象,订单项(OrderItem)是不能脱离订单而独立存在的业务对象,因此将订单项作为值对象。由于业务过程只直接与订单对象交互,因此订单也是聚合根。

  1. public class Order {
  2. /**
  3. * 订单聚合
  4. */
  5. private Long id;
  6. private Customer customer;
  7. private OrderStatus status;
  8. private BigDecimal totalPrice;
  9. private BigDecimal totalPayment;
  10. // 其他属性
  11. /**
  12. * 订单项子聚合
  13. */
  14. private List<OrderItem> orderItems;
  15. /**
  16. * 创建聚合根
  17. */
  18. public static Order create(/*输入参数*/) {
  19. List<OrderItem> items = new ArrayList<>();
  20. items.add(/**/);
  21. items.add(/**/);
  22. Order order = new Order();
  23. order.setItems(items);
  24. order.setStatus(/**/);
  25. // ...
  26. return order;
  27. }
  28. }
  29. public class OrderItem {
  30. private Long id;
  31. private Product product;
  32. private BigDecimal amount;
  33. private BigDecimal subTotal;
  34. private OrderItemStatus status;
  35. // 其他属性
  36. }

OrderServiceImpl作为业务过程,提供了创建订单(createOrder)、修改订单(updateOrder)的两个方法。

  1. @Service
  2. public class OrderServiceImpl implements OrderService {
  3. @Autowire
  4. private OrderRepository orderRepository;
  5. @Override
  6. @Transcation
  7. public void createOrder(OrderCommand command) {
  8. Order order = Order.create(/*输入参数*/);
  9. // 。。。
  10. orderRepository.save(order);
  11. }
  12. @Override
  13. @Transcation
  14. public void updateOrder(OrderCommand command) {
  15. Order order = orderRepository.find(command.getOrderId);
  16. // 。。。
  17. orderRepository.save(order);
  18. }
  19. }

到目前为止,代码看起来很干净、漂亮,完全符合DDD设计,但我们并没有展示OrderRepository中的代码,事实上DDD最难实现的就是Repository。

在实践过程中,没有ORM的支持很难实现领域对象到数据库表之间的映射。

实现Repository.save主要有这么几种方法:

  • 隐式读时复制
  • 隐式写时复制
  • 显式写前复制

《实现领域驱动设计》

隐式读时复制(mplicit Copy-on-Read)[Keith&Stafford]:在从数据存储中读取一个对象时,持久化机制隐式地对该对象进行复制,在提交时,再将该复制对象与客户端中的对象进行比较。详细过程如下:当客户端请求持久化机制从数据存储中读取一个对象时,该持久化机制一方面将获取到的对象返回给客户端,一方面立即创建一份该对象的备份(除去延迟加载部分,这些部分可以在之后实际加载时再进行复制)。当客户端提交事务时,持久化机制把该复制对象与客户端中的对象进行比较。所有的对象修改都将更新到数据存储中。

隐式写时复制(Implicit Copy-on-Write)[Keith&Stafford]:持久化机制通过委派来管理所有被加载的持久化对象。在加载每个对象时,持久化机制都会为其创建一个微小的委派并将其交给客户端。客户端并不知道自己调用的是委派对象中的行为方法,委派对象会调用真实对象中的行为方法。当委派对象首次接收到方法调用时,它将创建一份对真实对象的备份。委派对象将跟踪发生在真实对象上的改变,并将其标记为“肮脏的”(dity)。当事务提交时, 该事务检查所有的“肮脏”对象并将对它们的修改更新到数据存储中。

显式写前复制(Explicit Copy-before-Write)[Keith&Stafford]:这里的“显式”表示,在修改对象之前, 客户端必须通知Unit of Work.。在接到通知之后,Unit of Work便会克隆相应的领域对象以做好修改准备(Unit of Work称为“编辑(edit)”,本章后面将讨论到)。这种方式的好处在于,TopLink只有在需要的时候才会占用内存。·


再来看看查询的场景,Order中包含一个 List,在OrderRepository.find中进行两次数据库查询完成Order聚合根组装。如果OrderItem数量较少这没什么问题,但对于数据量较大的场景显然不能将OrderItem一次性查出全部放入内存。

  1. @Repository
  2. public class OrderRepositoryImpl implements OrderRepository {
  3. @Autowire
  4. private OrderMapper orderMapper;
  5. @Autowire
  6. private OrderItemMapper orderItemMapper;
  7. @Override
  8. public Order find(long orderId) {
  9. return new Order(
  10. orderMapper.select(orderId), orderItemMapper.select(orderId));
  11. }
  12. }

一种变通的方法是Order不存储 List orderItems,放弃OrderRepository.save而是直接在对象的业务方法中实现持久化逻辑,但这并不是DDD推荐的做法。

  1. public class Order {
  2. /**
  3. * 直接通过SQL查询数据
  4. */
  5. public List<OrderItem> getOrders(/*查询条件*/) {
  6. // select * from order_item where ...
  7. return orderItems;
  8. }
  9. public void addOrderItem(OrderItem orderItem) {
  10. // insert into
  11. }
  12. public void updateOrderItem(OrderItem orderItem) {
  13. // update
  14. }
  15. public void removeOrderItem(OrderItem orderItem) {
  16. // delete
  17. }
  18. }


对于1-N大数据量的问题,《实现领域驱动设计》也给出了相应的方案。

《实现领域驱动设计》

有时,如果我们要获取聚合根下的某些子聚合,我们不用先从资源库中获取到聚合根,然后再从聚合根中获取这些子聚合,而是可以直接从资源库中返回。在有些情况下,这种做法是有好处的。比如,某个聚合根拥有一个很大的实体类型集合,而你需要根据某种查询条件返回该集合中的一部分实体。当然,只有在聚合根中提供了对该实体集合的导航时,我们才能这么做,否则,我们便违背了聚合的设计原则。我建议不要因为客户端的方便而提供这种访问方式。更多的时候,采用这种方式是由于性能上的考虑,比如从聚合根中访问子聚合将带来性能瓶颈的时候。此时的查找方法和其他查找方法具有相同的基本特征,只是它直接返回聚合根下的子聚合,而不是聚合根本身。无论如何,请慎重使用这种方式。

除了这些问题外,应用DDD的另一个问题就是性能问题。例如如果想修改订单项,必然先查询订单,然后在查询订单项,这样不如直接操作order_item表效率高,但从业务上讲确实清晰易懂。

  1. public void updateOrder(OrderCommand command) {
  2. Order order = orderRepository.find(command.getOrderId);
  3. order.getOrderItems().get(i).setXXX();
  4. // 。。。
  5. orderRepository.save(order);
  6. }

三、实现

从可落地实践的角度讲,放弃Repository中的save方法,改由在领域对象中实现持久化更为容易,仅仅是将贫血模型变为充血模型。

  1. public class Order {
  2. /**
  3. * 直接通过SQL查询数据
  4. */
  5. public List<OrderItem> getOrders(/*查询条件*/) {
  6. // select * from order_item where ...
  7. return orderItems;
  8. }
  9. public void addOrderItem(OrderItem orderItem) {
  10. // insert into
  11. }
  12. public void updateOrderItem(OrderItem orderItem) {
  13. // update
  14. }
  15. public void removeOrderItem(OrderItem orderItem) {
  16. // delete
  17. }
  18. }
  19. @Repository
  20. public class OrderServiceImpl implements OrderService {
  21. @Override
  22. @Transcation
  23. public void updateOrder(OrderCommand command) {
  24. Order order = orderRepository.find(command.getOrderId);
  25. order.updateAddress(...);
  26. order.addOrderItem(...);
  27. order.updateOrderItem(...);
  28. // 。。。
  29. }
  30. }

这里也必须客观的评价,DDD只是一种程序设计方法,绝非最优,把Order对象中的方法提取出来放到OrderDomainService中也可以,代码看起来同样清晰易懂,但这就并非是DDD所推荐的方式了。归根到底,DDD还是沿用了面向对象的思想,将数据与行为封装到一起。

  1. @Repository
  2. public class OrderServiceImpl implements OrderService {
  3. @Override
  4. @Transcation
  5. public void updateOrder(OrderCommand command) {
  6. orderDomainService.updateAddress(...);
  7. orderDomainService.addOrderItem(...);
  8. orderDomainService.updateOrderItem(...);
  9. // 。。。
  10. }
  11. }
  12. public class OrderDomainServiceImpl implements OrderDomainService {
  13. //
  14. }


四、推荐阅读

image.png
实现领域驱动设计/(美)弗农(Vmon.V.)著:滕云译.一北京:电了工业出版社,2014.3 I书名原文:Implementing domain—driven design。

业界出版领域驱动设计方面的图书很多,我个人比较推荐这本书,内容讲解清晰明了。