第2章 组织领域逻辑
按规则复杂度、数据结构和用例变化选择事务脚本、领域模型、表模块与服务层。
学习目标
- 能区分事务脚本、领域模型、表模块和服务层各自承载的责任
- 能用 TypeScript 把订单折扣规则放进可测试的领域对象,并由服务层编排用例
- 能根据规则复杂度、复用范围和数据结构变化,判断何时保留过程式逻辑以及何时迁移
为什么第2章组织领域逻辑值得单独学习
第2章组织领域逻辑的核心不是选择一个流行框架,而是决定业务规则落在哪里、由谁维护以及如何在多个用例之间保持一致。订单折扣与授信一开始可能只有几条线性判断,但当规则开始共享、组合和频繁变化时,把所有逻辑塞进控制器或服务层过程会让每次改动都扩大回归范围。
↡把一个业务用例按步骤写成过程,由过程读取数据、判断规则并完成写回,适合规则少且变化范围局部的场景。不是低级方案;它的边界是规则是否仍能由单个用例清楚拥有。本文以 2024 年中文版公开目录限定第2章的范围,并依据 Martin Fowler 的作者图书页和模式目录独立重写;目录只决定要讲什么,案例、实验、判断题和答案均为本课程原创。
先建立直觉:问题、机制与代价
↡把跨用例共享的业务规则和不变量放进相互协作的领域对象,让对象承担行为而不是只承载数据。适合规则复杂、对象关系丰富且变化频繁的领域,但会增加对象设计、持久化映射和团队理解成本。
↡代表单张业务表或记录集,并为该表的查询与操作提供方法的组织方式,适合数据结构清晰、行为集中在表上的场景。则更贴近数据操作;
↡负责协调用例步骤、事务边界和外部端口,但不把核心业务规则全部吞进过程的应用入口。负责编排而不是替代领域模型。
对第2章组织领域逻辑,优先比较的替代路线是:简单且局部的用例使用事务脚本,行为与数据紧密对应时考虑表模块,跨用例共享且规则复杂时引入领域模型,再由服务层协调事务和端口。若实验只能显示一次调用成功,不能指出规则所有者、共享范围和变化成本,就不能据此选择组织方式。
目录单元到教学证据
第2章 组织领域逻辑
第2章组织领域逻辑要求把“业务规则放在哪里”变成可观察的架构决定:先标出规则所有者、调用者、状态不变量和事务边界,再用订单折扣与授信的变更测试验证决定。只有新增规则时能看见影响范围,组织策略才不是类名或目录结构的装饰。
2.1 抉择
2.1 抉择关注的是复杂度与变化,而不是哪一种模式听起来更先进。先统计规则分支、共享用例数、跨对象不变量和测试隔离成本;当逻辑仍然线性且局部时保留事务脚本,当同一规则被多个用例重复实现时再考虑领域模型或表模块。
2.2 服务层
2.2 服务层把请求转换为一次用例:加载对象、调用领域行为、协调外部端口并提交事务。服务层不应成为所有规则的垃圾桶;如果它开始计算折扣、修改授信不变量并复制到多个入口,就应该把稳定行为移回领域模型或明确的策略对象。
专属代码案例:订单折扣与授信
把订单折扣与授信切成“用例入口 → 领域行为 → 授信端口 → 事务提交 → 可观察结果”五个观察点。第2章组织领域逻辑的设计草案必须写出谁拥有状态、谁作出业务决定、失败怎样传播,以及复杂度、事务脚本、领域模型、表模块和服务层中哪个指标最先提示当前方案不再适用。
设计记录采用五个可换行字段:单元键为 poeaa24-chapter-02-organizing-domain-logic;模式族为 domain;裁决是“规则局部则保留事务脚本,规则共享且复杂则由领域模型拥有,服务层只编排”;观测项包括复杂度、事务脚本、领域模型、表模块、服务层;拒绝条件是“同一折扣规则在多个服务入口复制且没有单一不变量所有者”。
先预测:如果新增“会员等级折扣”并要求授信额度也参与判断,哪一段代码会变化?先记录事务脚本、领域模型和服务层各自的修改范围,再阅读实现;验证时应能说明领域对象保护了什么不变量,以及服务层为何没有吞掉业务规则。
type OrderLine = { sku: string; unitCents: number; quantity: number };
type CreditDecision =
| { approved: true; remainingCents: number }
| { approved: false; reason: "limit" | "blocked" };
interface CreditPort {
authorize(customerId: string, amountCents: number): CreditDecision;
}
class Order {
constructor(
readonly id: string,
readonly customerId: string,
private readonly lines: OrderLine[],
) {}
totalCents(): number {
return this.lines.reduce(
(sum, line) => sum + line.unitCents * line.quantity,
0,
);
}
totalAfterDiscountCents(discountRate: number): number {
return Math.round(this.totalCents() * (1 - discountRate));
}
}
function placeOrder(
order: Order,
discountRate: number,
credit: CreditPort,
): "accepted" | "rejected" {
const amount = order.totalAfterDiscountCents(discountRate);
const decision = credit.authorize(order.customerId, amount);
return decision.approved ? "accepted" : "rejected";
}这段代码让订单对象拥有金额计算,让服务入口只协调折扣输入与授信端口。真实系统还应把折扣资格、不变量和持久化提交放进可测试的领域边界;如果 placeOrder 开始复制会员、地区和授信规则,就应把这些行为提炼为领域策略,而不是继续扩张服务层。
三种策略解剖
组织领域逻辑没有固定的升级顺序。事务脚本适合简单用例,表模块适合行为与记录集紧密对应的场景,领域模型适合跨对象不变量与共享规则;服务层横跨这些方案,负责协调用例而不是替它们拥有全部业务知识。下面的图把策略的适用条件与迁移代价放在同一张证据图中。
当规则分支、共享用例和跨对象不变量同时增长时,应比较迁移成本,而不是机械地把所有代码改成领域对象;小而稳定的规则留在事务脚本中,反而更容易测试和维护。
组织策略决策图
先看规则是否局部,再看是否存在跨对象不变量与多用例复用,最后检查服务层是否仍然只是编排。若没有真实复杂度证据,不要为了“未来灵活性”引入完整领域模型;若已经出现复制规则和不变量分裂,则应停止继续堆叠过程代码。
选择与拒绝矩阵
| 评审问题 | 选择组织策略的证据 | 应拒绝当前方案的信号 |
|---|---|---|
| 责任 | 规则所有者、服务层编排者和持久化边界清晰 | 控制器、服务层和表模块都能改写同一不变量 |
| 变化 | 规则变化只影响对应策略和测试 | 新增一个折扣条件要复制多个用例过程 |
| 复用 | 共享规则只有一个可调用的领域入口 | 每个入口各自计算金额、授信与资格 |
| 替代 | 已比较事务脚本、表模块和领域模型的真实成本 | 因团队偏好或框架示例直接选择复杂方案 |
常见误区
可验证练习
练习
本组练习覆盖 2.1 抉择和 2.2 服务层,并要求把复杂度、事务脚本、领域模型、表模块和服务层映射到订单折扣与授信。
问题 1:选择起点。 一个订单用例只有两条折扣规则,没有跨对象不变量,也没有第二个调用入口,应从哪种组织方式开始?
问题 2:识别服务层边界。 多个入口都需要相同的会员折扣和授信规则,服务层开始出现重复计算,应该怎么改?
问题 3:比较表模块。 订单查询主要是按状态筛选记录,但新增规则会跨订单、客户和授信额度,继续让 OrderTable 承担全部行为是否合适?
名词解释
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 事务脚本
把一个业务用例按步骤写成过程,由过程读取数据、判断规则并完成写回的组织方式。
- 领域模型
把跨用例共享的业务规则和不变量放进相互协作的领域对象的组织方式。
- 表模块
代表单张业务表或记录集,并为该表的查询与操作提供方法的组织方式。
- 服务层
负责协调用例步骤、事务边界和外部端口,但不替代领域对象拥有核心业务规则的应用入口。
本章小结
掌握第2章组织领域逻辑的标志不是记住某种模式名称,而是能在订单折扣与授信中解释“用例入口 → 领域行为 → 授信端口 → 事务提交 → 可观察结果”的责任链。学习者应利用复杂度、事务脚本、领域模型、表模块和服务层作出可证伪的选择,并在规则复制或不变量所有权分裂时明确拒绝当前实现。
前后导航
来源与改写范围
- Martin Fowler 作者图书页:核对全书主题、教程与模式参考结构。
- Martin Fowler 模式目录:核对模式名称、所属模式族和作者公开摘要。
- Pearson 出版社页面:交叉核对英文版出版信息与目录范围。