mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
1393 字
4 分钟
注册表(Registry)
2026-06-13

一、为什么需要注册表#

你正在写一个序列化框架,支持 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 函数。消费者在运行时按名称查找实现,消除编译时耦合。这实现了插件架构,新功能无需修改现有代码即可添加。

flowchart LR subgraph 注册阶段 J[JsonCodec] -->|register "json"| R[Registry] X[XmlCodec] -->|register "xml"| R C[CsvCodec] -->|register "csv"| R end subgraph 查找阶段 U[Consumer] -->|get "json"| R R -->|返回| J2[JsonCodec] end
属性
注册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") 自动注册。

六、生产验证#

  • TensorFlowop.h#L258-L290 中的 REGISTER_OP 宏将新操作注册到全局 OpRegistry。每个 op 定义名称、输入、输出和形状函数,运行时按名称查找,新 op 可以在不修改图执行器的情况下添加。
  • gRPC-Goserver.go#L154-L170 中的 RegisterService 将服务描述添加到服务器的服务映射中。RPC 到达时,服务器在注册表中查找方法分派到正确的处理程序,服务在 init 期间自注册。
  • Docker — 驱动注册表:存储、网络和日志驱动在守护进程启动时注册,按名称查找和加载。

七、小结#

何时使用:

  • 插件系统——按名称加载和发现插件,无需编译时耦合
  • 序列化编解码器——注册 JSON、XML、Protobuf 编解码器,按内容类型查找
  • 命令/处理器分派——CLI 命令、RPC 方法、事件处理器自注册
  • ML 框架操作——TensorFlow、PyTorch 注册可组合到图中的算子

何时不用:

  • 实现数量少且固定——只有 2-3 个已知实现时,switch/match 更简单直接
  • 类型安全至关重要——基于字符串的查找失去编译时类型检查,改用依赖注入或泛型
  • 初始化顺序重要——注册表通常是无序的,如果初始化顺序有依赖,需要显式排序

八、参考资料#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

注册表(Registry)
https://blog.souloss.com/posts/programming/system-patterns/system-patterns-registry/
作者
Tsukimi
发布于
2026-06-13
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时