编辑
2026-05-10
记录知识
0

目录

前言
开闭原则的本源
CPU 背后的架构思维
插件机制:让软件拥抱变化
单一职责原则与开闭原则的互补
示例程序
总结
参考资料

前言

开闭原则最早由勃兰特·梅耶在 1988 年提出时,很多人都以为这只是面向对象编程(OOP)领域的设计原则。但当你真正看清 CPU 的设计哲学,就会发现它与面向对象毫无关系——它是一切信息技术架构的根基。

开闭原则的本源

开闭原则的核心定义只有一句话:软件实体应该对功能扩展开放,但对修改封闭

这句话听起来简单,但它的含义远比字面深远。一个软件产品在其生命周期内会不断发生变化。我们不能让代码随着需求变更是非曲直,而是要让软件具备适应变化的能力。变化的应对之道是扩展,而非修改。

为什么这条原则重要?因为它背后藏着一套完整的架构哲学。

开闭原则认为,模块的业务范畴变更需要极其谨慎。一个模块可以修复缺陷(Bug),但不应该被随意调整功能边界。增加功能或减少功能都是需要经过充分推敲的决策。这意味着,模块的业务是"只读"的——如果业务变了,不如将旧模块归档,重新实现一个新模块。

这就是开闭原则鼓励的"只读"业务模块思想。一旦设计完成,业务模块就不应再被修改。如果要改变业务范畴,就废弃它,转而实现新的业务模块。

这种思想我们其实不陌生。Git 的版本管理、容器的服务治理,都是通过"只读"设计来降低系统治理的复杂度。架构设计同样如此:每一次正交的架构分解都在沉淀可复用的业务模块。随着时间推移,组装复杂业务系统将变得越来越简单。

所以,开闭原则是架构治理的根本哲学,而不仅仅是编码规范。

CPU 背后的架构思维

有一种广泛的误解认为开闭原则是面向对象的专属产物。这个误解把一条无比普世的原则窄化了。

早在面向对象概念出现之前,开闭原则的思想就已经在信息科技中无处不在。让我们看看 CPU 的设计。

在"大厦基石:无生有,有生万物"一讲中,我们讨论过冯·诺依曼体系的核心洞察:需求的稳定点往往是系统的核心价值点,而需求的变化点则需要做开放性设计。CPU 的设计完美印证了这一点。

指令是稳定的,但指令序列是变化的。 正是这种设计,让计算机得以实现"解决一切可以用'计算'来解决问题"的目标。无论软件多么复杂,CPU 永远只需要支持那一套稳定的指令集。

计算是稳定的,但数据交换是多变的。 正是因为这一点,计算机不必修改基础架构,却能适应不断发展变化的交互技术革命——从磁带,到硬盘,到 SSD,到网络存储,CPU 从不需要改变。

体会一下:我们怎么做到支持多变的指令序列的?由此发明了软件。我们怎么做到支持多变的输入输出设备的?我们定义了输入输出规范。

换句话说,我们不必修改 CPU,却因此支持了如此多姿多彩的信息世界。这种优雅的设计与面向对象无关,完全是开闭原则带来的威力。

CPU 的优雅远不止于此。在"软件运行机制及内存管理"一讲中,我们介绍了虚拟内存和缺页中断机制。CPU 通过引入缺页中断,将自身与多变的外部存储设备、多变的文件系统格式完全解耦。中断机制本质上是 CPU 引入的回调函数,通过它,CPU 把对计算机外设的演进能力交给了操作系统。这同样是开闭原则的鲜活案例。

插件机制:让软件拥抱变化

开闭原则关注的是模块,而不是最终的软件产品。那么,如果软件本身需要适应需求变化,该怎么办?

答案是插件机制。

常规的插件以动态库(dll/so)形式存在,这是操作系统层面引入的机制,可以跨语言使用。部分语言有自己的插件机制,它提供了完整的二次开发能力,这些接口构成了软件的插件机制,最终让软件成为一个生态型软件。

提供插件机制的二次开发接口通常包含三个部分。

第一,软件自身能力的暴露。 也就是 DOM API,插件借此调用软件已实现的功能。这是基础,不再展开。

第二,插件加载机制。 通常基于文件系统——规定所有插件放到某个目录下。Windows 平台额外支持将插件信息写入注册表。

第三,事件监听。 这是关键,也是难点所在。没有事件,插件就没有机会介入业务。那么应该提供什么样的事件?这非常依赖架构能力。在提供能力相同的情况下,事件越少越好。但怎么做到少而精?

事件通常分三类:

  • 界面操作类:最原始的是鼠标和键盘操作,但它们太过底层,提供出去是双刃剑。更常见的是暴露更高级的界面事件,如菜单项或按钮的点击。
  • 数据变更类:在数据发生变化时允许捕获并响应。最典型的是 onSelectionChanged,基本上所有软件二次开发接口都会提供。
  • 业务流程类:发生在业务流的中间环节或完成之后。比如 Office 软件,打开文件前后都可能发出事件,以便插件介入。

但完整的插件机制并非总是必要的。以 Python 的 Pillow 库(PIL 的现代分支)为例,它提供的 Image.open() 函数天然支持插件式的格式扩展,我们可以增加新的格式支持,无需修改 Pillow 的核心代码。实现方式极为简单:

from PIL import Image # 只需要导入对应的插件模块,注册机制通常在模块内部自动完成 # 假设我们要支持某种特殊的 WebP 或 自定义格式 import pillow_avif # 第三方插件,自动注册 AVIF 格式支持 # 核心代码无需任何改动,即可识别并打开新格式 img = Image.open("example.avif")

这段代码利用了 Python 动态导入的特性,为 Image 类加载了新的格式解码器。这里最大的简化是放弃了复杂的插件加载器——由 Python 的 import 机制和模块的 init.py 自动完成注册。

插件机制让核心系统与周边系统的耦合度大大降低。但它并非没有成本。插件机制本身也是核心系统的功能,也需要考虑与其他功能的耦合度。如果一个插件机制没有多少客户——没有几个功能依赖它开发,而它本身的代码散落在核心系统的各个角落——投入产出比就不划算。

所以,维持足够的通用性,是提供插件机制的重大前提。

单一职责原则与开闭原则的互补

总结开闭原则,其实就两点:

第一,模块的业务要稳定。 业务范畴遵循"只读"设计,如果需要变化就归档旧模块,转而实现新模块。这是架构治理的基础哲学。

第二,模块的业务变化点,简单一点的通过回调函数或接口开放出去,交给其他业务模块;复杂一点的通过引入插件机制,把系统分解为"最小化的核心系统 + 多个彼此正交的周边系统"。 回调函数或接口本质上就是一种事件监听机制,所以它们是插件机制的特例。

这让我们联想到另一个常见原则——单一职责原则(Single Responsibility Principle,SRP)。它强调每个模块只负责一个业务,而不是同时干多个业务。开闭原则强调的是把模块业务的变化点抽离出来,包给其他模块。

它们谈的是同一个问题的两个面:SRP 保障模块的业务边界清晰,OCP 保障模块的业务变化点被优雅地对外暴露。两者相辅相成,缺一不可。

示例程序

开闭原则在日常开发中最常见的应用,就是插件机制的实现。下面用 Python 模拟一个简化版的图片解码插件系统,演示如何通过插件机制在不修改核心代码的情况下扩展功能。

python
#!/usr/bin/env python3 """ plugin_system.py - 开闭原则的插件机制演示 核心系统(ImageCodec)只定义接口,具体的 codec 实现以插件形式加载。 新增格式支持只需添加新插件,无需修改核心代码。 """ from typing import Protocol, Iterator import sys class ImagePlugin(Protocol): """插件协议:所有图片解码插件必须实现 decode 方法""" def decode(self, data: bytes) -> str: """解码图片数据,返回描述信息""" ... def format_name(self) -> str: """返回支持的格式名称""" ... class ImageCodec: """ 核心系统:图片编解码器 遵循开闭原则,自身代码不变,通过插件扩展功能 """ def __init__(self): self._plugins: dict[str, ImagePlugin] = {} def register(self, plugin: ImagePlugin) -> None: """注册插件——扩展行为而非修改核心代码""" name = plugin.format_name() self._plugins[name] = plugin print(f"[核心系统] 注册插件: {name}") def decode(self, data: bytes, format_hint: str = "") -> str: """解码图片,支持自动探测格式或使用格式提示""" if not format_hint: format_hint = self._detect_format(data) plugin = self._plugins.get(format_hint) if not plugin: available = ", ".join(self._plugins.keys()) or "无" raise ValueError(f"不支持的格式: {format_hint},可用格式: {available}") return plugin.decode(data) def _detect_format(self, data: bytes) -> str: """模拟格式探测:从数据头识别格式""" if data.startswith(b'\xff\xd8'): return "jpeg" elif data.startswith(b'\x89PNG'): return "png" elif data.startswith(b'GIF'): return "gif" return "unknown" def supported_formats(self) -> Iterator[str]: """返回当前已注册的格式列表""" return iter(self._plugins.keys()) # ═══════════════════════════════════════════════════════════════ # 插件实现(可独立扩展,无需修改核心系统) # ═══════════════════════════════════════════════════════════════ class JpegPlugin: """JPEG 格式插件""" def format_name(self) -> str: return "jpeg" def decode(self, data: bytes) -> str: size = len(data) return f"JPEG 图片,解码后约 {size} 字节" class PngPlugin: """PNG 格式插件""" def format_name(self) -> str: return "png" def decode(self, data: bytes) -> str: size = len(data) return f"PNG 图片,解码后约 {size} 字节" class GifPlugin: """GIF 格式插件""" def format_name(self) -> str: return "gif" def decode(self, data: bytes) -> str: size = len(data) return f"GIF 图片,解码后约 {size} 字节" # ═══════════════════════════════════════════════════════════════ # 新增插件(未来扩展,无需修改 ImageCodec) # ═══════════════════════════════════════════════════════════════ class WebpPlugin: """未来扩展:WebP 格式插件(如果未来需要支持)""" def format_name(self) -> str: return "webp" def decode(self, data: bytes) -> str: size = len(data) return f"WebP 图片,解码后约 {size} 字节" def main(): # 模拟图片数据(带格式签名) jpeg_data = b'\xff\xd8\xff\xe0Jpeg解码器模拟数据' png_data = b'\x89PNG\r\n\x1a\nPNG解码器模拟数据' gif_data = b'GIF89aGIF解码器模拟数据' webp_data = b'RIFFxxxxWEBP解码器模拟数据' # 初始化核心系统 codec = ImageCodec() # 初始只注册 JPEG 和 PNG 插件 print("阶段1:核心系统仅加载 JPEG、PNG 插件") codec.register(JpegPlugin()) codec.register(PngPlugin()) print(f"可用格式: {', '.join(codec.supported_formats())}") print() # 使用已注册插件解码 print("阶段2:解码已注册格式") print(f" JPEG: {codec.decode(jpeg_data)}") print(f" PNG: {codec.decode(png_data)}") print() # 解码未注册格式(GIF 尚未注册) print("阶段3:尝试解码未注册格式(GIF)") try: result = codec.decode(gif_data) print(f" 结果: {result}") except ValueError as e: print(f" 预期行为: {e}") print() # 未来扩展:注册 WebP 插件(无需修改 ImageCodec) print("阶段4:未来扩展——注册 WebP 插件") codec.register(WebpPlugin()) print(f" 可用格式: {', '.join(codec.supported_formats())}") print() print("开闭原则体现:") print(" 1. 核心系统 ImageCodec 业务只读,代码从不修改") print(" 2. 新增格式支持只需注册新插件,插件之间正交无耦合") print(" 3. 插件注册行为(事件监听)是插件机制的特例") if __name__ == "__main__": main()

运行效果:

bash
$ python3 plugin_system.py 阶段1:核心系统仅加载 JPEG、PNG 插件 [核心系统] 注册插件: jpeg [核心系统] 注册插件: png 可用格式: jpeg, png 阶段2:解码已注册格式 JPEG: JPEG 图片,解码后约 25 字节 PNG: PNG 图片,解码后约 21 字节 阶段3:尝试解码未注册格式(GIF) 预期行为: 不支持的格式: gif,可用格式: jpeg, png 阶段4:未来扩展——注册 WebP 插件 [核心系统] 注册插件: webp 可用格式: jpeg, png, webp 开闭原则体现: 1. 核心系统 ImageCodec 业务只读,代码从不修改 2. 新增格式支持只需注册新插件,插件之间正交无耦合 3. 插件注册行为(事件监听)是插件机制的特例

代码解读:核心系统 ImageCodec 遵循开闭原则——业务只读,代码从不修改。通过 register() 方法注册插件来扩展功能,而不是修改核心代码。GIF 格式从未注册,所以解码时抛出异常;未来只需新增 GifPlugin 并注册,无需触动 ImageCodec 一行代码。这就是插件机制对开闭原则的实践。

总结

  • 开闭原则(OCP)的核心是:软件实体对扩展开放,对修改封闭,通过扩展而非修改来适应需求变化。
  • OCP 背后的架构哲学是模块业务的"只读"设计——业务范畴一经确定就不应随意调整,要变就建新模块。
  • "只读"思想贯穿整个信息技术领域:Git 版本管理、容器服务治理、以及 CPU 的指令集设计,都体现了这一哲学。
  • CPU 的设计完美诠释了 OCP:指令稳定但序列变化,计算稳定但数据交换多变,无需修改基础架构即可适应变化。
  • 插件机制是应对复杂变化的手段,包含三个部分:能力暴露(DOM API)、插件加载机制、事件监听。
  • 事件监听分三类:界面操作类、数据变更类、业务流程类。回调函数和接口本质上是事件监听的特例。
  • 插件机制需要维持足够的通用性才有价值——如果投入产出不成比例,就不值得做。
  • 单一职责原则(SRP)与开闭原则(OCP)互补:SRP 保障模块业务边界清晰,OCP 保障变化点被优雅暴露。
  • 基础架构 + 业务架构,才是软件设计的全部。作为架构师,不能只关注业务架构而忽视基础架构的选择。

参考资料

  • 软件架构基础