18.9 插件
在配置期而非编译期连接实现类,让部署环境选择符合接口的具体行为。
学习目标
- 能解释插件如何把实现选择推迟到配置期,并画出核心、接口、配置与实现之间的依赖方向
- 能编写 TypeScript 插件注册与加载代码,验证实现遵守接口并让加载失败保持可见
- 能根据变化轴、版本约束、测试隔离和回退成本,判断何时采用或拒绝插件
为什么 18.9 插件 值得单独学习
在配置期而非编译期连接实现类,让部署环境选择符合接口的具体行为。18.9 插件的核心不是套用某个框架 API,而是回答:如何把可替换实现隔离在稳定契约之后,让核心代码依赖语义而不是某个部署环境。在订单系统基础设施替换中,如果无法说清责任、状态和失败由谁承担,即使配置能加载,架构决定也没有完成。
↡在配置期或运行期选择并装配、且遵守共同契约的可替换实现。 的价值不是“任何东西都可以动态加载”,而是把明确的变化轴放进受控边界。本页以 2024 年中文版公开目录限定 18.9 插件 的范围,并依据 Martin Fowler 的作者图书页和模式目录独立重写。它不复现原书正文、插图或代码;目录只决定“要讲什么”,这里的案例、实验、判断题和答案均为本课程原创。
先建立直觉:问题、机制与代价
18.9 插件面对的具体压力是“实现替换频率、部署差异、版本约束与测试隔离成本”。它采用的机制可以概括为:核心只依赖接口,配置在装配阶段选择实现,插件实现通过注册表接入。机制带来的收益必须与新增间接层、加载失败、兼容性检查和回滚成本同时记录,否则学习者只会得到一个没有拒绝条件的模式名称。
对 18.9 插件,优先比较的替代路线是:直接调用稳定实现、策略对象或显式依赖注入。只有在变化轴确实跨越部署边界,且每个实现都能遵守 ↡插件与核心之间必须共同遵守的输入、输出、错误和版本约束。 时,才值得引入插件;若实验只能显示结果而不能指出配置从何处选择实现,就不能据此选择 18.9 插件。
目录单元到教学证据
18.9 插件
在 18.9 插件 的学习边界里,18.9 插件不是待背诵的目录词,而是用来检查“核心代码”是否把实现选择交给正确的装配边界。对订单系统基础设施替换,学习者要记录配置、版本和依赖方向的可观察变化,并说明它何时支持或否定 18.9 插件。
专属设计案例:订单系统税费基础设施替换
把订单系统税费基础设施替换切成“核心调用者 → 税费接口 → 配置注册表 → 插件实现 → 结果”五个观察点。18.9 插件的设计草案必须写出谁拥有配置、谁验证版本、失败怎样传播,以及依赖方向、对象语义、配置、测试隔离、表示转换中哪个指标最先提示当前方案不再适用。
设计记录采用五个可换行字段:单元键为 poeaa24-pattern-49-plugin;模式族为 base;裁决是“能通过配置切换插件、拒绝不兼容实现,并记录加载失败与版本约束”;观测项包括依赖方向、对象语义、配置、测试隔离、表示转换;拒绝条件是“没有真实变化轴,或插件契约无法被自动验证”。
配置不是生产框架语法,而是一张评审卡。对 18.9 插件的任何实现都要能把运行证据重新映射到这张卡;如果更换 ORM、Web 框架或部署平台后无法回答同一组问题,说明决定依赖的是工具偶然行为而不是模式语义。
结构解剖
选择与拒绝矩阵
| 评审问题 | 选择 18.9 插件 的证据 | 应拒绝或改用其他方案的信号 |
|---|---|---|
| 责任 | 核心只依赖稳定接口,装配边界拥有实现选择 | 业务代码读取配置并自行实例化多个实现 |
| 变化 | 新实现可独立发布且不改核心调用者 | 每加入一个实现都要修改核心分支 |
| 失败 | 缺少插件、版本不兼容和运行错误可分别观测 | 加载失败被静默回退成错误实现 |
| 替代 | 已与策略对象、依赖注入等方案比较并保留回滚路径 | 因为“以后可能替换”就预先增加全局注册表 |
代码实践:安全加载一个税费插件
订单系统可以在部署配置中选择税费实现,但核心调用者只面对 TaxCalculator。loadPlugin 的责任是检查名称和版本约束;它不能把缺少插件伪装成零税率。↡插件能被核心安全调用所需共同遵守的接口、错误分类和版本边界。必须由测试和启动检查共同守住。
type Order = {
country: string;
subtotalCents: number;
};
interface TaxCalculator {
readonly version: string;
compute(order: Order): number;
}
type AvailablePlugin = {
key: string;
implementation: TaxCalculator;
};
class ChinaTax implements TaxCalculator {
readonly version = "1";
compute(order: Order): number {
return Math.round(order.subtotalCents * 0.13);
}
}
class USTax implements TaxCalculator {
readonly version = "1";
compute(order: Order): number {
return Math.round(order.subtotalCents * 0.08);
}
}
function loadPlugin(
key: string,
requiredVersion: string,
available: AvailablePlugin[],
): TaxCalculator {
const entry = available.find((candidate) => candidate.key === key);
if (!entry) throw new Error(`plugin not found: ${key}`);
if (entry.implementation.version !== requiredVersion) {
throw new Error(`incompatible plugin version: ${key}`);
}
return entry.implementation;
}
const configuredTax = loadPlugin("cn-tax", "1", [
{ key: "cn-tax", implementation: new ChinaTax() },
{ key: "us-tax", implementation: new USTax() },
]);
const order = { country: "CN", subtotalCents: 10000 };
const taxCents = configuredTax.compute(order);这段代码把实现选择留在装配边界,把业务计算留在插件契约内。启动时找不到插件或版本不兼容会明确失败;测试则可以用一个记录调用的假实现验证核心调用者,而不必连接真实税务服务。
常见误区
可验证练习
练习
问题 1:识别边界。 核心订单服务直接读取环境变量并实例化 ChinaTax,这仍然是插件结构吗?
问题 2:处理兼容性。 配置选择了版本 2 的插件,而核心只支持版本 1。应该返回零税率还是继续启动?
问题 3:决定是否使用。 只有一个实现、没有部署差异,团队仍想引入插件以“方便未来替换”。应如何评审?
名词解释
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 插件
在配置期或运行期选择并装配、且遵守共同契约的可替换实现。
- 兼容契约
插件与核心共同遵守的输入、输出、错误和版本约束。
- 装配边界
解析配置、验证版本并把具体实现交给核心调用者之前的启动边界。
本章小结
掌握 18.9 插件的标志不是记住定义,而是能在订单系统税费基础设施替换中解释“核心调用者 → 税费接口 → 配置注册表 → 插件实现 → 结果”的责任链,利用依赖方向、对象语义、配置、测试隔离、表示转换作出可证伪的选择,并在没有真实变化轴或兼容契约无法验证时明确拒绝插件化。
前后导航
来源与改写范围
- Martin Fowler 作者图书页:核对全书主题、教程与模式参考结构。
- Martin Fowler 模式目录:核对模式名称、所属模式族和作者公开摘要。
- Pearson 出版社页面:交叉核对英文版出版信息与目录范围。