9.3 表模块
用一个实例处理某张表或视图全部行的业务逻辑,在记录集与面向对象行为之间取得集合式平衡。
学习目标
- 能用一个表模块实例集中处理订单表的一组行,并说清它拥有的业务责任
- 能比较表模块、事务脚本和领域模型在规则复杂度、协作与演化成本上的取舍
- 能用正常、临界和受控故障场景验证事务边界,并识别何时应撤回表模块
为什么 9.3 表模块值得单独学习
↡以某张表或视图的全部行作为业务边界、集中提供这组记录相关操作的对象组织方式。表模块不是把每一行机械地包装成对象,而是用一个实例处理一组记录的业务逻辑,在记录集与面向对象行为之间取得集合式平衡。它适合规则围绕表或视图集合变化、但还没有足够理由为每一行建立完整领域对象的场景。
订单折扣与授信能暴露这个取舍。订单表模块可以集中处理折扣计算、授信校验和批量更新,让多个入口复用同一套集合规则;但如果每个订单都有复杂生命周期、不变量和跨对象协作,表模块的集合边界就可能遮住真正的领域边界。
↡由表模块实例负责的记录集合边界,决定哪些行可以一起读取、计算、校验和提交。边界必须能回答谁拥有筛选条件、谁保护集合不变量、谁开启事务,以及失败时哪些行应一起回退。
↡在一次表模块操作中由筛选条件确定、需要共同计算或提交的一组记录。Row Set 是表模块每次操作的实际输入,必须带着筛选条件、版本和预期变更一起进入验证,不能只传一串没有语义的行号。
先建立直觉:集合责任要有明确所有者
↡把一个用例的业务步骤顺序写成过程,由过程直接读取数据并调用规则;规则较简单且变化集中时较直接。事务脚本适合少量步骤和单一用例;当多个入口都要处理订单表的相同集合规则时,复制脚本会让折扣、额度和状态判断逐渐分叉。
↡用一组协作对象表达业务状态、行为和不变量,让规则靠近拥有知识与状态的对象。领域模型适合复杂、频繁变化且需要对象协作的规则,但会引入对象边界和持久化映射成本。表模块位于两者之间:它集中一组记录的行为,却不承诺每一行都拥有完整对象身份。
先预测:如果订单查询、批量折扣和授信校验都读取同一张订单表,应该让控制器各写一遍条件,还是让 OrderTableModule 统一维护集合规则?判断依据不是类名,而是规则是否围绕同一记录集变化,以及事务能否把相关行作为一个一致性单元处理。
目录单元到教学证据
9.3 表模块
在 9.3 表模块 的单元边界内,学习者要把订单折扣与授信拆成查询、计算、校验和提交四类证据:哪些操作共享同一个集合边界,哪些规则只适用于单行,哪些变化需要对象协作。单元键为 poeaa24-pattern-03-table-module;核心证据是相同规则从两个入口调用时仍只有一个实现,并且失败能定位到事务中的首个差异。
评审记录还要回答:表模块实例拥有哪张表或视图,集合筛选条件是否稳定,批量操作的事务范围是什么,数据库约束与代码规则如何分工,什么时候应迁移到领域模型。如果只能说“ORM 有一个模块类”,却说不出这些边界,模式选择还没有完成。
专属代码案例:订单折扣与授信
下面的代码把订单表模块的责任固定为集合操作。它不模拟 ORM API,而是刻意让评审看到:同一实例负责加载订单、应用折扣、校验额度和提交结果;调用者不能绕过模块直接改写中间状态。
type OrderRow = {
id: string;
customerId: string;
totalCents: number;
discountCents: number;
status: "pending" | "approved" | "rejected";
};
type CreditSnapshot = { availableCents: number };
class OrderTableModule {
constructor(private readonly rows: OrderRow[]) {}
approveBatch(ids: string[], credit: CreditSnapshot): OrderRow[] {
const selected = this.rows.filter((row) => ids.includes(row.id));
const required = selected.reduce(
(sum, row) => sum + row.totalCents - row.discountCents,
0,
);
if (required > credit.availableCents) {
throw new Error("credit limit exceeded");
}
for (const row of selected) row.status = "approved";
return selected;
}
}这段代码的最小证据合同是:选择条件由表模块拥有,批量总额计算集中在模块内,授信失败不会部分批准,成功状态只在校验通过后改变。生产实现还需把数据库事务、并发保护和消息投递接入同一张决策记录,不能把内存示例当成完整一致性保证。
模式结构图
模式结构图先显示调用方、表模块实例、记录集合与授信边界的责任流;它帮助评审区分“集合规则集中”与“所有业务都塞进一个大类”。
表模块的事务边界
一次批量批准应在读取快照、计算总额、校验额度和写回状态之间保持明确边界。若数据库更新与外部通知不在同一事务中,必须额外记录消息状态和补偿路径,不能把提交成功误认为所有副作用都已完成。
选择与拒绝矩阵
| 评审问题 | 选择表模块的证据 | 应拒绝或迁移的信号 |
|---|---|---|
| 责任 | 一个实例拥有同一表或视图的集合规则 | 多个入口各自复制折扣和授信条件 |
| 边界 | 相关行的筛选、计算和提交范围稳定 | 每个订单都需要独立生命周期与跨对象不变量 |
| 变化 | 集合规则变化集中且可由批量测试覆盖 | 小改动同时触及对象协作、外部通知和多个聚合 |
| 失败 | 事务能定位首个差异并整体回退 | 只能看到最终错误,无法说明哪些行已改变 |
| 替代 | 已与事务脚本和领域模型比较并保留撤回路径 | 仅因 ORM 习惯或类名相似而选择 |
常见误区
可验证练习
练习
本组练习围绕 9.3 表模块,要求把集合责任、事务边界和迁移信号写入同一张评审卡。
问题 1:判断边界。 两个入口都要按客户和订单状态筛选待授信订单,应该如何证明共用表模块比复制事务脚本更合适?
问题 2:处理临界样本。 订单总额恰好等于可用额度时应批准,超过一分钱时应全部拒绝,评审要保存什么证据?
问题 3:决定迁移。 表模块开始同时处理订单生命周期、客户风险策略、外部通知和跨聚合不变量,应该继续扩展吗?
名词解释
名词解释
本章出现的专业名词,用大白话再讲一遍。
- Table Module
以一张表或视图的记录集合为边界,集中提供这组记录相关业务操作的对象组织方式。
- Table Boundary
决定哪些记录可以一起读取、计算、校验和提交的集合责任边界。
- Transaction Script
由一个用例过程直接组织步骤、读取数据并调用规则的实现方式。
- Domain Model
用协作对象表达业务状态、行为和不变量的组织方式,适合复杂且频繁变化的规则。
- Row Set
在一次表模块操作中由筛选条件确定、需要共同计算或提交的一组记录。
本章小结
掌握 9.3 表模块的标志,不是记住“一个类处理一张表”,而是能在订单折扣与授信中解释“选择记录 → 计算集合规则 → 校验额度 → 一致提交”的责任链。表模块应集中稳定的集合行为,事务脚本、领域模型和应用服务分别承担不同的变化与协调责任;当单行生命周期、跨聚合不变量或外部副作用成为主要变化轴时,应明确迁移和撤回条件。
前后导航
来源与改写范围
- Martin Fowler 作者图书页:核对全书主题、教程与模式参考结构。
- Martin Fowler 模式目录:核对 9.3 表模块的公开模式名称和相关模式族。
- Pearson 出版社页面:交叉核对英文版出版信息与目录范围。