DDD之如何合理设计一个聚合

聚合

聚合就是将那些一致性边界内的实体和值对象组合到一起。不同的人设计的聚合可大可小。一方面我们可能为了对象组合方便而将聚合设计的过大,另外一方面,我们设计的聚合又可能因为过于贫瘠而失去了保护真正不变条件的目的。

设计原则

不合理的设计聚合将会导致并发控制,性能,事务一致性等诸多问题。
看下面通用语言:

  • 产品拥有待定项,发布和冲刺
  • 为新的待定项制定计划
  • 为产品发布制定进度表
  • 为产品冲刺制定进度表
  • 一个计划好的待定项可以安排在发布中
  • 位于发布中的待定项可以提交到冲刺中

上述通用语言暗含了如下的一致性原则,所以其可以被设计成聚合。

  • 如果一个待定项提交到了冲刺中,那么我们不能将该待定项从系统中移除
  • 如果一个冲刺含有待定项,我们不能将该冲刺从系统中移除
  • 如果一个发布含有待定项,那么我们不能将发布从系统中移除。
  • 如果一个待定项位于发布中,那么我们不能将该待定项从系统中移除。
    此时,Product被建模成如下聚合:
    DDD之如何合理设计一个聚合_第1张图片
public class Product ...{
	....
	public void planBacklogItem(...){
		
	}
	public void scheduleRelease(...){
	
	}
	public void scheduleSprint(...){
	
	}
}

此时很容易存在并发控制的问题,如果多个用户同时修改一个product实例,很容易导致失败。

原则一:设计小的聚合

在实践中,绝大多数聚合可以设计成包含值类型的属性的根实体,小部分的可以设计成包含2-3个实体的聚合。上述的列子中我们通过这种方式改造大聚合。
DDD之如何合理设计一个聚合_第2张图片
方法签名变更,product承担着工厂的作用

public class Product ...{
	....
	public BacklogItem planBacklogItem(...){
		
	}
	public Release scheduleRelease(...){
	
	}
	public Sprint scheduleSprint(...){
	
	}
}

现在客户端计划一个待定项时,应用层变成

public class ProductBacklongItemService ...{
	@Autowired
	private ProductService productService;
	@Autowired
	private BacklogItemService backlogItemService;
	
	@Transactional
	public void planProductBacklogItem(...){
		Product product = productService.findProductById(new ProductId(id));
		BacklogItem backlogItem = product.planBacklogItem(...);
		backlogItemService.add(backlogItem);
	}
}

原则二:在一致性边界内建模真正不变条件

事务一致性要求立即性和原子性,并且最终一致性。我们尽量采用单事务来管理一致性,一个事务中只修改一个聚合实例。所以我们设计对象时,需要重点来关注需要保证一致性的哪些属性。

原则三:通过标识引用而不是对象引用其他聚合

对象引用方式

public class BacklogItem ...{
	....
	private Product product;
}

如果采用在聚合根BacklogItem中引用Product的方式, 很容易造成事物一致性的问题。此时事物的边界存在问题。我们在构建BacklogItem对象时,需要保证内在属性都是存在相同的事务边界。所以我们可以换成下面这种方式,持有对ProductId的引用。

public class BacklogItem ...{
	....
	private ProductId productId;
}

原则四:在边界之外使用最终一致性

在边界之外我们可以采用领域事件的方式来保证最终的一致性,具体实现可以参考我的这篇博客《Spring Data实现领域事件》,注意的是我们需要处理异常情况,因为这种方式产生的是两个事物。所以我们需要保证失败的重试或者提示。

上一篇:《DDD之实体与值对象区别》
下一篇:《DDD之领域服务与领域事件》

你可能感兴趣的:(编程架构,DDD,聚合,聚合根)