通常来说,DDD中的写操作并不需要向客户端返回数据,在某些情况下(比如新建聚合根)可以返回一个聚合根的ID,这意味着ApplicationService或者聚合根中的写操作方法通常返回void即可。比如,对于OrderApplicationService,各个方法签名如下:
public OrderId createOrder(CreateOrderCommand command) ; public void changeProductCount(String id, ChangeProductCountCommand command) ; public void pay(String id, PayOrderCommand command) ; public void changeAddressDetail(String id, String detail) ;可以看到,在多数情况下我们使用了后缀为Command的对象传给ApplicationService,比如CreateOrderCommand和ChangeProductCountCommand。Command即命令的意思,也即写操作表示的是外部向领域模型发起的一次命令操作。事实上,从技术上讲,Command对象只是一种类型的DTO对象,它封装了客户端发过来的请求数据。在Controller中所接收的所有写操作都需要通过Command进行包装,在Command比较简单(比如只有1-2个字段)的情况下Controller可以将Command解开之后,将其中的数据直接传递给ApplicationService,比如changeAddressDetail()便是如此;而在Command中数据字段比较多时,可以直接将Command对象传递给ApplicationService。当然,这并不是DDD中需要严格遵循的一个原则,比如无论Command的简繁程度,统一将所有Command从Controller传递给ApplicationService,也不存在太大的问题,更多的只是一个编码习惯上的选择。不过有一点需要强调,即前文提到的“ApplicationService需要接受原始数据类型而不是领域模型中的对象”,在这里意味着Command对象中也应该包含原始的数据类型。
统一使用Command对象还有个好处是,我们通过查找所有后缀为Command的对象,便可以概览性地了解软件系统向外提供的业务功能。
阶段性小结一下,以上我们主要围绕着软件的“写操作”在DDD中的实现进行讨论,并且讲到了3种场景,分别是:
通过聚合根完成业务请求
通过Factory完成聚合根的创建
通过DomainService完成业务请求
以上3种场景大致上涵盖了DDD完成业务写操作的基本方面,总结下来3句话:创建聚合根通过Factory完成;业务逻辑优先在聚合根边界内完成;聚合根中不合适放置的业务逻辑才考虑放到DomainService中。
DDD中的读操作软件中的读模型和写模型是很不一样的,我们通常所讲的业务逻辑更多的时候是在写操作过程中需要关注的东西,而读操作更多关注的是如何向客户方返回恰当的数据展现。
在DDD的写操作中,我们需要严格地按照“应用服务 -> 聚合根 -> 资源库”的结构进行编码,而在读操作中,采用与写操作相同的结构有时不但得不到好处,反而使整个过程变得冗繁。这里介绍3种读操作的方式:
基于领域模型的读操作
基于数据模型的读操作
CQRS
首先,无论哪种读操作方式,都需要遵循一个原则:领域模型中的对象不能直接返回给客户端,因为这样领域模型的内部便暴露给了外界,而对领域模型的修改将直接影响到客户端。因此,在DDD中我们通常为读操作专门创建相应的模型用于数据展现。在写操作中,我们通过Command后缀进行请求数据的统一,在读操作中,我们通过Representation后缀进行展现数据的统一,这里的Representation也即REST中的“R”。
基于领域模型的读操作这种方式将读模型和写模型糅合到一起,先通过资源库获取到领域模型,然后将其转换为Representation对象,这也是当前被大量使用的方式,比如对于“获取Order详情的接口”,OrderApplicationService实现如下:
@Transactional(readOnly = true) public OrderRepresentation byId(String id) { Order order = orderRepository.byId(orderId(id)); return orderRepresentationService.toRepresentation(order); }我们先通过orderRepository.byId()获取到Order聚合根对象,然后调用orderRepresentationService.toRepresentation()将Order转换为展现对象OrderRepresentation,OrderRepresentationService.toRepresentation()实现如下:
public OrderRepresentation toRepresentation(Order order) { List<OrderItemRepresentation> itemRepresentations = order.getItems().stream() .map(orderItem -> new OrderItemRepresentation(orderItem.getProductId().toString(), orderItem.getCount(), orderItem.getItemPrice())) .collect(Collectors.toList()); return new OrderRepresentation(order.getId().toString(), itemRepresentations, order.getTotalPrice(), order.getStatus(), order.getCreatedAt()); }这种方式的优点是非常直接明了,也不用创建新的数据读取机制,直接使用Repository读取数据即可。然而缺点也很明显:一是读操作完全束缚于聚合根的边界划分,比如,如果客户端需要同时获取Order及其所包含的Product,那么我们需要同时将Order聚合根和Product聚合根加载到内存再做转换操作,这种方式既繁琐又低效;二是在读操作中,通常需要基于不同的查询条件返回数据,比如通过Order的日期进行查询或者通过Product的名称进行查询等,这样导致的结果是Repository上处理了太多的查询逻辑,变得越来越复杂,也逐渐偏离了Repository本应该承担的职责。
基于数据模型的读操作