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 框架或部署平台后无法回答同一组问题,说明决定依赖的是工具偶然行为而不是模式语义。

结构解剖

Special Case:用子类封装特殊情况CustomergetBillingPlan()RealCustomer正常计费逻辑NullCustomer(特例)返回默认值,不抛异常• 调用者:customer.getBillingPlan() — 无需 if (customer == null)• 多态分派自动处理边界,特殊情况的行为集中在一处特例子类封装边界行为,调用者无需到处写 null 检查
Special Case 用子类封装特殊情况的行为(如 NullCustomer), 调用者无需到处写 null 检查,多态分派自动处理边界。

选择与拒绝矩阵

评审问题选择 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《企业应用架构模式》与公开模式目录权威目录界定学习范围,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

讨论

评论区加载中…