本篇要回答的问题:架构的本质是业务的正交分解,那么如何应对需求的变化?著名的开闭原则(OCP)背后真正的架构哲学是什么,它为什么不只是 OOP 的编程技巧?
上一讲谈全局性功能,点出架构分解的两大难题——需求的交织与需求的易变。本讲正面回答后者:怎么应对变化。许式伟把架构设计的工具收敛为两个:组合(用小业务拼大业务)与应对变化(开闭原则)。本讲就是把 OCP 从一句口号还原成"架构治理的根本哲学"。
开闭原则(OCP)是什么
OCP 由 Bertrand Meyer 在 1988 年《面向对象软件构造》中首次提出:
软件实体(模块/类/函数)应对功能扩展开放,对修改封闭。
| 维度 | 说明 |
|---|---|
| 核心主张 | 通过扩展行为应对变化,而非修改现有代码 |
| 背后哲学 | 推崇模块业务的确定性:可改 Bug,但不轻易调整业务范畴 |
| 与正交分解的关系 | 一脉相承——与其改业务,不如实现一个新业务模块 |
| "只读"思想 | 模块一经设计就不改;要变就废弃它,转而实现新模块 |
关键判断:OCP 鼓励写"只读"的业务模块。这种只读思想和 Git 源码版本管理、容器服务治理同源——都靠"只读"来降低系统治理难度,让可复用模块越积越多。
CPU 背后的架构思维:OCP 不止于 OOP
把 OCP 误解成 OOP 编程思想太狭隘。它是信息技术架构的基本原则。冯·诺依曼体系的 CPU 设计就是 OCP 的完美体现:
| 稳定点(封闭) | 变化点(开放) | 带来的能力 |
|---|---|---|
| 指令集稳定 | 指令序列多变 → 发明了软件 | 解决一切可计算问题 |
| 计算稳定 | 数据交换多变 → 定义 I/O 规范 | 适应不断革新的交互技术 |
| CPU 不变 | 缺页中断把外存/文件系统格式解耦给操作系统 | 中断 = CPU 引入的回调函数 |
洞见:我们从不修改 CPU,却支撑了多姿多彩的信息世界。这与面向对象无关,完全是开闭原则的威力。
插件机制:让代码不变而业务能变
OCP 关注的是模块而非最终软件。对"软件"这个大模块若坚持业务不变就是放弃进步。让代码不变、业务范畴却能适应变化,靠的就是插件机制(DOM API 二次开发、生态型软件如 Office / Visual Studio)。
完整插件机制的三个部分:
| 组成 | 作用 | 难度 |
|---|---|---|
| 软件能力暴露(DOM API) | 插件调用已实现的功能 | 最基础 |
| 插件加载机制 | 基于文件系统目录或 Windows 注册表 | 中等 |
| 事件监听 | 没有事件,插件无法介入业务 | 关键且最难 |
事件设计追求少而精,一般分三类:
| 事件类别 | 典型例子 | 说明 |
|---|---|---|
| 界面操作类 | 菜单/按钮点击 | 鼠标键盘太底层,是双刃剑,一般不直接暴露 |
| 数据变更类 | onSelectionChanged、onDataChanged |
对应 MVC 中 Model 层发出的变更通知 |
| 业务流程类 | Office 打开文件前/后 | 发生在业务流中间或完成后 |
关键判断:插件机制可大可小。Go 的
image包(import _ "image/jpeg")放弃了自动加载机制、改为手工加载,就是轻量插件的范本。但插件机制本身也是核心系统的功能,没有足够通用性、没几个客户的插件机制就是投入产出失衡。
OCP 与单一职责原则(SRP):一体两面
| 原则 | 强调点 |
|---|---|
| 开闭原则(OCP) | 把模块业务的变化点抽离出去,交给别的模块 |
| 单一职责原则(SRP) | 每个模块只负责一个业务,不同时干多个 |
二者谈的本质是同一个问题的两个面。
总结
OCP 总结为两点:① 模块业务要稳定,遵循"只读"设计,要变就归档放弃;② 变化点简单的用回调/接口开放,复杂的引入插件机制,分解为"最小化核心系统 + 多个正交周边系统"。回调/接口本质是事件监听,是插件机制的特例。架构师除业务架构外,还要慎重选择基础架构——基础架构 + 业务架构才是软件的全部。
先把"是什么"回答清楚
| 概念 | 一句话说明 |
|---|---|
| 开闭原则(OCP) | 对扩展开放、对修改封闭,鼓励写"只读"业务模块 |
| "只读"设计 | 模块一经设计不再改,要变就废弃重写新模块 |
| 插件机制 | 让核心代码不变而业务能扩展的手段:能力暴露+加载+事件 |
| 单一职责(SRP) | 一个模块只干一个业务,与 OCP 是一体两面 |
一句话速记
把稳定的核心压到最小最封闭,把会变的部分用回调/接口/插件开放出去——OCP 不是 OOP 技巧,而是从 CPU 到 Git 到容器都在用的架构治理根本哲学。
几条值得记住的判断
- OCP 关注模块,不是最终软件;软件这个大模块必须靠插件机制进步。
- 事件越少越好,但要少而精,这极度依赖架构能力。
- 维持通用性是提供插件机制的前提,否则投入产出失衡。
思考题
回到你自己的项目:有没有某个模块,你总是在反复"修改"它来满足新需求?它该被抽出一个事件/插件点对扩展开放,还是干脆按"只读"思想归档、重写一个新模块?
