9.4 服务层
在应用边界定义一组可用操作,协调领域对象、事务和外部端口对每个用例的响应。
学习目标
- 能为订单折扣与授信定义清晰的服务层操作、输入输出和用例边界
- 能区分服务层的编排、事务协调与领域对象的业务决定
- 能识别胖服务、贫血领域对象和跨系统副作用,并提出可测试的拆分方案
为什么 9.4 服务层 值得单独学习
↡在应用边界提供一组面向用例的操作,负责把请求转换为对领域对象、持久化和外部端口的协调调用。它的价值不是再造一层抽象,而是让客户端依赖稳定的应用操作,同时把授权、事务和失败传播放在可审查的位置。
订单折扣与授信可以有 HTTP、批处理和后台任务三种入口。如果三种入口各自读取订单、计算折扣和扣减额度,规则会迅速漂移;如果一个控制器承担所有决定,入口协议又会吞掉业务边界。服务层应该把“提交订单”定义为一个可复用的用例操作,而不是把所有业务知识集中到一个万能类。
本章的案例、服务流程图、实验和练习均为课程原创;公开图书页与模式目录只用于核对 9.4 的名称、范围和模式族关系。学习重点是通过调用路径和失败证据判断一层协调代码是否仍然保持了清晰责任。
先建立直觉:服务层协调什么
先猜一猜:当客户端只知道 PlaceOrder 这个操作,而不知道订单存在哪张表、折扣由哪个对象计算时,服务层为客户端隐藏了什么?它隐藏的是应用内部的协作细节,不是业务规则本身。服务层可以决定调用顺序,却不应重新实现每一条折扣和授信条件。
一次操作要先有明确的↡:输入从哪里开始,成功结果是什么,哪些状态必须一起变化,失败应该返回什么。服务层围绕这个边界组织调用;领域模型或事务脚本负责作出业务决定;仓储和消息端口负责技术交互。
服务层的另一个核心职责是↡。它可以开启事务,加载订单与授信账户,调用领域行为,再保存必要变化;但它不应为了“方便”绕过对象行为直接改写余额或复制折扣条件。协调意味着管理顺序和边界,不意味着拥有所有知识。
目录单元到教学证据
9.4 服务层
在 9.4 服务层 的单元范围内,学习者要能画出“客户端 → 服务操作 → 领域对象 → 持久化/外部端口 → 结果”的调用链,并在每个节点标注它拥有的责任。单元键为 poeaa24-pattern-04-service-layer;核心证据是换一个客户端入口后,业务决定仍然沿用同一个服务操作和同一组领域行为。
评审记录要回答五个问题:操作是否以用例命名,输入输出是否稳定,事务由谁开启和结束,领域规则由谁决定,外部失败如何传播。如果服务层同时回答所有问题,说明它已经从协调者膨胀为新的领域逻辑容器。
专属设计案例:订单折扣与授信
定义 PlaceOrder 服务操作:接收订单行、客户标识和幂等键,加载 Order、Customer 与 CreditAccount,调用折扣和授信行为,在一个事务中保存本地状态,最后返回订单编号与提交结果。HTTP 控制器只负责认证、解析请求和映射响应;批处理入口也调用同一个服务操作。
设计记录采用五个字段:模式键为 poeaa24-pattern-04-service-layer;当前裁决是“当多个客户端需要相同用例边界和事务协调时采用服务层”;观测项包括操作契约、对象协作、事务范围、外部端口和演化成本;保留条件是“服务方法可以用调用顺序解释,业务决定落在领域对象或事务脚本”;拒绝条件是“服务层开始暴露表结构,或每个客户端都需要一套业务分支”。
服务操作的最小契约
一个服务操作应有业务可理解的名字、明确输入和稳定结果。PlaceOrder 不应暴露 ORM 实体,也不应让调用者自行拼出一串数据库更新;它可以接收不可变的命令数据,返回订单状态、拒绝原因和可追踪的操作编号。输入输出稳定,客户端才能与内部持久化变化解耦。
服务层与领域行为的分工
应用服务可以调用 order.submit()、credit.reserve() 和 discount.calculate(),但不应把这些行为展开成一长串条件。领域对象知道如何保护状态,服务层知道何时调用它们以及成功后保存哪些变化。这样既能复用规则,也能单独测试“编排是否完整”和“规则是否正确”。
模式结构图
用订单提交走一遍服务层设计
先定义一个用例操作
先预测:如果 HTTP 和批处理都需要提交订单,应该复制两套业务流程,还是让它们调用同一个 PlaceOrder 操作?先写出输入、成功结果和拒绝结果,再检查操作名是否表达业务用例,而不是数据库表动作。
常见误区
事务、错误与外部端口
服务层通常负责声明↡:哪些本地状态必须一起提交,哪些调用必须在提交前完成,哪些副作用要在提交后可靠投递。它不需要把所有端口实现写在同一个类中,而应通过接口或明确的适配器与消息、支付和库存系统连接。
当领域对象拒绝提交时,服务层应把业务拒绝转换为客户端能理解的结果,同时保持本地状态不变。当数据库提交成功但外部通知失败时,服务层需要记录待投递状态、安排补偿或使用可靠消息机制。↡让重复的 PlaceOrder 请求不会重复扣减额度;幂等键应进入操作契约,而不是隐藏在某个控制器的临时变量里。
服务层也可以承担↡的角色,但名称不应掩盖责任。应用服务适合做授权、事务、加载、调用和结果映射;如果它开始拥有会员等级规则、折扣公式和跨对象不变量,就应重新检查领域模型或事务脚本是否被绕过。
与相邻模式的选择
事务脚本把一个用例的业务流程集中在一个过程中,服务层则把应用入口和事务协调明确成稳定操作;领域模型把复杂规则和不变量放入对象协作;表模块适合以一张表为中心的结构化操作。服务层可以与后三者组合,不是它们的替代品。
选择服务层的证据是:多个客户端需要同一个用例契约,事务和授权需要统一处理,而业务决定仍能交给合适的领域组织。若系统只有一个简单入口,服务层只是转发一行调用,额外层次可能没有收益;若服务层必须读写每个表字段才能完成一个用例,则边界已经失去抽象价值。
选择与拒绝矩阵
| 评审问题 | 选择服务层的证据 | 应拒绝或准备重构的信号 |
|---|---|---|
| 契约 | 操作按业务用例命名,输入输出稳定 | 对外暴露表结构和 ORM 实体 |
| 编排 | 服务层清晰协调加载、领域行为、事务和结果 | 调用顺序隐藏在多个控制器或回调里 |
| 规则 | 折扣、授信和不变量由领域对象或事务脚本决定 | 服务方法包含大段业务条件和状态计算 |
| 失败 | 本地回滚、业务拒绝和外部补偿路径可区分 | 所有异常都被包装成成功或统一重试 |
| 复用 | HTTP、批处理和后台任务共享同一个用例操作 | 每个客户端复制一套流程和权限判断 |
本章小结
掌握 9.4 服务层的标志,不是记住“在控制器外面再包一层”,而是能画出稳定的用例契约,并说明服务层如何协调授权、事务、领域行为和外部端口。它负责应用边界和调用顺序,不应吞掉折扣规则、授信不变量或持久化结构。通过重复请求、业务拒绝和外部失败三类证据,才能判断服务层是否真的降低了耦合。
本章练习
练习
问题 1: HTTP 入口和每日批处理都需要提交订单。请为服务层设计一个操作,并列出它应该协调的主要步骤。
问题 2: PlaceOrder 服务方法中出现了会员等级判断、折扣计算和授信额度公式。如何判断哪些代码应该移出服务层?
问题 3: 数据库事务提交成功后,库存通知发送失败;客户端随后重试同一个请求。服务层需要提供哪些证据?
前后导航
出处声明
名词解释
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 服务层
- 在应用边界提供面向用例的操作,协调领域对象、事务和外部端口,不负责复制全部领域规则。
- 用例边界
- 一次业务操作从输入、决定、状态变化到结果的责任范围。
- 事务协调
- 安排加载、领域行为、持久化提交和失败处理顺序的应用职责。
- 事务边界
- 必须整体提交或整体失败的一组本地持久化变化。
- 幂等性
- 同一个操作被重复处理时,不会产生非预期的额外状态变化或副作用。
- 应用服务
- 以用例为单位执行授权、加载、调用、事务和结果映射的服务层实现。