这种方式绕开了资源库和聚合,直接从数据库中读取客户端所需要的数据,此时写操作和读操作共享的只是数据库。比如,对于“获取Product列表”接口,通过一个专门的ProductRepresentationService直接从数据库中读取数据:
@Transactional(readOnly = true) public PagedResource<ProductSummaryRepresentation> listProducts(int pageIndex, int pageSize) { MapSqlParameterSource parameters = new MapSqlParameterSource(); parameters.addValue("limit", pageSize); parameters.addValue("offset", (pageIndex - 1) * pageSize); List<ProductSummaryRepresentation> products = jdbcTemplate.query(SELECT_SQL, parameters, (rs, rowNum) -> new ProductSummaryRepresentation(rs.getString("ID"), rs.getString("NAME"), rs.getBigDecimal("PRICE"))); int total = jdbcTemplate.queryForObject(COUNT_SQL, newHashMap(), Integer.class); return PagedResource.of(total, pageIndex, products); }然后在Controller中直接返回:
@GetMapping public PagedResource<ProductSummaryRepresentation> pagedProducts(@RequestParam(required = false, defaultValue = "1") int pageIndex, @RequestParam(required = false, defaultValue = "10") int pageSize) { return productRepresentationService.listProducts(pageIndex, pageSize); }可以看到,真个过程并没有使用到ProductRepository和Product,而是将SQL获取到的数据直接新建为ProductSummaryRepresentation对象。
这种方式的优点是读操作的过程不用囿于领域模型,而是基于读操作本身的需求直接获取需要的数据即可,一方面简化了整个流程,另一方面大大提升了性能。但是,由于读操作和写操作共享了数据库,而此时的数据库主要是对应于聚合根的结构创建的,因此读操作依然会受到写操作的数据模型的牵制。不过这种方式是一种很好的折中,微软也提倡过这种方式,更多细节请参考微软官网。
CQRSCQRS(Command Query Responsibility Segregation),即命令查询职责分离,这里的命令可以理解为写操作,而查询可以理解为读操作。与“基于数据模型的读操作”不同的是,在CQRS中写操作和读操作使用了不同的数据库,数据从写模型数据库同步到读模型数据库,通常通过领域事件的形式同步变更信息。
这样一来,读操作便可以根据自身所需独立设计数据结构,而不用受写模型数据结构的牵制。CQRS本身是一个很大的话题,已经超出了本文的范围,读者可以自行研究。
到此,DDD中的读操作可以大致分为3种实现方式:
总结本文主要介绍了DDD中的应用服务、聚合、资源库和工厂等概念以及与它们相关的编码实践,然后着重讲到了软件的读写操作在DDD中的实现方式,其中写操作的3种场景为:
通过聚合根完成业务请求,这是DDD完成业务请求的典型方式
通过Factory完成聚合根的创建,用于创建聚合根
通过DomainService完成业务请求,当业务放在聚合根中不合适时才考虑放在DomainService中
对于读操作,同样给出了3种方式:
基于领域模型的读操作(读写操作糅合在了一起,不推荐)
基于数据模型的读操作(绕过聚合根和资源库,直接返回数据,推荐)
CQRS(读写操作分别使用不同的数据库)
以上“3读3写”基本上涵盖了程序员完成业务功能的日常开发之所需,原来DDD就这么简单,不是吗?