一、为什么需要注册表
你正在写一个序列化框架,支持 JSON、XML、Protobuf 三种格式。最初用 switch-case 分发:收到请求看 Content-Type,匹配到 “json” 就调 JSON 编解码器,匹配到 “xml” 就调 XML 编解码器。后来要加 CSV 支持,你得改 switch-case、改类型定义、改导入语句——每加一种格式就要改三四个文件。
更糟的是,这些编解码器散落在不同模块里,新同事想加一种格式,根本不知道要改哪些地方。switch-case 里的字符串拼写错误也不会在编译时被发现,只有运行时才会抛出「未知格式」的异常。
注册表把「名字 → 实现」的映射集中管理。新编解码器只需在启动时调用 registry.register("csv", csvCodec),消费者通过 registry.get("csv") 查找。添加新实现不需要修改任何现有代码——这就是开放-封闭原则的体现。
二、现实类比
酒店前台。客人用名字登记入住,任何人都可以问前台「Alice 住哪个房间?」前台不关心房间里发生了什么——它只负责名字到房间号的映射。新客人来了就登记,退房了就注销,前台不需要重新装修大堂。
三、核心思想
注册表是名称(字符串)到实现(函数、类、工厂)的中心映射。生产者在启动时自注册——通常通过装饰器、宏或 init 函数。消费者在运行时按名称查找实现,消除编译时耦合。这实现了插件架构,新功能无需修改现有代码即可添加。
| 属性 | 值 |
|---|---|
| 注册 | O(1) 哈希表插入 |
| 查找 | O(1) 哈希表查找 |
| 耦合度 | 生产者和消费者之间零编译时依赖 |
| 可扩展性 | 无需修改现有代码即可添加新实现 |
四、变体与对比
| 模式 | 关系 | 区别 |
|---|---|---|
| 中间件链(Middleware Chain) | 中间件处理器通常将自身注册到注册表中 | 注册表管发现,中间件管执行流 |
| 依赖图(Dependency Graph) | 注册表可以追踪已注册组件之间的依赖关系 | 注册表管名字映射,依赖图管顺序约束 |
| 一致性哈希(Consistent Hashing) | 服务注册表为一致性哈希提供可用节点列表 | 注册表是查找表,一致性哈希是分布策略 |
| 依赖注入(DI) | DI 容器内部通常使用一个注册表 | 注册表是消费者主动拉取,DI 是框架推送 |
注册表与依赖注入的区别在于控制流方向:注册表中消费者主动按名称拉取实现(registry.get("json")),DI 中框架将依赖推送到消费者中(构造函数参数或注解)。注册表更简单但将消费者耦合到字符串名称,DI 进一步解耦但增加了框架复杂性。
五、多语言实现
5.1 Go 实现
package registry
import ( "fmt" "sync")
// Factory 是创建组件实例的工厂函数type Factory func(args ...any) any
// Registry 是线程安全的名称 → 工厂映射type Registry struct { mu sync.RWMutex entries map[string]Factory}
func New() *Registry { return &Registry{entries: make(map[string]Factory)}}
// Register 注册一个工厂函数,重复名称会报错func (r *Registry) Register(name string, factory Factory) error { r.mu.Lock() defer r.mu.Unlock() if _, ok := r.entries[name]; ok { return fmt.Errorf("%q 已注册", name) } r.entries[name] = factory return nil}
// Create 按名称查找工厂并创建实例func (r *Registry) Create(name string, args ...any) (any, error) { r.mu.RLock() factory, ok := r.entries[name] r.mu.RUnlock() if !ok { return nil, fmt.Errorf("%q 未注册", name) } return factory(args...), nil}
// List 返回所有已注册的名称func (r *Registry) List() []string { r.mu.RLock() defer r.mu.RUnlock() names := make([]string, 0, len(r.entries)) for name := range r.entries { names = append(names, name) } return names}Go 版本加了 sync.RWMutex 保证并发安全。注册时用写锁,查找和列举时用读锁,允许并发读取。重复注册直接报错而不是静默覆盖——这是防止 bug 的关键设计。
5.2 TypeScript 实现
// 工厂函数类型type Factory<T> = (...args: any[]) => T;
class Registry<T> { private entries = new Map<string, Factory<T>>();
// 注册工厂函数,重复名称抛错 register(name: string, factory: Factory<T>): void { if (this.entries.has(name)) { throw new Error(`"${name}" 已注册`); } this.entries.set(name, factory); }
// 按名称查找并创建实例 create(name: string, ...args: any[]): T { const factory = this.entries.get(name); if (!factory) { throw new Error(`"${name}" 未注册`); } return factory(...args); }
// 列出所有已注册的名称 list(): string[] { return [...this.entries.keys()]; }}TypeScript 版本更简洁,利用 Map 的原生方法。如果需要装饰器自注册,可以扩展一个 decorator 方法,让类通过 @registry.decorator("name") 自动注册。
六、生产验证
- TensorFlow — op.h#L258-L290 中的
REGISTER_OP宏将新操作注册到全局OpRegistry。每个 op 定义名称、输入、输出和形状函数,运行时按名称查找,新 op 可以在不修改图执行器的情况下添加。 - gRPC-Go — server.go#L154-L170 中的
RegisterService将服务描述添加到服务器的服务映射中。RPC 到达时,服务器在注册表中查找方法分派到正确的处理程序,服务在 init 期间自注册。 - Docker — 驱动注册表:存储、网络和日志驱动在守护进程启动时注册,按名称查找和加载。
七、小结
何时使用:
- 插件系统——按名称加载和发现插件,无需编译时耦合
- 序列化编解码器——注册 JSON、XML、Protobuf 编解码器,按内容类型查找
- 命令/处理器分派——CLI 命令、RPC 方法、事件处理器自注册
- ML 框架操作——TensorFlow、PyTorch 注册可组合到图中的算子
何时不用:
- 实现数量少且固定——只有 2-3 个已知实现时,switch/match 更简单直接
- 类型安全至关重要——基于字符串的查找失去编译时类型检查,改用依赖注入或泛型
- 初始化顺序重要——注册表通常是无序的,如果初始化顺序有依赖,需要显式排序
八、参考资料
- TensorFlow REGISTER_OP - C++ 宏实现的自注册机制
- gRPC-Go 服务注册 - RPC 方法分派的注册表实现
- pytest fixture 注册 - Python 测试框架的装饰器自注册
- Babel 插件系统 - 转换器按访问者模式名称自注册
- Docker 驱动注册表 - 守护进程启动时的驱动注册机制
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






