62 | 重新认识开闭原则 (OCP)
架构分解中有两大难题:其一,需求的交织。不同需求混杂在一起,也就是存在所谓的全局性功能。其二,需求的易变。不同客户,不同场景下需求看起来很不一样,场景呈发散趋势。
就架构的本质而言,我们核心要掌握的架构设计的工具其实就只有两个:
- 组合。用小业务组装出大业务,组装出越来越复杂的系统。
- 如何应对变化(开闭原则)。
开闭原则(OCP)
软件实体(模块,类,函数等)应该对于功能扩展是开放的,但对于修改是封闭的 -- 勃兰特·梅耶
本质上,开闭原则的背后,是推崇模块业务的确定性。
可以修改模块代码的缺陷(Bug),但不要去随意调整模块的业务范畴,增加功能或减少功能都并不鼓励
它认为模块的业务变更是需要极其谨慎的,需要经得起推敲的
与其修改模块的业务,不如实现一个新业务。
只要业务的分解一直被正确执行的话,实现一个新的业务模块来完成新的业务范畴,是一件极其轻松的事情
开闭原则鼓励写 “只读” 的业务模块,一经设计就不可修改,如果要修改业务就直接废弃它,转而实现新的业务模块。
基于 Git 的源代码版本管理、基于容器的服务治理都是通过 “只读” 设计来改善系统的治理难度
CPU 背后的架构思维
冯·诺依曼体系的中央处理器(CPU)的设计完美体现了 “开闭原则” 的架构思想
- 指令是稳定的,但指令序列是变化的
- 计算是稳定的,但数据交换是多变的
怎么做到支持多变的指令序列的?
通过软件
怎么做到支持多变的输入输出设备的?
定义输入输出规范
它与面向对象无关,完全是开闭原则带来的威力
插件机制
一些人对开闭原则的错误解读,认为开闭原则不鼓励修改软件的源代码来响应新需求
开闭原则关注的焦点是模块,并不是最终形成的软
模块应该坚持自己的业务不变,这是开闭原则所鼓励的。
但对软件系统这个大模块来说,如果我们坚持它的业务范畴不变,就意味着我们放弃进步。
让软件的代码不变,但业务范畴却能够适应需求变化,有没有可能?
常规我们理解的插件,通常以动态库(dll/so
)形式存在,这种插件机制是操作系统引入的,可以做到跨语言。
比如 Java,它有自己的插件机制,以 jar
包的形式存在。
提供插件机制的二次开发接口需要包含以下三个部分。
- 软件自身能力的暴露,也就是我们经常说的 DOM API
- 插件加载机制。通常,这基于文件系统,比如我们规定把所有插件放到某个目录下。
- 事件监听。这是关键,也是难点所在。没有事件,插件没有机会介入到业务中去
事件当然越少越好。但是怎么做到少而精,这非常有讲究。一般来说,事件分以下三类:
- 界面操作类。
- 数据变更类。
- 业务流程类
完整的插件机制还是比较庞大的
Go 语言中的 image
包,它提供的 Decode 和 DecodeConfig 等功能都支持插件,我们可以增加一种格式支持,而无需修改 image 包。
最大的简化,是放弃了插件加载机制。我们自己手工来加载插件,比如
import "image"
import _ "image/jpeg"
import _ "image/png"
把复杂系统分解为一个最小化的核心系统,加上多个相互正交的周边系统,它背后的机制往往就是我们这里提的插件机制。
插件机制的确让核心系统与周边系统耦合度大大降低。但插件机制并非没有成本。插件机制本身也是核心系统的一个功能,它本身也需要考虑与核心系统其他功能的耦合度
所以维持足够的通用性,是提供插件机制的重大前提。
单一职责原则
它强调的是每个模块只负责一个业务,而不是同时干多个业务。而开闭原则强调的是把模块业务的变化点抽离出来,包给其他的模块。它们谈的本质上是同一个问题的两个面。
- 模块的业务要稳定。模块的业务遵循 “只读” 设计,如果需要变化不如把它归档,放弃掉
- 模块的业务变化点,简单一点的,通过回调函数或者接口开放出去,交给其他的业务模块。复杂一点的,通过引入插件机制把系统分解为 “最小化的核心系统 + 多个彼此正交的周边系统”。事实上回调函数或者接口本质上就是一种事件监听机制,所以它是插件机制的特例。

本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。
- 上一篇: 61 | 全局性功能的架构设计
- 下一篇: 63 | 接口设计的准则
目录