9.1 事务脚本
按一个业务用例组织一个过程,直接协调校验、计算和数据访问;适合规则少、流程线性的系统。
学习目标
- 能用一次订单折扣请求画出事务脚本的输入、规则、持久化和结果责任链
- 能依据规则数量、变化频率、复用范围和事务边界判断何时适合采用事务脚本
- 能识别控制器膨胀、跨用例复制和回滚不清等信号,并提出迁移到领域模型或服务层的方案
为什么 9.1 事务脚本 值得单独学习
↡是一种以单个业务用例为单位组织逻辑的过程式模式:它读取输入,依次执行校验、计算和写入,再返回结果。它的价值不在于“把所有代码放进一个大函数”,而在于让一个简单用例的责任链可以被完整阅读、测试和回滚。
在订单折扣与授信中,最初的需求可能只有“金额满 100 减 10”和“授信足够才允许下单”。如果每条规则都稳定、调用者只有一个、步骤顺序也清楚,直接建立复杂对象网络反而会增加理解成本。本章把这个小场景逐步加压,观察事务脚本何时仍然清晰,以及何时必须把规则移到更合适的结构中。
本页依据 Martin Fowler 的作者图书页与模式目录独立重写,不复现原书正文、插图或代码。原书目录只限定本章要解释的模式边界;订单折扣与授信、评审表、练习和视觉图均为本课程原创。
先建立直觉:一个过程能负责到哪里
先猜一猜:如果下单请求只有一个入口,过程依次完成校验、读取订单、计算折扣、检查授信并保存结果,那么把这五步放在一个事务脚本里,最先得到的收益是什么?答案通常是责任链短、调试路径直接;代价则是规则一旦被其他用例复用,过程会开始承担它不该承担的变化。
事务脚本的关键不是函数长短,而是清楚的↡。一次“提交订单”由一个过程负责,不代表所有业务概念都必须变成这个过程的私有变量。过程可以调用折扣表、授信查询或持久化接口,但它必须明确谁决定通过、谁修改状态,以及失败时哪些写入需要撤销。
还要区分过程的应用编排和数据库的↡。前者回答“这次用例按什么顺序完成”,后者回答“哪些持久化变化必须整体提交”。一个过程可以位于事务边界之内,也可以调用多个边界受控的资源;把两者混成一个概念,会让异常处理只剩下“捕获错误然后重试”。
目录单元到教学证据
9.1 事务脚本
在 9.1 事务脚本 的学习边界里,学习者要能解释“一个用例对应一个过程”为什么适合规则简单、流程线性的场景,也要能说明它不是对未来复杂度的承诺。单元键为 poeaa24-pattern-01-transaction-script;本章用订单折扣与授信作为同一条观察线,记录规则在哪里执行、状态在哪里落盘、失败在哪里停止。
对照证据包括四项:规则是否集中在一个可读过程内,规则是否只被一个用例使用,写入是否受同一事务边界保护,以及新增规则是否会迫使多个用例同步修改。只要第四项持续变坏,就不能继续用“过程直观”掩盖结构已经失配。
专属设计案例:订单折扣与授信
把一次提交订单拆成“请求 → 校验 → 读取 → 计算 → 授信 → 保存 → 结果”七个观察点。事务脚本的设计记录必须写出:输入由谁验证,折扣由谁决定,授信失败时是否已经写入订单,重试会不会重复扣减额度,以及这条规则是否会被退款和改价用例复用。
记录采用五个可换行字段:模式键为 poeaa24-pattern-01-transaction-script;当前裁决是“规则少、流程顺序稳定时先采用单用例过程”;观测项包括规则复杂度、对象协作、事务边界、复用范围和演化成本;保留条件是“失败可以在保存前明确返回或整体回滚”;拒绝条件是“第二个用例必须复制同一组业务规则,或一个小规则改动同时触及多个过程”。
过程与责任的最小切片
一个可审查的事务脚本可以按四段阅读:先校验输入,再读取必要状态;然后计算折扣并检查授信;最后在同一事务边界内写入订单和额度变化。这个顺序不是框架模板,而是让评审者能指出每个失败点发生在保存之前还是之后。
如果折扣规则只是稳定的金额区间,过程内的条件分支是可接受的。若规则开始依赖会员等级、促销互斥、历史订单和渠道组合,条件本身就变成需要独立表达的业务知识;此时“少建对象”不再等于“少维护代码”。
模式结构图
用订单折扣走一遍选择过程
先固定一个用例边界
先预测:如果只有“满 100 减 10”一条折扣规则,过程是否能从请求一路读到结果,并在一处说明失败条件?若能,事务脚本可以让入口、规则和持久化顺序保持可见;不要为了抽象而先拆出没有独立变化的对象。
常见误区
选择与拒绝矩阵
| 评审问题 | 选择事务脚本的证据 | 应拒绝或准备迁移的信号 |
|---|---|---|
| 责任 | 一个用例由一个过程完整编排 | 控制器、仓储和多个过程同时改写同一规则 |
| 变化 | 规则少且变化集中在当前用例 | 一个小规则改动需要同步修改多个用例 |
| 失败 | 校验和计算在写入前完成,失败路径可测试 | 写入、外部通知和重试的先后关系无法说明 |
| 复用 | 其他入口只调用当前用例,不复制业务条件 | 退款、改价、批处理都复制折扣与授信分支 |
| 替代 | 规则增长时保留迁移到领域模型的切口 | 因“函数看起来简单”而拒绝承认对象协作已经复杂 |
何时迁移到其他模式
事务脚本并不要求系统永远保持过程式结构。它适合先把一个边界清晰的用例做对;当同一业务决定被多个用例共享,或者对象之间的不变量必须持续保持时,应评估↡。领域模型不是“类越多越好”,而是把需要独立演化的状态和不变量放在能表达它们的协作对象中。
应用入口仍然需要一层协调,但服务层应该负责授权、事务范围和用例编排,而不是再次复制折扣条件。迁移的最小步骤可以是:先为现有过程补齐规则测试,再抽出最稳定的不变量,最后让原事务脚本调用新对象;这样每一步都有可回退的运行证据。
在过程里处理跨系统副作用时,还需要明确↡:同一个请求因网络重试再次到达,结果不能无意中重复扣减额度或发送确认。幂等键、唯一约束和补偿记录都可以成为设计的一部分,但它们不应被含糊地归因于数据库事务本身。
本章小结
掌握 9.1 事务脚本的标志,不是记住“一个大函数”这个表面形式,而是能在订单折扣与授信中说清用例边界、规则顺序、事务边界和失败恢复。规则少、调用者单一、流程线性时,事务脚本能以最低的结构成本交付清晰结果;规则开始跨用例复用、跨对象维护不变量或产生外部副作用时,就要用测试证据推动迁移,而不是继续堆叠条件分支。
本章练习
练习
问题 1: 一个订单服务只有“满 100 减 10”和“授信额度足够才允许提交”两条稳定规则。请写出事务脚本的四个主要阶段,并说明哪一步必须发生在保存之前。
问题 2: 退款和改价也需要会员等级、促销互斥和授信规则。团队准备把原下单过程复制两份,最快的评审结论是什么?
问题 3: 网络超时后客户端重试提交订单,数据库事务已经提交,但确认消息可能已经发送。如何评审这段事务脚本的设计?
前后导航
出处声明
名词解释
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 事务脚本
- 以一个业务用例为单位组织逻辑的过程式模式,适合规则少、流程线性的场景。
- 用例边界
- 一次业务用例负责的输入、决定、状态变化与结果范围,用来界定过程应承担的责任。
- 事务边界
- 必须整体提交或整体失败的一组持久化状态变化,不等同于业务规则本身。
- 领域模型
- 用对象及其协作表达复杂业务规则与不变量的组织方式,适合规则频繁变化或跨用例复用的场景。
- 幂等性
- 同一个请求被重复处理时不会产生非预期的额外状态变化或副作用。