文章目录
- 1 定义
- 2 最佳实践
- 2.1 需求
- 2.2 需求变更
- 2.3 变更原则
- 2.4 实现逻辑
- 2.4.1 组件化
- 2.4.2 组件关系
- 2.5 依赖方向的控制
- 3 本章小结
1 定义
开闭原则(OCP)是Bertrand Meyer在1988年提出的,该设计原则认为:
设计良好的计算机软件应该易于扩展,同时抗拒修改。
换句话说,一个设计良好的计算机系统应该在不需要修改的前提下就可以轻易被扩展。
如果对原始需求的小小延伸就需要对原有的软件系统进行大幅修改,那么这个系统的架构设计显然是失败的。在软件架构层面,这项原则的意义重大。
2 最佳实践
2.1 需求
我们现在要设计一个在Web页面上展示财务数据的系统,页面上的数据要可以滚动显示,其中负值应显示为红色。
2.2 需求变更
接下来,该系统的所有者又要求同样的数据需要形成一个报表,该报表要能用黑白打印机打印,并且其报表格式要得到合理分页,每页都要包含页头、页尾及栏目名。同时,负值应该以括号表示。
2.3 变更原则
显然,我们需要增加一些代码来完成这个要求。但在这里我们更关注的问题是,满足新的要求需要更改多少旧代码。
2.4 实现逻辑
2.4.1 组件化
但该如何实现这一点呢?我们可以先将满足不同需求的代码分组(即SRP),然后再来调整这些分组之间的依赖关系(即DIP)。
利用SRP,我们可以按图中所展示的方式来处理数据流。即先用一段分析程序处理原始的财务数据,以形成报表的数据结构,最后再用两个不同的报表生成器来产生报表。
这里的核心就是将应用生成报表的过程拆成两个不同的操作。即先计算出报表数据,再生成具体的展示报表(分别以网页及纸质的形式展示)。
2.4.2 组件关系
接下来,我们就该修改其源代码之间的依赖关系了。这样做的目的是保证其中一个操作被修改之后不会影响到另外一个操作。同时,我们所构建的新的组织形式应该保证该程序后续在行为上的扩展都无须修改现有代码。
在具体实现上,我们会将整个程序进程划分成一系列的类,然后再将这些类分割成不同的组件。下面,我们用下图中的那些双线框来具体描述一下整个实现。在这个图中,左上角的组件是Controller,右上角是Interactor,右下角是Database,左下角则有四个组件分别用于代表不同的Presenter和View。在下图中,用<I>标记的类代表接口,用<DS>标记的则代表数据结构;开放箭头指代的是使用关系,闭合箭头则指代了实现与继承关系。
下图中看到的所有依赖关系都是其源代码中存在的依赖关系。这里,从类A指向类B的箭头意味着A的源代码中涉及了B,但是B的源代码中并不涉及A。因此在下图中,FinancialDataMapper在实现接口时需要知道FinancialDataGateway的实现,而FinancialDataGateway则完全不必知道FinancialDataMapper的实现。
这里很重要的一点是这些双线框的边界都是单向跨越的。也就是说,上图中所有组件之间的关系都是单向依赖的,如上图所示,图中的箭头都指向那些我们不想经常更改的组件。
让我们再来复述一下这里的设计原则:
如果A组件不想被B组件上发生的修改所影响,那么就应该让B组件依赖于A组件。
所以我们不想让发生在Presenter上的修改影响到Controller,也不想让发生在View上的修改影响到Presenter。而最关键的是,我们不想让任何修改影响到Interactor。
其中,Interactor组件是整个系统中最符合OCP的。发生在Database、Controller、Presenter甚至View上的修改都不会影响到Interactor。
为什么Interactor会被放在这么重要的位置上呢?因为它是该程序的业务逻辑所在之处,Interactor中包含了其最高层次的应用策略。其他组件都只是负责处理周边的辅助逻辑,只有Interactor才是核心组件。
虽然Controller组件只是Interactor的附属品,但它却是Presenter和View所服务的核心。同样的,虽然Presenter组件是Controller的附属品,但它却是View所服务的核心。
这里利用“层级”这个概念创造了一系列不同的保护层级。譬如,Interactor是最高层的抽象,所以它被保护得最严密,而Presenter比View的层级高,但比Controller和Interactor的层级低。
以上就是我们在软件架构层次上对OCP这一设计原则的应用。软件架构师可以根据相关函数被修改的原因、修改的方式及修改的时间来对其进行分组隔离,并将这些互相隔离的函数分组整理成组件结构,使得高阶组件不会因低阶组件被修改而受到影响。
2.5 依赖方向的控制
刚刚在图中所看到的复杂度是我们想要对组件之间的依赖方向进行控制而产生的。
例如,FinancialReportGenerator和FinancialDataMapper之间的FinancialDataGateway接口是为了反转Interactor与Database之间的依赖关系而产生的。同样的,FinancialReportPresenter接口与两个View接口之间也类似于这种情况。
FinancialReportRequester接口的作用则完全不同,它的作用是保护FinancialReportController不过度依赖于Interactor的内部细节。如果没有这个接口,则Controller将会传递性地依赖于FinancialEntities。
这种传递性依赖违反了“软件系统不应该依赖其不直接使用的组件”这一基本原则。
虽然我们的首要目的是为了让Interactor屏蔽掉发生在Controller上的修改,但也需要通过隐藏Interactor内部细节的方法来让其屏蔽掉来自Controller的依赖。
3 本章小结
OCP是我们进行系统架构设计的主导原则,其主要目标是让系统易于扩展,同时限制其每次被修改所影响的范围。实现方式是通过将系统划分为一系列组件,并且将这些组件间的依赖关系按层次结构进行组织,使得高阶组件不会因低阶组件被修改而受到影响。
参考内容来源于:《架构整洁之道》