开闭原则最早由勃兰特·梅耶在 1988 年提出时,很多人都以为这只是面向对象编程(OOP)领域的设计原则。但当你真正看清 CPU 的设计哲学,就会发现它与面向对象毫无关系——它是一切信息技术架构的根基。
开闭原则的核心定义只有一句话:软件实体应该对功能扩展开放,但对修改封闭。
这句话听起来简单,但它的含义远比字面深远。一个软件产品在其生命周期内会不断发生变化。我们不能让代码随着需求变更是非曲直,而是要让软件具备适应变化的能力。变化的应对之道是扩展,而非修改。
为什么这条原则重要?因为它背后藏着一套完整的架构哲学。
开闭原则认为,模块的业务范畴变更需要极其谨慎。一个模块可以修复缺陷(Bug),但不应该被随意调整功能边界。增加功能或减少功能都是需要经过充分推敲的决策。这意味着,模块的业务是"只读"的——如果业务变了,不如将旧模块归档,重新实现一个新模块。
这就是开闭原则鼓励的"只读"业务模块思想。一旦设计完成,业务模块就不应再被修改。如果要改变业务范畴,就废弃它,转而实现新的业务模块。
这种思想我们其实不陌生。Git 的版本管理、容器的服务治理,都是通过"只读"设计来降低系统治理的复杂度。架构设计同样如此:每一次正交的架构分解都在沉淀可复用的业务模块。随着时间推移,组装复杂业务系统将变得越来越简单。
所以,开闭原则是架构治理的根本哲学,而不仅仅是编码规范。
有一种广泛的误解认为开闭原则是面向对象的专属产物。这个误解把一条无比普世的原则窄化了。
早在面向对象概念出现之前,开闭原则的思想就已经在信息科技中无处不在。让我们看看 CPU 的设计。
在"大厦基石:无生有,有生万物"一讲中,我们讨论过冯·诺依曼体系的核心洞察:需求的稳定点往往是系统的核心价值点,而需求的变化点则需要做开放性设计。CPU 的设计完美印证了这一点。
指令是稳定的,但指令序列是变化的。 正是这种设计,让计算机得以实现"解决一切可以用'计算'来解决问题"的目标。无论软件多么复杂,CPU 永远只需要支持那一套稳定的指令集。
计算是稳定的,但数据交换是多变的。 正是因为这一点,计算机不必修改基础架构,却能适应不断发展变化的交互技术革命——从磁带,到硬盘,到 SSD,到网络存储,CPU 从不需要改变。
体会一下:我们怎么做到支持多变的指令序列的?由此发明了软件。我们怎么做到支持多变的输入输出设备的?我们定义了输入输出规范。
换句话说,我们不必修改 CPU,却因此支持了如此多姿多彩的信息世界。这种优雅的设计与面向对象无关,完全是开闭原则带来的威力。
CPU 的优雅远不止于此。在"软件运行机制及内存管理"一讲中,我们介绍了虚拟内存和缺页中断机制。CPU 通过引入缺页中断,将自身与多变的外部存储设备、多变的文件系统格式完全解耦。中断机制本质上是 CPU 引入的回调函数,通过它,CPU 把对计算机外设的演进能力交给了操作系统。这同样是开闭原则的鲜活案例。
开闭原则关注的是模块,而不是最终的软件产品。那么,如果软件本身需要适应需求变化,该怎么办?
答案是插件机制。
常规的插件以动态库(dll/so)形式存在,这是操作系统层面引入的机制,可以跨语言使用。部分语言有自己的插件机制,它提供了完整的二次开发能力,这些接口构成了软件的插件机制,最终让软件成为一个生态型软件。
提供插件机制的二次开发接口通常包含三个部分。
第一,软件自身能力的暴露。 也就是 DOM API,插件借此调用软件已实现的功能。这是基础,不再展开。
第二,插件加载机制。 通常基于文件系统——规定所有插件放到某个目录下。Windows 平台额外支持将插件信息写入注册表。
第三,事件监听。 这是关键,也是难点所在。没有事件,插件就没有机会介入业务。那么应该提供什么样的事件?这非常依赖架构能力。在提供能力相同的情况下,事件越少越好。但怎么做到少而精?
事件通常分三类:
但完整的插件机制并非总是必要的。以 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 一行代码。这就是插件机制对开闭原则的实践。