领域设计:聚合与聚合根

本文试图回答如下问题:

  • 什么是聚合?
  • 什么是聚合根?
  • 如何确定聚合和聚合根?
  • Respository与DAO的区别

设计的表现力

《程序员必读之软件架构》一书在「软件架构和编码」一章有这么一段话:

尽管很多人以组件来谈论他们的软件系统,然而代码通常并未反映出这种结构。这就是软件架构和依据原则编码之间会脱节的原因之一:墙上的架构图说的是一回事,代码说的却是另一回事。

个人认为这是架构与代码差异的一个原因。还有一个原因就是某些约束没有在设计中体现出来,也就是说设计的表现力不够,而这些约束需要阅读代码才能够知道,这就增加了理解和使用这个组件的难度。这个问题在基于数据建模的设计方法上比较明显。

领域设计:Entity与VO提到的淘宝购物为例,以数据驱动的方式来设计,我们会有如下两张表:

CREATE TABLE `order` (
 `rec_id` BIGINT(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
 `seller_id` BIGINT(11) NOT NULL COMMENT '卖家',
 `buyer_id` BIGINT(11) NOT NULL COMMENT '买家',
 `price` BIGINT(11) NOT NULL COMMENT '订单总价格,按分计算',
 ...
 PRIMARY KEY (`rec_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ;

CREATE TABLE `order_detail` (
 `rec_id` BIGINT(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
 `order_id` BIGINT(11) NOT NULL COMMENT '订单主键',
 `product_name` VARCHAR(50) COMMENT '产品名称',
 `product_desc` VARCHAR(200) COMMENT '产品描述',
 `product_price` BIGINT(11) NOT NULL COMMENT '产品价格,按分计算',
 ...
 PRIMARY KEY (`rec_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ;

从表关系上,我们只能知道order与order_detail是一对多的关系。我们再看下面这两张表:

CREATE TABLE `product` (
 `rec_id` BIGINT(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
 `name` VARCHAR(50) COMMENT '产品名称',
 `desc` VARCHAR(200) COMMENT '产品描述',
 ...
 PRIMARY KEY (`rec_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ;

CREATE TABLE `product_comment` (
 `rec_id` BIGINT(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
 `product_id` BIGINT(11) NOT NULL COMMENT '产品',
 `cont` VARCHAR(2000) COMMENT '评价内容',
 ...
 PRIMARY KEY (`rec_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ;

从表关系上,我们也只能知道product与product_comment之间是一对多的关系。

那么,请问:order与order_detail之间的关系与product与product_comment之间的关系是一样的吗?至少从上面的表设计上,完全看不出来!

我们需要深入到代码,才能够发现差异:

@Service
@Transactional
public class OrderService {
 public void createOrder(Order order,List<OrderDetail> orderDetailList) throws Exception {
 // 保存订单
 // 保存订单详情
 }
 }
}

@Service
@Transactional
public class ProductService {
 public void createProduct(Product prod) throws Exception {
 // 保存产品
 }
 }
}
  • 订单和订单明细是一起保存的,也就是说两者可以作为一个整体来看待(这个整体就是我们下面要说的聚合)
  • 而产品和产品评论之间并不能被看做一个整体,所以没有在一起进行操作

这层逻辑,你光看上面的设计是看不出来的,只有看到代码了,才能理清这一层关系。这无形中就增加了理解和使用难度。「聚合」就是缓解这种问题的一种手段!

什么是聚合和聚合根?

在讨论聚合之前,我们先来看一段Java代码:

public class People {
 public void say() {
 System.out.println("1");
 System.out.println("2");
 }
}

对于上面的代码,如何保障在多线程情况下1和2能按顺序打印出来?最简单的方法就是使用synchronized关键字进行加锁操作,像这样:

public class People {
 public synchronized void say() {
 System.out.println("1");
 System.out.println("2");
 }
}

synchronized保证了代码的原子性执行。与之类似的就是事务,在JDBC的架构设计中已经聊过了事务,这里不再赘述。事务保证了原子性操作。

但是,这和「聚合」有什么关系呢?

如果说,synchronized是多线程层面的锁;事务是数据库层面的锁,那么「聚合」就是业务层面的锁!

在业务逻辑上,有些对象需要保持操作上的原子性,否则就没有任何意义。这些对象就组成了「聚合」!

对于上面的订单与订单详情,从业务上来看,订单与订单明细需要保持业务上的原子性操作:

  • 订单必须要包含订单明细
  • 订单明细必须要属于某个订单
  • 订单和订单明细被视为一个整体,少了任何一个都没有意义

所以其对象模型可以表示为:

领域设计:聚合与聚合根

 

  • 订单和订单明细组成一个「聚合」
  • 订单是操作的主体,所以订单是这个「聚合」的「聚合根」
  • 所有对这个「聚合」的操作,只能通过「聚合根」进行

相应的,产品和产品评价就不构成「聚合」。虽然在表设计时,订单和订单明细的结构关系与产品与产品评价的结构关系是一样的!因为:

  • 虽然产品评价需要属于某个产品
  • 但是产品不一定就有产品评价
  • 产品评价可以独立操作

所以产品与产品评论的模型则可以表示为:

领域设计:聚合与聚合根

 

  • 产品和产品评论是两个「聚合」
  • 产品评论通过productId与「产品聚合」进行关联

如何确定聚合和聚合根?

对象在业务逻辑上是否需要保证原子性操作是确定聚合和聚合根的其中一个约束。还有一个约束就是「边界」,即聚合多大才合适?过大的「聚合」会带来各种问题。

还以锁举例,看下面的代码:

public class People {
 public synchronized void say() {
 System.out.println("0");
 System.out.println("1");
 System.out.println("2");
 System.out.println("4");
 }
}

如果我只希望12能按顺序打印出来,而0和4没有这个要求!上面的代码能满足要求,但是影响了性能。优化方式是使用同步块,缩小同步范围:

public class People {
 public void say() {
 System.out.println("0");
 synchronized(Locker.class){
 System.out.println("1");
 System.out.println("2");
 }
 System.out.println("4");
 }
}

「边界」就像上面的同步块一样,只将需要的对象组合成聚合!

假设上面的产品和产品评论构成了一个聚合!那会发生什么事情呢?当A,B两个用户同时对这个商品进行评论,A先开始评论,此时就会锁定该产品对象以及下面的所有评论,在A提交评论之前,B是无法操作这个产品对象的,显然这是不合理的。

Respository与DAO的区别

在理解了聚合之后,就可以很容易的区分Respository与DAO了:

  • DAO是技术手段,Respository是抽象方式
  • DAO只是针对对象的操作,而Respository是针对「聚合」的操作

DAO的操作方式如下:

@Service
@Transactional
public class OrderService {
 public void createOrder(Order order,List<OrderDetail> orderDetailList) throws Exception {
 Long orderId = orderDao.save(order);
 for(OrderDetail detail : orderDetailList) {
 detail.setOrderId(orderId);
 orderDetailDao.save(detail);
 }
 }
 }
}
  • 订单和和订单明细都有一个对应的DAO
  • 订单和订单明细的关系并没有在对象之间得到体现

而Respository的操作方式如下:

// 订单和订单明细构成聚合
Order{
 List<OrderDetail> itemLine; // 这里就保证了设计与编码的一致性
 ...
}
@Service
@Transactional
public class OrderService {
 public void createOrder(Order order) throws Exception {
 orderRespository.save(order);
 //or
 order.save(); // 内部调用orderRespository.save(this);
 }
}

当然,orderRespository的save方法中,可能还是数据库相关操作,但也可能是NoSql操作甚至内存操作。

参考资料

  • 《领域驱动设计:软件核心复杂性应对之道》
  • 《实现领域驱动设计》

posted @ 2020-12-06 18:35  一瑜一琂  阅读(1880)  评论(3编辑  收藏  举报