18.5 注册表

提供众所周知的对象来查找公共对象与服务,以显式入口管理受控的全局访问。

学习目标

  • 能解释注册表如何提供受控的共享查找点,并说清它与依赖注入、裸全局变量的区别
  • 能编写带键类型、作用域和测试替换能力的 TypeScript Registry,拒绝未注册或错误类型的对象
  • 能根据生命周期、访问范围和隐式依赖的证据,判断何时采用或拒绝注册表

为什么 18.5 注册表 值得单独学习

提供众所周知的对象来查找公共对象与服务,以显式入口管理受控的全局访问。 18.5 注册表 的核心不是套用某个框架 API,而是回答:如何隔离变化、身份或特殊行为,让上层模式依赖稳定语义而非具体基础设施。在 订单系统基础设施替换 中,如果无法说清责任、状态和失败由谁承担,即使正常请求能够返回,架构决定也没有完成。

不是“把任何东西放进全局变量”的许可,而是一个需要明确作用域、键类型、生命周期和替换策略的边界。本页以 2024 年中文版公开目录限定 18.5 注册表 的范围,并依据 Martin Fowler 的作者图书页和模式目录独立重写。它不复现原书正文、插图或代码;目录只决定“要讲什么”,这里的案例、实验、判断题和答案均为本课程原创。

先建立直觉:问题、机制与代价

18.5 注册表 面对的具体压力是“调用方数量、实现变化率、替换范围与测试隔离成本”。它采用的机制可以概括为:提供众所周知的对象来查找公共对象与服务,以显式入口管理受控的全局访问。 机制带来的收益必须与新增间接层、同步责任或迁移成本同时记录,否则学习者只会得到一个没有拒绝条件的模式名称。

对 18.5 注册表,优先比较的替代路线是:仅在确有变化轴或语义约束时引入网关、映射器、接口、值对象等构件。本页的通过条件是“能限制注册表作用域和键类型,替换测试服务,并说明何时依赖注入更清楚”;若实验只能显示结果而不能指出 从何处越界,就不能据此选择 18.5 注册表。

目录单元到教学证据

18.5 注册表

18.5 注册表 的学习边界里,18.5 注册表 不是待背诵的目录词,而是用来检查“调用者”是否把责任交给正确对象。对 订单系统基础设施替换,学习者要记录 依赖方向 的可观察变化,并说明它何时支持或否定 18.5 注册表。

专属设计案例:订单系统基础设施替换

把 订单系统基础设施替换 切成“调用者 → 抽象边界 → 适配机制 → 协作者 → 结果”五个观察点。18.5 注册表 的设计草案必须写出谁拥有状态、谁作出业务决定、失败怎样传播,以及 依赖方向、对象语义、配置、测试隔离、表示转换 中哪个指标最先提示当前方案不再适用。

设计记录采用五个可换行字段:单元键为 poeaa24-pattern-45-registry;模式族为 base;裁决是“能限制注册表作用域和键类型,替换测试服务,并说明何时依赖注入更清楚”;观测项包括 依赖方向、对象语义、配置、测试隔离、表示转换;拒绝条件是“依赖方向 超过团队为 18.5 注册表 设定的边界”。

配置不是生产框架语法,而是一张评审卡。对 18.5 注册表 的任何实现都要能把运行证据重新映射到这张卡;如果更换 ORM、Web 框架或部署平台后无法回答同一组问题,说明决定依赖的是工具偶然行为而不是模式语义。

三步交互实验

Registry:全局查找点,按键取对象任意调用者Registry.get("db")无需传递引用Registry"db" → Connection"cfg" → Config"log" → Logger注意:本质是受控的全局变量测试时需可替换避免滥用• 适用:确实需要全局共享且生命周期与应用一致的对象• 常见实例:Identity Map、数据库连接池、配置中心Registry 提供全局查找点,按键获取共享对象
Registry 是全局可访问的查找点,通过键获取共享对象, 替代裸全局变量。本质是受控的全局状态,测试时需可替换。

选择与拒绝矩阵

评审问题选择 18.5 注册表 的证据应拒绝或改用其他方案的信号
责任调用者 到 结果 的所有者清晰抽象边界 可以绕过边界直接改写状态
变化依赖方向 的变化被局部吸收一次小改动同时触及 对象语义、配置、测试隔离
失败故障能在 结果 前被识别并回退只能看到最终错误,无法定位 依赖方向 的首个异常
替代已与同族候选比较并保留撤回路径因框架内置或团队习惯而跳过问题分析

代码实践:限制键、作用域与替换能力

注册表适合生命周期与应用一致、确实需要共享的对象;它不应成为隐藏所有依赖的服务定位器。和类型约束是最小安全边界。

type Services = {
  clock: { now(): number };
  payment: { charge(orderId: string): Promise<"ok" | "retry"> };
};
 
class AppRegistry {
  constructor(private readonly services: Services) {}
 
  get<K extends keyof Services>(key: K): Services[K] {
    return this.services[key];
  }
}
 
const registry = new AppRegistry({
  clock: { now: () => Date.now() },
  payment: new StripePayment(),
});
 
async function settleOrder(orderId: string) {
  return registry.get("payment").charge(orderId);
}

测试可以把 payment 换成 Fake,但更深的业务对象仍应优先通过参数或构造器声明依赖。若任何调用者都能注册同名键、覆盖生产服务或跨租户读取对象,注册表的“受控”边界已经消失。

常见误区

可验证练习

练习

问题 1:找出隐式依赖。 一个函数内部调用 Registry.get("payment"),但签名没有任何参数。请说明一个风险和一个改进方向。

问题 2:设计测试替换。 生产注册表中的 payment 使用 Stripe,测试需要稳定返回 retry。应替换哪里,如何防止 Fake 泄漏到其他测试?

问题 3:选择方案。 一个对象只被单个 OrderService 使用,生命周期也随请求变化。为什么不应放进全局 Registry?

名词解释

名词解释

本章出现的专业名词,用大白话再讲一遍。

注册表

提供受控查找入口,通过键访问共享对象或服务的对象。

隐式依赖

调用者没有声明依赖,却通过全局名称或查找入口取得对象的关系。

可替换性

在测试或装配阶段用另一实现替换已注册对象而不改调用者的能力。

作用域

对象可被哪些请求、租户或模块访问,以及它应存活多久的边界。

本章小结

掌握 18.5 注册表 的标志不是记住定义,而是能在 订单系统基础设施替换 中解释“调用者 → 抽象边界 → 适配机制 → 协作者 → 结果”的责任链,利用 依赖方向、对象语义、配置、测试隔离、表示转换 作出可证伪的选择,并在出现“没有变化轴也预先增加全局入口和间接层”时明确拒绝当前实现。

前后导航

来源与改写范围

资料与写作方式声明

本章以Martin Fowler《企业应用架构模式》与公开模式目录权威目录界定学习范围,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

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

讨论

评论区加载中…