Bootstrap

领域驱动落地实现

领域驱动(DDD:Domain-Driven Design)在业界已经流行多年,经验丰富的程序员或多或少都在项目中引入了一些DDD的思想,但完全遵照DDD构建的项目却很少。除了领会DDD思想有一定难度外,面向对象与数据库实体模型间的阻抗也是一个非常重要的原因,这个原因也一直困扰我很长时间。

文本中以日常熟悉的订单为例,讨论一下使用DDD会遇到哪些问题以及如何解决。订单聚合包括订单(Order)、订单明细行(OrderItem)两个实体,其中订单是聚合根。很多讲述DDD的文章中经常以类似的代码进行讲解,本文中我们也延续这种描述方式。

public class Order {
    /**
     * 订单聚合
     */
    private Long id;
    private Customer customer;
    private OrderStatus status;
    private BigDecimal totalPrice;
    private BigDecimal totalPayment;

    // 其他属性
    
    /**
     * 订单项子聚合
     */
    private List orderItems;

    /**
     * 创建聚合根
     */
    public static Order create(/*输入参数*/) {
        List items = new ArrayList<>();
        items.add(/**/);
        items.add(/**/);

        Order order = new Order();
        order.setItems(items);
        order.setStatus(/**/);
        // ...
        
        return order;
    }
} 

public class OrderItem {
    private Long id;
    private Product product;
    private BigDecimal amount;
    private BigDecimal subTotal;
    private OrderItemStatus status;

    // 其他属性
}

@Service
public class OrderServiceImpl implements OrderService {
    @Autowire
    private OrderRepository orderRepository;
    
    @Override
    @Transcation
    public void createOrder(OrderCommand command) {
        Order order = Order.create(/*输入参数*/);
        orderRepository.save(order);
    }
} 

到目前为止,代码看起来很干净、漂亮,完全符合DDD设计,Order是一个聚合根,OrderItem是其中的子聚合,但我们并没有展示OrderRepository中的代码,事实上DDD实现层面最难处理的就是Repository。通常有这么几个难点:

我们先来看一下问题一如何实现upsert逻辑。众所周知,关系型数据库将插入、更新分为两个独立操作。代码层面我们希望save能够实现upsert逻辑,代码中可以通过order.id是否为null进行区分,如果id等于null意味着是一个新的对象需要执行insert,否则执行update。

@Repository
public class OrderRepositoryImpl implements OrderRepository {
    @Autowire
    private OrderMapper orderMapper;
    
    @Autowire
    private OrderItemMapper orderItemMapper;
    
    @Override
    public void save(Order order) {
        if(order.getId() == null) {
            orderMapper.insert(order);
        
            for(OrderItem item : order.getItems()) {
                orderItemMapper.insert(item)
            }
        } else {
            // update
        }
    }
}

上面代码中并没有给出update的实现过程,通常在service中会这样写代码。

@Service
public class OrderServiceImpl implements OrderService {
    @Autowire
    private OrderRepository orderRepository;

    @Override
    @Transcation
    public void updateOrder(/*输入参数*/) {
        Order order = orderRepository.find(orderId);
        order.getOrderItems().get(orderItemNumber).setStatus(/**/);
        
        orderRepository.save(order);
    }
} 

前文中提到Order与OrderItem是1对N的关系,Order聚合根包含着order表中的一行数据和order_item表中的N行数据,对聚合根的操作放在service中,而实际的db更新却在OrderRepository.save中。对于问题二,在1-N关系中判断哪个元素发生变更就是一个要解决的问题。

代码中,可以在每个实体上添加一个字段记录变更状态来解决这个问题。

public class EntityState {
    /**
     * 记录变化状态,1:insert、2:update、3:delete、4: none
     */
    protected int changeState;
}

public class Order extend EntityState {
    private Long id;
    private Customer customer;
    private OrderStatus status;
    private BigDecimal totalPrice;
    private BigDecimal totalPayment;
    private List items;
} 

public class OrderItem extend EntityState {
    private Long id;
    private Product product;
    private BigDecimal amount;
    private BigDecimal subTotal;
    private OrderStatus status;

    // 其他属性
    
    public void setStatus(OrderStatus status) {
        //  update
        this.changeState = 2;
        
        this.status = status;
        
        // ...
    } 
    
    // ...
}

@Repository
public class OrderRepositoryImpl implements OrderRepository {
    @Autowire
    private OrderMapper orderMapper;
    
    @Autowire
    private OrderItemMapper orderItemMapper;
    
    @Override
    public void save(Order order) {
        if(order.getId() == null) {
           // insert ...
           // ...
        } else {
            // 处理Order
            // ...
            
            // 处理OrderItem
            for(OrderItem item : order.getOrderItems()) {
                switch(item.getChangeStatus()) {
                case 1:
                    orderItemMapper.insert(item);
                    break;
                case 2:
                    orderItemMapper.update(item);
                    break;
                case 3:
                    orderItemMapper.delete(item.getId());
                    break;
                default:
                    break;
                }
            }
        }
    }
}

@Service
public class OrderServiceImpl implements OrderService {
    @Autowire
    private OrderRepository orderRepository;
    
    @Override
    @Transcation
    public void updateOrder(OrderItem orderItem) {
        Order order = orderRepository.find(orderId);
        
        OrderItem orderItem = order.getOrderItems().stream().filter(elem -> elem.getId().equals(orderItem.getId())).findFirst().get();
        orderItem.setStatus(/**/);
        
        orderRepository.save(order);
    }
}

解决完更新的问题,我们再来看看查询的场景。

public class Order {
    private Long id;
    private Customer customer;
    private OrderStatus status;
    private BigDecimal totalPrice;
    private BigDecimal totalPayment;
    private List orderItems;

    // 其他属性
} 

public class OrderItem {
    private Long id;
    private Product product;
    private BigDecimal amount;
    private BigDecimal subTotal;
    private OrderStatus status;

    // 其他属性
}
 
@Repository
public class OrderRepositoryImpl implements OrderRepository {
    @Autowire
    private OrderMapper orderMapper;
    
    @Autowire
    private OrderItemMapper orderItemMapper;
    
    @Override
    public Order find(long id) {
        return new Order(orderMapper.select(/**/), orderItemMapper.select(/**/));
    }
}

上面代码,Order中包含一个List,在OrderRepository.find中进行两次数据库查询完成Order聚合根组装。如果OrderItem数量较少这没什么问题,但对于数据量较大的场景显然不能将OrderItem一次性查出全部放入内存。这就引出了问题三:“对于1-N关系,如何处理N过大的问题”。

一种变通的方法是Order不存储List orderItems,只存储OrderItems的变更,这时候充血模型变成了失血模型。

public class Order {
    private Long id;
    private Customer customer;
    private OrderStatus status;
    private BigDecimal totalPrice;
    private BigDecimal totalPayment;

    private List changeOrderItems;

    /**
     * 直接通过SQL查询数据
     */
    public List getOrders(/*查询条件*/) {
        // select * from order_item where ...

        return orderItems;
    }

    public void addOrderItem(OrderItem orderItem) {
        // 新增
        orderItem.setChangeState(1); 
        
        changeOrderItems.add(orderItem);
    }

    public void updateOrderItem(OrderItem orderItem) {
        // 更新
        orderItem.setChangeState(2); 
        
        changeOrderItems.add(orderItem);
    }

    public void removeOrderItem(OrderItem orderItem) {
        // 删除
        orderItem.setChangeState(3); 
        
        changeOrderItems.add(orderItem);
    }
}

@Repository
public class OrderRepositoryImpl implements OrderRepository {
    @Autowire
    private OrderMapper orderMapper;
    
    @Autowire
    private OrderItemMapper orderItemMapper;
    
    @Override
    public void save(Order order) {
        // ...

        // 处理OrderItem变更
        for(OrderItem item : order.getChangeStatus()) {
            switch(item.getChangeStatus()) {
                case 1:
                    orderItemMapper.insert(item);
                    break;
                case 2:
                    orderItemMapper.update(item);
                    break;
                case 3:
                    orderItemMapper.delete(item.getId());
                    break;
                default:
                    break;
                }
            }
        }
    }
}

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

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

除了这些问题外,应用DDD也还有其他问题: