DTO Assembler与Factory的区别及聚合创建最佳实践咨询
DDD分层下跨层创建聚合的标准实现
你对两个核心组件的职责认知是准确的:
- 领域层Factory的核心定位是封装复杂聚合的创建逻辑,保证返回的聚合实例始终满足业务不变式,不能依赖应用层的任何数据结构
- 应用层DTO Assembler的原生职责是做已构建完成的领域对象与出/入参DTO的结构转换,不应该承载聚合创建的业务校验逻辑
核心实现思路
核心原则非常明确:层间依赖只能是外层依赖内层,内层绝对不能感知外层的存在,参数适配逻辑放在外层完成,核心创建逻辑收敛在内层Factory,落地分3步走:
1. 应用层完成入参适配,不向领域层透传DTO
入参DTO是应用层/接口层的专属数据结构,本身可能携带序列化注解、前端参数校验注解、冗余透传字段等和领域逻辑无关的内容,绝对不能传到领域层。
你只需要在应用服务的业务方法里,先完成应用层维度的参数校验(比如手机号格式、字段非空这类和核心业务规则无关的入参合法性校验),再把DTO里创建聚合需要的有效字段拆解出来,转换成领域层定义的基础类型、值对象,再传给领域层Factory即可。
举个订单聚合创建的落地代码示例:
// 应用服务层代码 public OrderDTO create(CreateOrderDTO request) { // 应用层参数校验:只做格式、非空这类无业务含义的入参校验 checkRequestValid(request); // 拆解DTO,转换为领域层定义的类型,没有任何DTO依赖 UserId buyerId = new UserId(request.getBuyerId()); DeliveryAddress deliveryAddress = DeliveryAddress.builder() .province(request.getProvince()) .city(request.getCity()) .district(request.getDistrict()) .detail(request.getDetailAddress()) .contactName(request.getReceiverName()) .contactPhone(request.getReceiverPhone()) .build(); List<OrderItemCreateParam> itemParams = request.getSkuList().stream() .map(sku -> new OrderItemCreateParam(sku.getSkuId(), sku.getPurchaseCount(), sku.getActualUnitPrice())) .toList(); // 调用领域层Factory创建聚合,传入的所有参数都是领域层自身定义的类型 Order orderAggregate = OrderFactory.create(buyerId, deliveryAddress, itemParams, request.getOrderRemark()); // 后续持久化、通过Assembler转换为出参DTO返回 orderRepository.save(orderAggregate); return orderAssembler.toDTO(orderAggregate); }
这一步的参数拆解本质是外层的适配工作,不属于核心业务逻辑,完全不需要塞给Assembler或者Factory,放在应用服务里是最合理的。
2. 领域层Factory收敛所有创建相关的核心业务规则
Factory不需要关心入参来自HTTP接口的DTO、MQ消息还是数据库重建,它只识别领域层自身定义的参数类型,只负责和聚合核心规则相关的校验与初始化:比如订单条目不能为空、收货地址信息满足配送范围要求、订单初始状态为「待支付」、订单总金额计算和明细金额一致这类核心业务不变式,校验通过后返回完整、合法的聚合实例。
3. 多参数场景的优化方案
如果创建聚合需要的参数过多,直接在Factory方法上传递一长串参数可读性很差,可以选择两种合规的优化方式,都不会违反分层规则:
- 在领域层定义专属的聚合创建命令对象(比如
CreateOrderCommand):注意这个Command是定义在领域层的模型,和应用层DTO有本质区别——它没有任何应用层专属的注解、依赖,只承载创建聚合需要的业务字段和基础字段约束,应用层拆解DTO后组装成这个Command传给Factory即可。 - 对高内聚的参数组抽值对象:比如把收货地址相关的7、8个字段封装为
DeliveryAddress值对象,把优惠相关的字段封装为OrderDiscount值对象,既减少参数个数,也符合领域模型的抽象原则。
常见错误做法避坑
- 不要为了省事把DTO传到领域层:哪怕给DTO改个名字,只要它定义在应用层、携带应用层专属逻辑,就会导致领域层产生对外层的依赖,后续如果入参来源从HTTP换成MQ、RPC,就要改动领域层代码,完全违背DDD隔离核心领域逻辑的初衷。
- 不要让DTO Assembler承担聚合创建职责:Assembler的定位是无业务逻辑的结构转换器,如果在Assembler里嵌入聚合创建、业务校验逻辑,会导致核心业务规则散落在转换层,后续维护时根本找不到逻辑入口。
- 不要在应用服务里直接
new聚合:聚合创建往往包含大量默认值初始化、业务规则校验逻辑,直接散落在应用服务里会导致领域逻辑泄露,后续其他场景(比如从数据库重建聚合、从历史数据迁移重建聚合)要重复编写创建逻辑,很容易出现规则不一致的bug。
内容的提问来源于stack exchange,提问作者NiYanchun
相关产品推荐
相关产品推荐

