这里,聚合会实现Aggregate接口,而实体会实现Entity接口 。聚合本质上是一种特殊的实体,这种结构使逻辑更加清晰 。另外,我们引入了Identifier接口来表示实体的唯一标识符,它将唯一标识符视为值对象,这是DDD中常见的做法 。如下面所示的案例
public class OrderId implements Identifier<Long> {@Serialprivate static final long serialVersionUID = -8658575067669691021L;public Long id;public OrderId(Long id){this.id = id;}@Overridepublic Long getValue() {return id;}}4.2 创建通用Repository接口【DDD实战 - Repository模式的妙用】接下来,我们定义一个基础的Repository接口 。
public interface Repository <T extends Aggregate<ID>, ID extends Identifier<?>> {T find(ID id);void remove(T aggregate);void save(T aggregate);}业务特定的接口可以在此基础上进行扩展 。例如,对于订单,我们可以添加计数和分页查询 。
public interface OrderRepository extends Repository<Order, OrderId> { // 自定义Count接口,在这里OrderQuery是一个自定义的DTOLong count(OrderQuery query);// 自定义分页查询接口Page<Order> query(OrderQuery query);}请注意,Repository的接口定义位于Domain层,而具体的实现则位于Infrastructure层 。
4.3 实施Repository的基本功能下面是一个简单的Repository实现示例 。注意,OrderRepositoryNativeImpl在Infrastructure层 。
@Repository@RequiredArgsConstructor(onConstructor = @__(@Autowired))@Slf4jpublic class OrderRepositoryNativeImpl implements OrderRepository {private final OrderMapper orderMapper;private final OrderItemMapper orderItemMapper;private final OrderConverter orderConverter;private final OrderItemConverter orderItemConverter;@Overridepublic Order find(OrderId orderId) {OrderDO orderDO =orderMapper.selectById(orderId.getValue());return orderConverter.fromData(orderDO);}@Overridepublic void save(Order aggregate) {if(aggregate.getId() != null && aggregate.getId().getValue() > 0){// updateOrderDO orderDO = orderConverter.toData(aggregate);orderMapper.updateById(orderDO);}else{// insertOrderDO orderDO = orderConverter.toData(aggregate);orderMapper.insert(orderDO);aggregate.setId(orderConverter.fromData(orderDO).getId());}} ...}这段代码展示了一个常见的模式:Entity/Aggregate转换为Data Object(DO),然后使用Data Access Object(DAO)根据业务逻辑执行相应操作 。在操作完成后,如果需要,还可以将DO转换回Entity 。代码很简单,唯一需要注意的是save方法,需要根据Aggregate的ID是否存在且大于0来判断一个Aggregate是否需要更新还是插入 。
4.4 Repository复杂实现处理单一实体的Repository实现通常较为直接,但当聚合中包含多个实体时,操作的复杂性会增加 。主要的问题在于,在单次操作中,并不是聚合中的所有实体都需要变更,而使用简单的实现会导致许多不必要的数据库操作 。
以一个典型的场景为例:一个订单中包含多个商品明细 。如果修改了某个商品明细的数量,这会同时影响主订单的总价,但对其他商品明细则没有影响 。

文章插图
图片
若采用基础的实现方法,会多出两个不必要的更新操作,如下所示:
@Repository@RequiredArgsConstructor(onConstructor = @__(@Autowired))@Slf4jpublic class OrderRepositoryNativeImpl implements OrderRepository { //省略其他逻辑@Overridepublic void save(Order aggregate) {if(aggregate.getId() != null && aggregate.getId().getValue() > 0){// 每次都将Order和所有LineItem全量更新OrderDO orderDO = orderConverter.toData(aggregate);orderMapper.updateById(orderDO);for(OrderItem orderItem : aggregate.getOrderItems()){save(orderItem);}}else{//省略插入逻辑}}private void save(OrderItem orderItem) {if (orderItem.getId() != null && orderItem.getId().getValue() > 0) {OrderItemDO orderItemDO = orderItemConverter.toData(orderItem);orderItemMapper.updateById(orderItemDO);} else {OrderItemDO orderItemDO = orderItemConverter.toData(orderItem);orderItemMapper.insert(orderItemDO);orderItem.setItemId(orderItemConverter.fromData(orderItemDO).getId());}}}在此示例中,会执行4个UPDATE操作,而实际上只需2个 。通常情况下,这个额外的开销并不严重,但如果非Aggregate Root的实体数量很大,这会导致大量不必要的写操作 。4.5 变更追踪(Change-Tracking)针对上述问题,核心在于Repository接口的限制使得调用者只能操作Aggregate Root,而不能单独操作非Aggregate Root的实体 。这与直接调用DAO的方式有显著差异 。
一种解决方案是通过变更追踪能力来识别哪些实体有变更,并且仅对这些变更过的实体执行操作 。这样,先前需要手动判断的代码逻辑现在可以通过变更追踪来自动实现,让开发者真正只关注聚合的操作 。以前面的示例为例,通过变更追踪,系统可以判断出只有OrderItem2和Order发生了变化,因此只需要生成两个UPDATE操作 。
推荐阅读
- 为什么从 MVC 到 DDD,架构的本质是什么?
- 赛尔号帕罗迪亚实战 赛尔号帕罗迪亚
- 云上VPC网络规划实战
- DDD 中关于应用架构的那些事
- pc端格斗类|男漫游VS男大枪,谁更适合你?解析职业优缺点,揭秘实战表现!
- Spring/SpringBoot中的声明式事务和编程式事务源码、区别、优缺点、适用场景、实战
- 鲤鱼|水面很大鲤鱼怎么钓?多年实战得出两个绝招,新手看懂也能连竿
- 京东快递H5项目接入vite实战
- 宠儿|塑造自我形象,让你成为职场上的宠儿:形象打造的实战技巧
- 做刀用什么钢最好(实战刀剑用什么钢好)
