开闭原则(OCP) 由伯特兰・迈耶在 1988 年提出。它的定义是:软件实体应当对扩展开放,对修改关闭。
换句话说:软件的行为应该可以被扩展,而不必去修改这个实体本身。当然,这正是我们学习软件架构最根本的原因。显然,如果需求的一点点扩展,都会迫使软件发生大规模修改,那么这个软件系统的架构师就完全失败了。大多数学习软件设计的人,都把 OCP 看作指导类和模块设计的原则。但当我们站在架构组件层面来看时,这条原则的意义会更加重大。一个思想实验就能把这一点讲清楚。

思想实验
想象一下,我们有一个系统,在网页上展示财务汇总信息。页面数据可以滚动,负数用红色显示。现在,利益相关方要求:把同样的信息生成一份黑白打印机报表。报表需要正确分页、页眉、页脚、列标题,负数用括号包裹。显然,必须写一些新代码。但旧代码要改多少?一个好的软件架构会把需要修改的代码降到最低。理想情况下:零修改。
怎么做到?
按照 ** 单一职责原则(SRP)** 正确分离因为不同原因而变化的内容;再通过 ** 依赖反转原则(DIP)** 正确组织它们之间的依赖关系。
通过 SRP,我们可以得到如图 8.1 所示的数据流视图:某个分析过程读取财务数据,生成可报表数据,再由两个不同的展示器做不同格式处理。这里的核心认知是:生成报表涉及两个完全不同的职责:
计算要展示的数据
把数据展示成网页 / 打印格式

做出这种分离后,我们需要组织源码依赖,确保修改其中一个职责不会影响另一个。同时,新结构必须保证行为可以扩展,而不需要破坏性修改。
我们通过把流程拆分成类,并把类分到不同组件里来实现这一点,如图 8.2 中的双线所示。
左上:控制器(Controller)
右上:交互器(Interactor)
右下:数据库(Database)
左下:四个展示器(Presenter)和视图(View)组件
图中:
的是接口
的是数据结构
空心箭头:使用关系
实心箭头:实现或继承关系
首先要注意:所有箭头都是源码依赖。从 A 指向 B,意思是:A 的源码里用到了 B,但 B 完全不知道 A。
其次要注意:所有跨双线的依赖都只有一个方向。这意味着所有组件关系都是单向的。这些箭头指向我们希望被保护、不受变更影响的组件。
我再说一遍:如果希望组件 A 不受组件 B 的变更影响,
那么组件 B 应该依赖组件 A。
我们希望 Controller 不受 Presenter 变更影响
希望 Presenter 不受 View 影响
希望 Interactor 不受任何东西影响
Interactor 最符合 OCP 的保护目标。数据库、控制器、展示器、视图的修改,都完全不会影响 Interactor。
为什么 Interactor 地位这么特殊?因为它包含业务规则,包含应用最高层级的策略。其他组件都在处理外围问题,只有 Interactor 处理核心问题。
这就形成了一个基于层级的保护体系:
Interactor 最高,最受保护
View 最低,最不受保护
Presenter 比 View 高,但比 Controller、Interactor 低
这就是 OCP 在架构层面的工作方式:架构师根据功能如何变、为何变、何时变来拆分功能,然后把它们组织成组件层级结构。高层级组件不受低层级组件变更的影响。
方向控制
如果你觉得前面的类图很复杂,再看一眼:图里的大部分复杂度,都是为了保证组件之间的依赖指向正确的方向。
例如:FinancialReportGenerator 和 FinancialDataMapper 之间的FinancialDataGateway 接口,就是为了反转原本会从 Interactor 指向 Database 的依赖。
FinancialReportPresenter 接口和两个 View 接口也是同理。
信息隐藏
FinancialReportRequester 接口有另一个作用:保护 Controller,不让它知道 Interactor 的内部细节。
如果没有这个接口,Controller 就会产生对 FinancialEntities 的传递依赖。
传递依赖违反了一条通用原则:软件实体不应该依赖它们不直接使用的东西。
我们在后面讲 ** 接口隔离原则(ISP)和共同复用原则(CRP)** 时会再次遇到这条原则。
所以,虽然我们的首要目标是保护 Interactor 不受 Controller 影响,但我们同时也要隐藏 Interactor 的内部,保护 Controller 不受 Interactor 影响。
结论
OCP 是系统架构背后最主要的驱动力之一。它的目标是:让系统易于扩展,同时不会带来高成本的变更影响。
实现方式是:把系统拆分成组件,并将这些组件组织成依赖层级结构,让高层组件不受低层组件变更的影响。