Skip to content

应用层落地模式

本文档介绍应用服务层的落地方式:WriteService 怎么搭、execute() 模板顺序、创建 / 修改 / 预校验三场景、规则装配与执行器选择。前置阅读:聚合目录落地骨架 · 聚合设计原则

1. 本质与定位

应用服务层(Application)是编排领域逻辑的层:接收 Input → 建聚合(EntityFactory)/ 改聚合(EntityUpdater)→ 规则校验 → 仓储持久化 → 领域事件发布,全部由 execute() 模板统一编排,置于同一事务边界。

  • 职责:编排(把 Factory / Updater / Rule / Repository / EventManager 串起来)。
  • 不做:不写业务逻辑(业务逻辑在聚合根)、不操作持久化细节(仓储承担)、不做协议转换(UI 层承担)。
  • 核心形态:应用服务分命令(写)查询(读)两类——写服务 {Agg}WriteService 继承 AbstractApplicationService,每个用例一个公开方法、内部走 execute() / tryExecute();读服务 {Agg}ReadService 无框架基类,注入领域层源端口、implements IQueryApplicationService,只读不写(见 §4.8)。

2. 命名与包结构

2.1 包结构

text
application/{agg}/
├── {Agg}WriteService.java      # 命令应用服务(外层)
├── {Agg}ReadService.java       # 查询应用服务(外层)
├── input/                      # {Action}Input 业务语义入参
├── factory/                    # EntityFactory 聚合工厂(创建场景)
├── updater/                    # EntityUpdater 修改器(修改场景)
├── resolver/                   # Command → 领域输入适配
├── rule/                       # 规则装配(如 OrderRuleConfig)
├── service/                    # 领域服务实现
└── subscriber/                 # 事件订阅登记

2.2 命名规范

类型命名示例
命令应用服务{Agg}WriteServiceOrderWriteService
查询应用服务{Agg}ReadServiceOrderReadService
入参{Action}InputPayOrderInput
工厂{Agg}FactoryOrderFactory
修改器{Agg}{Action}UpdaterOrderPayUpdater
规则装配{Agg}RuleConfigOrderRuleConfig

3. 数据 / 职责承载

承载不承载
编排依赖(Factory / Updater / Rule / Repository / EventManager)业务规则判断(聚合根 / 规则容器)
事务边界与事件发布(委托执行器)持久化 SQL / 技术细节(仓储)
Input → 领域对象的组装与转换(Factory / Updater / Resolver)协议 Request / Response(UI 层)

4. 落地方式(核心)

4.1 WriteService 骨架

java
@Service
public class OrderWriteService extends AbstractApplicationService
        implements ICommandApplicationService {

    private final OrderFactory orderFactory;
    private final OrderRule orderRule;
    private final OrderRepository orderRepository;
    private final OrderPayUpdater orderPayUpdater;
    // ... 其余 Updater

    public OrderWriteService(IEventManager eventManager,
                             OutboxCommandExecutor commandExecutor,
                             Supplier<IUnitOfWork> unitOfWorkFactory,
                             OrderFactory orderFactory,
                             OrderRule orderRule,
                             OrderRepository orderRepository,
                             OrderPayUpdater orderPayUpdater) {
        // 执行器与工厂由组合根(OutboxConfig)以成品 Bean 注入,服务内不再手动 new
        super(eventManager, commandExecutor, unitOfWorkFactory);
        this.orderFactory = orderFactory;
        this.orderRule = orderRule;
        this.orderRepository = orderRepository;
        this.orderPayUpdater = orderPayUpdater;
    }

    /** 支付:加载聚合 → Updater 改聚合 → 统一校验 + 持久化 + 事件发布。 */
    public Order payOrder(Long orderId, PayOrderInput input) {
        Order order = orderRepository.findById(orderId);
        if (order == null) {
            return null;
        }
        return super.execute(order, orderRule, orderRepository, t -> orderPayUpdater.apply(t, input));
    }
}

要点:

  • 继承 AbstractApplicationService必须显式注入命令执行器与工作单元工厂(不再提供默认构造器)。
  • OutboxCommandExecutorSupplier<IUnitOfWork>(outbox 工厂)由 OutboxConfig 以成品 Bean 提供,执行器与工厂语义须一致。
  • 每个用例一个公开方法:加载 / 创建聚合 → execute(...)
  • execute 四参:聚合根、规则容器、仓储、领域逻辑(Consumer<T>)。

4.2 execute() 模板顺序

AbstractCommandExecutor.execute() 固定五步,子类只管"怎么落库 + 分发":

text
1. domainLogic.accept(aggregateRoot)   执行领域逻辑(Factory 建 / Updater 改)
2. satisfiesRule(rule)                 规则校验,失败抛 BrokenRuleException
3. persistAndDispatch                  落库(repository.save)+ 事件发布(eventManager.publish)
4. clearWorkUnitState()                事件 / 操作清空(防止跨请求串味)

事件发布后的清理由模板内置,继承 AbstractApplicationService 无需手动调用;仅在自研编排时需要。

4.3 创建场景:EntityFactory(先算后赋)

java
@Component
public class OrderFactory implements EntityFactory<Order, CreateOrderInput> {

    @Override
    public Order create(CreateOrderInput input) {
        Long orderId = idGenerator.nextId();
        List<OrderItem> items = totalAmountResolver.toOrderItems(input, orderId);
        Customer customer = new Customer(input.getCustomerId(), input.getCustomerName());
        Order probeOrder = new Order(probeData(customer), orderId);    // 临时探测 Order
        Money total = totalAmountResolver.resolve(input, probeOrder);  // 先算派生属性
        // ... 组装 OrderInitData 并 setTotalAmount(total),再 new Order(data, orderId) // 后赋
    }
}

EntityFactory<T, C> 契约:T create(C command),从 Command DTO 构建聚合,遵循「先算后赋」。WriteService 下单用例 orderFactory.create(input) 后交给 execute 落库。

4.4 修改场景:加载聚合 + EntityUpdater

java
@Component
public class OrderPayUpdater implements EntityUpdater<Order, PayOrderInput> {

    @Override
    public void apply(Order aggregateRoot, PayOrderInput command) {
        PaymentInfo paymentInfo = new PaymentInfo(
                command.getPaymentSerialNo(),
                new Money(command.getPlatformDiscountAmount(), command.getCurrency()),
                new Money(command.getAmount(), command.getCurrency()));
        aggregateRoot.pay(paymentInfo);   // 调聚合充血方法
    }
}

EntityUpdater<T, C> 契约:void apply(T aggregateRoot, C command)。职责 = Input → 领域对象转换 + 调充血方法;不做校验、不持久化、不发事件(由 execute 模板统一)。

4.5 预校验:tryExecute → DryRunResult

需要"先试跑不落库"的场景(如表单预校验 / 下单前检查):

java
public DryRunResult tryPayOrder(Long orderId, PayOrderInput input) {
    Order order = orderRepository.findById(orderId);
    if (order == null) {
        return null;
    }
    return super.tryExecute(order, orderRule, orderRepository, t -> orderPayUpdater.apply(t, input));
}

tryExecute 返回 DryRunResultpassed() / brokenRules()),不落库、不发事件,供预校验反馈。

4.6 规则装配:OrderRuleConfig

规则容器 OrderRule 的构造依赖(领域服务契约、仓储)由装配配置显式声明,领域层保持零 Spring 依赖:

java
@Configuration
public class OrderRuleConfig {

    @Bean
    public OrderRule orderRule(IOrderCustomerPermissionService permissionService,
                               IOrderRepository orderRepository) {
        return new OrderRule(permissionService, orderRepository);
    }
}

规则容器的构造与触发完整落地见 聚合业务规则(OrderRule 范式)

4.7 执行器选择

执行器语义场景
CommandExecutor落库后立即发布事件不需要 Outbox 的事务一致性兜底
OutboxCommandExecutor聚合写 + outbox 行同事务,异步投递跨模块可靠投递 / 崩溃兜底(见 Outbox 链路装配

AbstractApplicationService 不再提供默认构造器,执行器与工作单元工厂必须由继承者显式注入(通常以组合根 @Bean 提供成品),且二者语义须一致:默认场景用 CommandExecutor + UnitOfWork,outbox 场景用 OutboxCommandExecutor + OutboxUnitOfWork,禁止混用。

4.8 读侧 ReadService:不进 execute() 模板

应用层有两类服务,写走命令({Agg}WriteService)、读走查询({Agg}ReadService,但读侧与写侧有本质差异:

维度WriteService(命令侧)ReadService(查询侧)
承载基类extends AbstractApplicationService(可写)无基类(只读,只 implements 标记接口)
应用服务标记implements ICommandApplicationServiceimplements IQueryApplicationService
是否走 execute() / tryExecute()走模板(校验 → 落库 → 发布事件)不走:只查,不建/改聚合
是否产生领域事件 / 写库是(同步落库 + 事件发布):读不产生业务事件、不持有写仓储
依赖Factory / Updater / Rule / Repository / EventManager领域层源端口(IOrderRedisSource / IOrderESSource

读服务为什么不继承 AbstractApplicationService:读侧没有"改聚合 → 校验 → 落库 → 发事件"这一套写语义,不需要 execute() 模板。框架也不再提供 AbstractProjectionQuery 查询基类——「用哪个源」由「源端口 extends 了哪些查询族」在编译期决定,读服务只需注入领域源端口、按族分派并完成「查全量 → 裁剪」两跳:

java
@Service
public class OrderReadService implements IQueryApplicationService {

    private final IOrderRedisSource redisSource;
    private final IOrderESSource esSource;

    public OrderReadService(IOrderRedisSource redisSource, IOrderESSource esSource) {
        this.redisSource = redisSource;
        this.esSource = esSource;
    }

    // 选源写死在方法体内(本项目:主键族走 Redis,条件族 / 分页族走 ES)
    public <X extends IOrderProjection> X queryById(Long id, Class<X> projectionType) {
        var full = redisSource.getById(id);
        if (full == null) {
            return null;
        }
        return reduceWith(redisSource.getReducer(projectionType), full, projectionType);
    }

    public <X extends IOrderProjection> List<X> queryList(OrderListQuery criteria, Class<X> projectionType) {
        var reducer = esSource.getReducer(projectionType);
        return esSource.search(criteria).stream()
                .map(full -> reduceWith(reducer, full, projectionType))
                .toList();
    }
    // queryByIds / queryOne / queryPage 同构;reduceWith 为私有两跳编排
}

选源写在读服务方法体内,不外泄给调用方:不要把 ProjectionSource 作为方法入参。读服务直接注入领域层源端口(框架已无 ProjectorRegistry 这一「源 id → 源实例」登记表,无从按 id 反查源),选源写死在方法体内。调用方只传条件与目标投影类型。

读服务的角色定位(为什么门面放应用层、选源写在这里)与两跳 / 裁剪的完整落地投影读模型代码落地指南。本小节只区分读写两侧的应用服务形态,不重复投影机制的细节。

⚠️ 读侧不发布领域事件、不持有写仓储:读模型由写侧事件物化而来(见 投影设计),ReadService 只消费读模型副本,不反向触发业务事件,避免读路径污染写一致性。

5. 关键机制与避坑

  • 业务方法内"先 recordOperationcollectEvent":事件 operationCode 自动取最近一次操作;顺序颠倒抛 OperationException。详见 操作注册表设计
  • 延迟事件(ID 后生成必用):构造期 entityIdnull,用 () -> XxxEvent.buildEvent(this),发布时才读真实 ID。详见 事件建模指南
  • 事务边界@Transactional 由调用方(WriteService 方法 / 执行器)负责,仓储不管理事务。
  • 异常响应映射BrokenRuleException(单条)/ BrokenRuleAggregateException(全量)/ PragmaticException(兜底),用 @RestControllerAdvice 统一映射:
java
@ExceptionHandler(BrokenRuleException.class)
public ResponseEntity<ErrorResponse> handleBrokenRule(BrokenRuleException e) {
    return ResponseEntity.badRequest().body(new ErrorResponse(e.getCode(), e.getMessage()));
}

6. 常见反模式

反模式问题正确做法
应用服务里写业务规则 / 状态判断业务逻辑泄漏到应用层、不可复用业务逻辑内聚聚合根,校验走规则容器
绕过 execute() 手动编排放事件 / 清状态模板顺序丢失、事件清理遗漏统一走 execute() / tryExecute()
Updater / Factory 里做校验或持久化职责混杂、模板被打断Updater 只转换 + 调充血方法;校验 / 持久化交模板
每个方法 new 一个执行器 / 事件管理器资源浪费、语义漂移构造器注入一次,复用 AbstractApplicationService
WriteService 直接操作仓储细节 / 批量 SQL仓储职责泄漏复杂查询交查询侧,写仓储只收聚合根

7. 下一步