18.8 特殊情况
用提供特殊行为的子类或对象表示缺失、未知等情况,消除调用处重复条件分支。
学习目标
- 能区分可建模的缺失/未知情况与必须暴露的真实错误,并说明特殊对象的行为契约
- 能编写 TypeScript Special Case 实现,消除重复空值分支,同时保留可观测的边界语义
- 能根据默认值风险、调用者数量和错误可见性,判断何时采用或拒绝特殊情况
为什么 18.8 特殊情况 值得单独学习
用提供特殊行为的子类或对象表示缺失、未知等情况,消除调用处重复条件分支。 18.8 特殊情况 的核心不是套用某个框架 API,而是回答:如何隔离变化、身份或特殊行为,让上层模式依赖稳定语义而非具体基础设施。在 订单系统基础设施替换 中,如果无法说清责任、状态和失败由谁承担,即使正常请求能够返回,架构决定也没有完成。
↡用一个遵守正常接口的对象表示可预期缺失、未知或默认情况,从而集中边界行为的模式。的价值不是让所有异常都返回默认值,而是把“业务上可接受的缺失”变成可命名、可测试的行为。本页以 2024 年中文版公开目录限定 18.8 特殊情况 的范围,并依据 Martin Fowler 的作者图书页和模式目录独立重写。它不复现原书正文、插图或代码;目录只决定“要讲什么”,这里的案例、实验、判断题和答案均为本课程原创。
先建立直觉:问题、机制与代价
18.8 特殊情况 面对的具体压力是“调用方数量、实现变化率、替换范围与测试隔离成本”。它采用的机制可以概括为:用提供特殊行为的子类或对象表示缺失、未知等情况,消除调用处重复条件分支。 机制带来的收益必须与新增间接层、同步责任或迁移成本同时记录,否则学习者只会得到一个没有拒绝条件的模式名称。
对 18.8 特殊情况,优先比较的替代路线是:仅在确有变化轴或语义约束时引入网关、映射器、接口、值对象等构件。本页的通过条件是“能让特殊对象遵守同一接口,删除散落空值判断,并验证它不会掩盖真正错误”;若实验只能显示结果而不能指出 ↡调用者把对象交给同一接口、由多态分派选择正常或特殊行为的关系。从何处越界,就不能据此选择 18.8 特殊情况。
目录单元到教学证据
18.8 特殊情况
在 18.8 特殊情况 的学习边界里,18.8 特殊情况 不是待背诵的目录词,而是用来检查“调用者”是否把责任交给正确对象。对 订单系统基础设施替换,学习者要记录 依赖方向 的可观察变化,并说明它何时支持或否定 18.8 特殊情况。
专属设计案例:订单系统基础设施替换
把 订单系统基础设施替换 切成“调用者 → 抽象边界 → 适配机制 → 协作者 → 结果”五个观察点。18.8 特殊情况 的设计草案必须写出谁拥有状态、谁作出业务决定、失败怎样传播,以及 依赖方向、对象语义、配置、测试隔离、表示转换 中哪个指标最先提示当前方案不再适用。
设计记录采用五个可换行字段:单元键为 poeaa24-pattern-48-special-case;模式族为 base;裁决是“能让特殊对象遵守同一接口,删除散落空值判断,并验证它不会掩盖真正错误”;观测项包括 依赖方向、对象语义、配置、测试隔离、表示转换;拒绝条件是“依赖方向 超过团队为 18.8 特殊情况 设定的边界”。
配置不是生产框架语法,而是一张评审卡。对 18.8 特殊情况 的任何实现都要能把运行证据重新映射到这张卡;如果更换 ORM、Web 框架或部署平台后无法回答同一组问题,说明决定依赖的是工具偶然行为而不是模式语义。
结构解剖
选择与拒绝矩阵
| 评审问题 | 选择 18.8 特殊情况 的证据 | 应拒绝或改用其他方案的信号 |
|---|---|---|
| 责任 | 调用者 到 结果 的所有者清晰 | 抽象边界 可以绕过边界直接改写状态 |
| 变化 | 依赖方向 的变化被局部吸收 | 一次小改动同时触及 对象语义、配置、测试隔离 |
| 失败 | 故障能在 结果 前被识别并回退 | 只能看到最终错误,无法定位 依赖方向 的首个异常 |
| 替代 | 已与同族候选比较并保留撤回路径 | 因框架内置或团队习惯而跳过问题分析 |
代码实践:让可接受缺失遵守同一契约
订单查不到可选账单资料时,可以使用 NoBillingPlan;但付款失败、权限拒绝和数据损坏不能被伪装成“没有方案”。↡特殊对象在边界处返回的、业务明确允许继续处理的结果,而不是任意错误的默认值。必须有文档、指标和测试。
type BillingPlan = {
name: string;
monthlyCents: number;
isAvailable(): boolean;
};
class NoBillingPlan implements BillingPlan {
name = "none";
monthlyCents = 0;
isAvailable(): boolean {
return false;
}
}
class Customer {
constructor(private readonly plan: BillingPlan | null) {}
billingPlan(): BillingPlan {
return this.plan ?? new NoBillingPlan();
}
}
function showPlan(customer: Customer) {
const plan = customer.billingPlan();
return plan.isAvailable() ? plan.name : "未选择方案";
}这段代码只把“没有可选方案”建模为特殊情况;如果数据库连接失败,调用者应收到错误并记录告警,而不是得到 NoBillingPlan。测试要同时覆盖真实方案、特殊方案和不可隐藏的异常。
常见误区
可验证练习
练习
问题 1:区分两种情况。 findBillingPlan 查不到客户方案时应返回 NoBillingPlan,但数据库连接超时也走同一分支。应如何改?
问题 2:检查契约。 NoBillingPlan.isAvailable() 返回 false,但 monthlyCents 抛异常。为什么这可能违反多态契约?
问题 3:决定是否使用。 一个缺失值极少发生,且一旦发生就表示数据损坏。应采用 Special Case 吗?
名词解释
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 特殊情况
用遵守正常接口的对象表示可预期缺失、未知或默认情况的模式。
- 多态契约
正常对象与特殊对象共同遵守的输入、结果和行为约束。
- 安全默认值
业务明确允许继续处理、且不会掩盖真正错误的特殊结果。
- 可接受缺失
业务语义允许出现、可以被记录并安全处理的不存在或未选择状态。
本章小结
掌握 18.8 特殊情况 的标志不是记住定义,而是能在 订单系统基础设施替换 中解释“调用者 → 抽象边界 → 适配机制 → 协作者 → 结果”的责任链,利用 依赖方向、对象语义、配置、测试隔离、表示转换 作出可证伪的选择,并在出现“没有变化轴也预先增加全局入口和间接层”时明确拒绝当前实现。
前后导航
来源与改写范围
- Martin Fowler 作者图书页:核对全书主题、教程与模式参考结构。
- Martin Fowler 模式目录:核对模式名称、所属模式族和作者公开摘要。
- Pearson 出版社页面:交叉核对英文版出版信息与目录范围。