向读者们道歉,由于工作太忙,又对文章质量有追求,所以这篇文章产出速度较慢,但可以向大家保证:文章中的内容都经过了反复实践和踩坑。DDD系列的前几篇文章可以点击文字下方阅读~
DDD系列第二讲
DDD系列第三讲
DDD系列第四讲
在过去一年里我们团队做了大量的老系统重构和迁移,其中有大量的代码属于流水账代码,通常能看到是开发在对外的API接口里直接写业务逻辑代码,或者在一个服务里大量的堆接口,导致业务逻辑实际无法收敛,接口复用性比较差。所以这讲主要想系统性的解释一下如何通过DDD的重构,将原有的流水账代码改造为逻辑清晰、职责分明的模块。
案例简介

@RestController
@RequestMapping("/")
public class CheckoutController {
@Resource
private ItemService itemService;
@Resource
private InventoryService inventoryService;
@Resource
private OrderRepository orderRepository;
@PostMapping("checkout")
public Result<OrderDO> checkout(Long itemId, Integer quantity) {
// 1) Session管理
Long userId = SessionUtils.getLoggedInUserId();
if (userId <= 0) {
return Result.fail("Not Logged In");
}
// 2)参数校验
if (itemId <= 0 || quantity <= 0 || quantity >= 1000) {
return Result.fail("Invalid Args");
}
// 3)外部数据补全
ItemDO item = itemService.getItem(itemId);
if (item == null) {
return Result.fail("Item Not Found");
}
// 4)调用外部服务
boolean withholdSuccess = inventoryService.withhold(itemId, quantity);
if (!withholdSuccess) {
return Result.fail("Inventory not enough");
}
// 5)领域计算
Long cost = item.getPriceInCents() * quantity;
// 6)领域对象操作
OrderDO order = new OrderDO();
order.setItemId(itemId);
order.setBuyerId(userId);
order.setSellerId(item.getSellerId());
order.setCount(quantity);
order.setTotalCost(cost);
// 7)数据持久化
orderRepository.createOrder(order);
// 8)返回
return Result.success(order);
}
}
-
分离出独立的Interface接口层,负责处理网络协议相关的逻辑 -
从真实业务场景中,找出具体用例(UseCases),然后将具体用例通过专用的Command指令、Query查询、和Event事件对象来承接 -
分离出独立的Application应用层,负责业务流程的编排,响应Command、Query和Event。每个应用层的方法应该代表整个业务流程中的一个节点 -
处理一些跨层的横切关注点,如鉴权、异常处理、校验、缓存、日志等
Interface接口层
-
HTTP 框架:如Spring MVC框架,Spring Cloud等 -
RPC 框架:如Dubbo、HSF、gRPC等 -
消息队列MQ的“消费者”:比如JMS的 onMessage,RocketMQ的MessageListener等 -
Socket通信:Socket通信的receive、WebSocket的onMessage等 -
文件系统:WatcherService等 -
分布式任务调度:SchedulerX等
-
网络协议的转化:通常这个已经由各种框架给封装掉了,我们需要构建的类要么是被注解的bean,要么是继承了某个接口的bean。 -
统一鉴权:比如在一些需要AppKey+Secret的场景,需要针对某个租户做鉴权的,包括一些加密串的校验 -
Session管理:一般在面向用户的接口或者有登陆态的,通过Session或者RPC上下文可以拿到当前调用的用户,以便传递给下游服务。 -
限流配置:对接口做限流避免大流量打到下游服务 -
前置缓存:针对变更不是很频繁的只读场景,可以前置结果缓存到接口层 -
异常处理:通常在接口层要避免将异常直接暴露给调用端,所以需要在接口层做统一的异常捕获,转化为调用端可以理解的数据格式 -
日志:在接口层打调用日志,用来做统计和debug等。一般微服务框架可能都直接包含了这些功能。
▐返回值和异常处理规范,ResultvsException
规范:Interface层的HTTP和RPC接口,返回值为Result,捕捉所有异常 规范:Application层的所有接口返回值为DTO,不负责处理异常
Application层的具体规范等下再讲,在这里先展示Interface层的逻辑。
@PostMapping("checkout")
public Result<OrderDTO> checkout(Long itemId, Integer quantity) {
try {
CheckoutCommand cmd = new CheckoutCommand();
OrderDTO orderDTO = checkoutService.checkout(cmd);
return Result.success(orderDTO);
} catch (ConstraintViolationException cve) {
// 捕捉一些特殊异常,比如Validation异常
return Result.fail(cve.getMessage());
} catch (Exception e) {
// 兜底异常捕获
return Result.fail(e.getMessage());
}
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ResultHandler {
}
@Aspect
@Component
public class ResultAspect {
@Around("@annotation(ResultHandler)")
public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
Object proceed = null;
try {
proceed = joinPoint.proceed();
} catch (ConstraintViolationException cve) {
return Result.fail(cve.getMessage());
} catch (Exception e) {
return Result.fail(e.getMessage());
}
return proceed;
}
}
@PostMapping("checkout")
@ResultHandler
public Result<OrderDTO> checkout(Long itemId, Integer quantity) {
CheckoutCommand cmd = new CheckoutCommand();
OrderDTO orderDTO = checkoutService.checkout(cmd);
return Result.success(orderDTO);
}
▐接口层的接口的数量和业务间的隔离
//可以是RPCProvider或者Controller
public interface CardService {
// 1)统一接口,参数膨胀
Result openCard(int petType, int babyAge);
// 2)统一泛化接口,参数语意丢失
Result openCardV2(Map<String, Object> params);
// 3)不泛化,同一个类里的接口膨胀
Result openPetCard(int petType);
Result openBabyCard(int babyAge);
}
规范:一个Interface层的类应该是“小而美”的,应该是面向“一个单一的业务”或“一类同样需求的业务”,需要尽量避免用同一个类承接不同类型业务的需求。
publicinterfacePetCardService{
Result openPetCard(int petType);
}
public interface BabyCardService {
Result openBabyCard(int babyAge);
}

Application层
▐Application层的组成部分
-
ApplicationService应用服务:最核心的类,负责业务流程的编排,但本身不负责任何业务逻辑 -
DTO Assembler:负责将内部领域模型转化为可对外的DTO -
Command、Query、Event对象:作为ApplicationService的入参 -
返回的DTO:作为ApplicationService的出参
▐Command、Query、Event对象
-
Command指令:指调用方明确想让系统操作的指令,其预期是对一个系统有影响,也就是写操作。通常来讲指令需要有一个明确的返回值(如同步的操作结果,或异步的指令已经被接受)。 -
Query查询:指调用方明确想查询的东西,包括查询参数、过滤、分页等条件,其预期是对一个系统的数据完全不影响的,也就是只读操作。 -
Event事件:指一件已经发生过的既有事实,需要系统根据这个事实作出改变或者响应的,通常事件处理都会有一定的写操作。事件处理器不会有返回值。这里需要注意一下的是,Application层的Event概念和Domain层的DomainEvent是类似的概念,但不一定是同一回事,这里的Event更多是外部一种通知机制而已。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
-
为什么要用CQE对象?
Result<OrderDO>checkout(LongitemId,Integerquantity);
Result<OrderDO>checkout(LongitemId,Integerquantity);
Result<OrderDO> checkout(Long itemId, Integer quantity, Integer channel);
List<OrderDO> queryByItemId(Long itemId);
List<OrderDO> queryBySellerId(Long sellerId);
List<OrderDO> queryBySellerIdWithPage(Long sellerId, int currentPage, int pageSize);
-
接口膨胀:一个查询条件一个方法 -
难以扩展:每新增一个参数都有可能需要调用方升级 -
难以测试:接口一多,职责随之变得繁杂,业务场景各异,测试用例难以维护
-
CQE的规范
publicinterfaceCheckoutService{
OrderDTO checkout(@Valid CheckoutCommand cmd);
List<OrderDTO> query(OrderQuery query);
OrderDTO getOrder(Long orderId); // 注意单一ID查询可以不用Query
}
@Data
public class CheckoutCommand {
private Long userId;
private Long itemId;
private Integer quantity;
}
@Data
public class OrderQuery {
private Long sellerId;
private Long itemId;
private int currentPage;
private int pageSize;
}
-
CQEvsDTO
-
CQE:CQE对象是ApplicationService的输入,是有明确的”意图“的,所以这个对象必须保证其”正确性“。 -
DTO:DTO对象只是数据容器,只是为了和外部交互,所以本身不包含任何逻辑,只是贫血对象。
-
CQE的校验
if (itemId <= 0 || quantity <= 0 || quantity >= 1000) {
return Result.fail("Invalid Args");
}
规范:CQE对象的校验应该前置,避免在ApplicationService里做参数的校验。可以通过JSR303/380和Spring Validation来实现
@Validated//Spring的注解
public class CheckoutServiceImpl implements CheckoutService {
OrderDTO checkout(@Valid CheckoutCommand cmd) { // 这里@Valid是JSR-303/380的注解
// 如果校验失败会抛异常,在interface层被捕捉
}
}
@Data
public class CheckoutCommand {
@NotNull(message = "用户未登陆")
private Long userId;
@NotNull
@Positive(message = "需要是合法的itemId")
private Long itemId;
@NotNull
@Min(value = 1, message = "最少1件")
@Max(value = 1000, message = "最多不能超过1000件")
private Integer quantity;
}
-
避免复用CQE
-
规范:针对于不同语意的指令,要避免CQE对象的复用
publicinterfaceCheckoutService{
OrderDTO checkout(@Valid CheckoutCommand cmd);
OrderDTO updateOrder(@Valid UpdateOrderCommand cmd);
}
@Data
public class UpdateOrderCommand {
@NotNull(message = "用户未登陆")
private Long userId;
@NotNull(message = "必须要有OrderID")
private Long orderId;
@NotNull
@Positive(message = "需要是合法的itemId")
private Long itemId;
@NotNull
@Min(value = 1, message = "最少1件")
@Max(value = 1000, message = "最多不能超过1000件")
private Integer quantity;
}
▐ApplicationService

publicinterfaceCheckoutService{
// 下单
OrderDTO checkout(@Valid CheckoutCommand cmd);
// 支付成功
OrderDTO payReceived(@Valid PaymentReceivedEvent event);
// 支付取消
OrderDTO payCanceled(@Valid PaymentCanceledEvent event);
// 发货
OrderDTO packageSent(@Valid PackageSentEvent event);
// 收货
OrderDTO delivered(@Valid DeliveredEvent event);
// 批量查询
List<OrderDTO> query(OrderQuery query);
// 单个查询
OrderDTO getOrder(Long orderId);
}
@Component
public class CheckoutCommandHandler implements CommandHandler<CheckoutCommand, OrderDTO> {
@Override
public OrderDTO handle(CheckoutCommand cmd) {
//
}
}
public class CheckoutServiceImpl implements CheckoutService {
@Resource
private CheckoutCommandHandler checkoutCommandHandler;
@Override
public OrderDTO checkout(@Valid CheckoutCommand cmd) {
return checkoutCommandHandler.handle(cmd);
}
}
// Application层
// 在这里框架通常可以根据接口识别到这个负责处理PaymentReceivedEvent
// 也可以通过增加注解识别
@Component
public class PaymentReceivedHandler implements EventHandler<PaymentReceivedEvent> {
@Override
public void process(PaymentReceivedEvent event) {
//
}
}
// Interface层,这个是RocketMQ的Listener
public class OrderMessageListener implements MessageListenerOrderly {
@Resource
private EventBus eventBus;
@Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
PaymentReceivedEvent event = new PaymentReceivedEvent();
eventBus.dispatch(event); // 不需要指定消费者
return ConsumeOrderlyStatus.SUCCESS;
}
}
-
ApplicationService是业务流程的封装,不处理业务逻辑
@Service
@Validated
public class CheckoutServiceImpl implements CheckoutService {
private final OrderDtoAssembler orderDtoAssembler = OrderDtoAssembler.INSTANCE;
@Resource
private ItemService itemService;
@Resource
private InventoryService inventoryService;
@Resource
private OrderRepository orderRepository;
@Override
public OrderDTO checkout(@Valid CheckoutCommand cmd) {
ItemDO item = itemService.getItem(cmd.getItemId());
if (item == null) {
throw new IllegalArgumentException("Item not found");
}
boolean withholdSuccess = inventoryService.withhold(cmd.getItemId(), cmd.getQuantity());
if (!withholdSuccess) {
throw new IllegalArgumentException("Inventory not enough");
}
Order order = new Order();
order.setBuyerId(cmd.getUserId());
order.setSellerId(item.getSellerId());
order.setItemId(item.getItemId());
order.setItemTitle(item.getTitle());
order.setItemUnitPrice(item.getPriceInCents());
order.setCount(cmd.getQuantity());
Order savedOrder = orderRepository.save(order);
return orderDtoAssembler.orderToDTO(savedOrder);
}
}
-
判断是否业务流程的几个点:
boolean withholdSuccess = inventoryService.withhold(cmd.getItemId(), cmd.getQuantity());
if (!withholdSuccess) {
throw new IllegalArgumentException("Inventory not enough");
}
// 5)领域计算
Long cost = item.getPriceInCents() * quantity;
order.setTotalCost(cost);
@Data
public class Order {
private Long itemUnitPrice;
private Integer count;
// 把原来一个在ApplicationService的计算迁移到Entity里
public Long getTotalCost() {
return itemUnitPrice * count;
}
}
order.setItemUnitPrice(item.getPriceInCents());
order.setCount(cmd.getQuantity());
OrderDTO dto = orderDtoAssembler.orderToDTO(savedOrder);
-
常用的ApplicationService“套路”
-
准备数据:包括从外部服务或持久化源取出相对应的Entity、VO以及外部服务返回的DTO。 -
执行操作:包括新对象的创建、赋值,以及调用领域对象的方法对其进行操作。需要注意的是这个时候通常都是纯内存操作,非持久化。 -
持久化:将操作结果持久化,或操作外部系统产生相应的影响,包括发消息等异步操作。
▐DTOAssembler
ApplicationService应该永远返回DTO而不是Entity
-
构建领域边界:ApplicationService的入参是CQE对象,出参是DTO,这些基本上都属于简单的POJO,来确保Application层的内外互相不影响。 -
降低规则依赖:Entity里面通常会包含业务规则,如果ApplicationService返回Entity,则会导致调用方直接依赖业务规则。如果内部规则变更可能直接影响到外部。 -
通过DTO组合降低成本:Entity是有限的,DTO可以是多个Entity、VO的自由组合,一次性封装成复杂DTO,或者有选择的抽取部分参数封装成DTO可以降低对外的成本。
import org.mapstruct.Mapper;
@Mapper
public interface OrderDtoAssembler {
OrderDtoAssembler INSTANCE = Mappers.getMapper(OrderDtoAssembler.class);
OrderDTO orderToDTO(Order order);
}
public class CheckoutServiceImpl implements CheckoutService {
private final OrderDtoAssembler orderDtoAssembler = OrderDtoAssembler.INSTANCE;
@Override
public OrderDTO checkout(@Valid CheckoutCommand cmd) {
// ...
Order order = new Order();
// ...
Order savedOrder = orderRepository.save(order);
return orderDtoAssembler.orderToDTO(savedOrder);
}
}

▐ResultvsException
Application层只返回DTO,可以直接抛异常,不用统一处理。所有调用到的服务也都可以直接抛异常,除非需要特殊处理,否则不需要刻意捕捉异常
▐Anti-CorruptionLayer防腐层
ItemDO item = itemService.getItem(cmd.getItemId());
boolean withholdSuccess = inventoryService.withhold(cmd.getItemId(), cmd.getQuantity());
-
对于依赖的外部对象,我们抽取出所需要的字段,生成一个内部所需的VO或DTO类 -
构建一个新的Facade,在Facade中封装调用链路,将外部类转化为内部类 -
针对外部系统调用,同样的用Facade方法封装外部调用链路


// 自定义的内部值类
@Data
public class ItemDTO {
private Long itemId;
private Long sellerId;
private String title;
private Long priceInCents;
}
// 商品Facade接口
public interface ItemFacade {
ItemDTO getItem(Long itemId);
}
// 商品facade实现
@Service
public class ItemFacadeImpl implements ItemFacade {
@Resource
private ExternalItemService externalItemService;
@Override
public ItemDTO getItem(Long itemId) {
ItemDO itemDO = externalItemService.getItem(itemId);
if (itemDO != null) {
ItemDTO dto = new ItemDTO();
dto.setItemId(itemDO.getItemId());
dto.setTitle(itemDO.getTitle());
dto.setPriceInCents(itemDO.getPriceInCents());
dto.setSellerId(itemDO.getSellerId());
return dto;
}
return null;
}
}
// 库存Facade
public interface InventoryFacade {
boolean withhold(Long itemId, Integer quantity);
}
@Service
public class InventoryFacadeImpl implements InventoryFacade {
@Resource
private ExternalInventoryService externalInventoryService;
@Override
public boolean withhold(Long itemId, Integer quantity) {
return externalInventoryService.withhold(itemId, quantity);
}
}
@Service
public class CheckoutServiceImpl implements CheckoutService {
@Resource
private ItemFacade itemFacade;
@Resource
private InventoryFacade inventoryFacade;
@Override
public OrderDTO checkout(@Valid CheckoutCommand cmd) {
ItemDTO item = itemFacade.getItem(cmd.getItemId());
if (item == null) {
throw new IllegalArgumentException("Item not found");
}
boolean withholdSuccess = inventoryFacade.withhold(cmd.getItemId(), cmd.getQuantity());
if (!withholdSuccess) {
throw new IllegalArgumentException("Inventory not enough");
}
// ...
}
}
OrchestrationvsChoreography
▐模式简介


▐案例


▐模式的区别和选择
-
从代码依赖关系来看:
-
从代码灵活性来看:
-
从调用链路来看:
-
从业务职责来看:
-
小结:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
-
所以在日常业务中当你碰到一个需求时,该如何选择是用Orchestration还是Choreography?


-
哪个模式更好?
▐跟DDD分层架构的关系
-
O&C其实是Interface层的关注点,Orchestration =对外的API,而Choreography =消息或事件。当你决策了O还是C之后,需要在interface层承接这些“驱动力”。 -
无论O&C如何设计,Application层都“无感知”,因为ApplicationService天生就可以处理Command、Query和Event,至于这些对象怎么来,是Interface层的决策。
总结
Interface层:
-
职责:主要负责承接网络协议的转化、Session管理等
-
接口数量:避免所谓的统一API,不必人为限制接口类的数量,每个/每类业务对应一套接口即可,接口参数应该符合业务需求,避免大而全的入参
-
接口出参:统一返回Result
-
异常处理:应该捕捉所有异常,避免异常信息的泄漏。可以通过AOP统一处理,避免代码里有大量重复代码。
-
入参:具像化Command、Query、Event对象作为ApplicationService的入参,唯一可以的例外是单ID查询的场景。 -
CQE的语意化:CQE对象有语意,不同用例之间语意不同,即使参数一样也要避免复用。 -
入参校验:基础校验通过Bean Validation api解决。Spring Validation自带Validation的AOP,也可以自己写AOP。 -
出参:统一返回DTO,而不是Entity或DO。 -
DTO转化:用DTO Assembler负责Entity/VO到DTO的转化。 -
异常处理:不统一捕捉异常,可以随意抛异常。
-
用ACL防腐层将外部依赖转化为内部代码,隔离外部的影响
-
没有最好的模式,取决于业务场景、依赖关系、以及是否有业务“负责人”。避免拿着锤子找钉子。
▐前瞻预告
-
CQRS是Application层的一种设计模式,是基于Command和Query分离的一种设计理念,从最简单的对象分离,到目前最复杂的Event-Sourcing。这个topic有很多需要深入的点,也经常可以被用到,特别是结合复杂的Aggregate。后面单独会拉出来讲,标题暂定为《CQRS的7层境界》 -
在当今复杂的微服务开发环境下,依赖外部团队开发的服务是不可避免的,但强耦合带来的成本(无论是变更、代码依赖、甚至Maven Jar包间接依赖)是一个复杂系统长期不可忽视的点。ACL防腐层是一种隔离理念,将外部耦合去除,让内部代码更加纯粹。ACL防腐层可以有很多种,Repository是一种特殊的面相数据持久化的ACL,K8S-sidecar-istio 可以说是一种网络层的ACL,但在Java/Spring里可以有比Istio更高效、更通用的方法,待后文介绍。
-
当你开始用起来DDD时,会发现很多代码模式都非常类似,比如主子订单就是总分模式、类目体系的CPV模式也可以用到一些活动上,ECS模式可以在互动业务上发挥作用等等。后面会尝试总结出一些通用的领域设计模式,他们的设计思路、可以解决的问题类型、以及实践落地的方法。
欢迎看到这里的同学给我提任何关于DDD的问题,我会尽可能的回答。文章中的代码案例会申请发布到github上,供大家参考。
我的邮箱:guangmiao.lgm@alibaba-inc.com,也可以加我的钉钉号:luangm(殷浩)
作者|殷浩
编辑|橙子君
出品|阿里巴巴新零售淘系技术


本文仅供学习!所有权归属原作者。侵删!文章来源: 大淘宝技术
文章评论