OCP 实践:插件与注册机制
开闭原则的工程化形态是把扩展点做成显式注册:启动时扫描或登记实现(插件式),运行时按标识选择。新增能力以「新增模块加一条注册条目」完成,不触碰既有调用路径。关键前提是扩展点稳定——接口与注册协议变更时,所有实现都要跟着改,所以它适合已经出现或明确预期的变化。
基本信息
| 形态 | 插件目录、注册表、依赖注入集合、语言级服务加载 |
|---|---|
| 纪律 | 扩展不改核心,注册点唯一 |
| 风险 | 隐式发现顺序与冲突需要明确约定 |
| 语言机制 | Java 用 ServiceLoader 与 META-INF/services,其它生态各有插件约定 |
落地要点
- 扩展点接口保持最小:只暴露选择器需要的成员
- 注册集中在一处,避免多个模块各自维护一半注册表
- 实现之间不得互相依赖,避免插件加载顺序影响行为
- 冲突策略明确:同标识注册两次是报错、覆盖还是取第一个
- 插件加载要有可观测日志:加载了哪些、版本多少、跳过原因
- 扩展点带版本约束:插件与核心版本不匹配时给出清晰提示
注册示意(伪代码)
注册示意(伪代码)
for plugin in scan('plugins/*.jar'):
registry.register(plugin.id(), plugin) # 注册点唯一
export_service(registry.lookup('csv')) # 按标识选择实现
# Java 生态对应机制
# ServiceLoader.load(ExportService.class)
# 提供方在 META-INF/services 下声明实现类全限定名扩展机制选择
| 机制 | 适用 | 注意 |
|---|---|---|
| 插件目录扫描 | 二进制可扩展,独立部署 | 加载顺序与安全边界 |
| 显式注册表 | 同一进程内扩展 | 冲突策略要约定 |
| 依赖注入集合 | 框架内按接口注入多实现 | 启动期失败要清晰 |
| 语言级服务加载 | 标准机制,跨模块发现 | 需要提供方声明文件 |
插件加载没有可观测日志时,问题排查等于黑盒:加载失败、被跳过、版本不匹配都只在运行结果上体现。启动时打印一份已加载清单,成本很低,省下的排查时间很多。
扩展点没有版本约束时,核心与插件会慢慢脱节:核心改了接口语义,旧插件还在按老约定工作,问题只在特定组合下出现。约定兼容范围并做启动期校验,比事后排查便宜。