第六章 开闭原则(OCR)
定义:
Software entities like classes, modules and functions should beopen for extension but closed for modifications.( 一个软件实体如类、 模块和函数/方法应该对扩展开放, 对修改关闭。)
l 开闭原则告诉我们应尽量通过扩展软件实体的行为来实现变化, 而不是通过修改已有的代码来完成变化, 它是为软件实体的未来事件而制定的对现行开发设计进行约束的一个原则。
l 注意 开闭原则不意味着不做任何修改, 低层模块的扩展、变更, 必然要有高层模块进行耦合,否则就是一个孤立无意义的代码片段。
l 变化归纳为以下三种类型:
n 逻辑变化
u 只变化一个逻辑, 而不涉及其他模块,可以通过修改原有类中的方法的方式来完成, 前提条件是所有依赖或关联类都按照相同的逻辑处理。
n 子模块变化
u 一个模块变化, 会对其他的模块产生影响, 特别是一个低层次的模块变化必然引起高层模块的变化,因此在通过扩展完成变化时, 高层次的模块修改是必然的, 该部分的变化甚至会引起界面的变化。
n 可见视图变化
u 可见视图是提供给客户使用的界面, 该部分的变化一般会引起连锁反应。
u 不是指界面上按钮、 文字的重新排布等简单变化。
u 主要指业务耦合变化, 引起展示数据变化。 一个展示数据的列表, 按照原有的需求是6列, 突然有一天要增加1列, 而且这一列要跨N张表, 处理M个逻辑才能展现出来,这样的变化是比较恐怖的, 但还是可以通过扩展来完成变化, 这就要看我们原有的设计是否灵活。
l 例子:
需求:原因项目已经实施,从2008年开始, 全球经济开始下滑,对零售业影响比较大, 书店为了生存开始打折销售: 所有40元以上的书籍9折销售, 其他的8折销售。 对已经投产的项目来说,应该如何应对这样一个需求变化? 待选办法:1 修改接口;2 修改实现类;3通过扩展实现变化(派生子类)。 |
|
解决办法: 通过扩展实现变化。增加一个子类OffNovelBook, 覆写getPrice方法, 高层次的模块( 也就是static静态模块区) 通过OffNovelBook类产生新的对象, 完成业务变化对系统的最小化开发。 不用修改接口的办法,因为接口应该是稳定且可靠的, 不应该经常发生变化, 否则接口作为契约的作用就失去了效能。 不用修改实现类的办法,因为虽然 通过class文件替换的方式可以完成部分业务变化( 或是缺陷修复) 。但是该方法还是有缺陷的。 例如采购书籍人员看到的也是打折后的价格, 会因信息不对称而出现决策失误的情况。 |
解释:
开闭原则是最基础的一个原则, 前五章节介绍的原则都是开闭原则的具体形态,也就是说前五个原则就是指导设计的工具和方法, 而开闭原则才是其精神领袖。换一个角度来理解, 依照Java语言的称谓, 开闭原则是抽象类, 其他五大原则是具体的实现类。
开闭原则是可通过以下几个方面来理解其重要性:
1. 开闭原则对测试的影响。
l 所有已经投产的代码都是有意义的, 并且都受系统规则的约束, 这样的代码都要经过“千锤百炼”的测试过程, 不仅保证逻辑是正确的,还要保证苛刻条件( 高压力、 异常、 错误) 下不产生“有毒代码”( Poisonous Code) , 因此有变化提出时, 我们就需要考虑一下,原有的健壮代码是否可以不修改, 仅仅通过扩展实现变化呢?否则, 就需要把原有的测试过程回笼一遍, 需要进行单元测试、 功能测试、 集成测试甚至是验收测试。
2. 开闭原则可以提高复用性。
l 在面向对象的设计中, 所有的逻辑都是从原子逻辑组合而来的, 而不是在一个类中独立实现一个业务逻辑。
n 那为什么要复用呢? 减少代码量, 避免相同的逻辑分散在多个角落, 降低日后的维护人员为了修改一个微小的缺陷或增加新功能的难度。
n 那怎么才能提高复用率呢? 缩小逻辑粒度,直到一个逻辑不可再拆分为止。
3. 开闭原则可以提高可维护性
l 维护人员的工作不仅仅是对数据进行维护, 还可能要对程序进行扩展, 维护人员最乐意做的事情就是扩展一个类,而不是修改一个类,让维护人员读懂原有的代码, 然后再修改, 是一件很痛苦的事情。
4. 面向对象开发的要求
l 万物皆对象,但是万物皆运动, 有运动就有变化, 有变化就要有策略去应对, 怎么快速应对呢? 这就需要在设计之初考虑到所有可能变化的因素,然后留下接口, 等待“可能”转变为“现实”。
如何使用开闭原则
1. 抽象约束
l 通过接口或抽象类可以约束一组可能变化的行为, 并且能够实现对扩展开放, 其包含三层含义:
n 第一, 通过接口或抽象类约束扩展,对扩展进行边界限定, 不允许出现在接口或抽象类中不存在的public方法;
n 第二, 参数类型、 引用对象尽量使用接口或者抽象类, 而不是实现类;
n 第三, 抽象层尽量保持稳定, 一旦确定即不允许修改。
n 例如:
目前只是销售小说类书籍, 单一经营毕竟是有风险的, 于是书店新增加了计算机书籍, 它不仅包含书籍名称、 作者、 价格等信息, 还有一个独特的属性: 面向的是什么领域, 也就是它的范围, 比如是和编程语言相关的, 还是和数据库相关的。 ComputerBook类必须实现IBook的三个方法, 是通过IComputerBook接口传递进来的约束, 也就是我们制定的IBook接口对扩展类ComputerBook产生了约束力, 正是由于该约束力, BookStore类才不需要进行大量的修改。 |
2. 元数据( metadata) 控制模块行为。
l 编程是一个很苦很累的活,尽量使用元数据来控制程序的行为, 可以减少重复开发。
l 元数据是用来描述环境和数据的数据,通俗地说就是配置参数, 参数可以从文件中获得, 也可以从数据库中获得。
l 例如:Spring容器的SpringContext配置文件。
3. 制定项目章程
l 在一个团队中, 建立项目章程是非常重要的,因为章程中指定了所有人员都必须遵守的约定, 对项目来说,约定优于配置。该方法需要一个团队有较高的自觉性, 需要一个较长时间的磨合,一旦项目成员都熟悉这样的规则, 比通过接口或抽象类进行约束效率更高, 而且扩展性一点也没有减少。
4. 封装变化
l 对变化的封装包含两层含义:
n 第一, 将相同的变化封装到一个接口或抽象类中;
n 第二,将不同的变化封装到不同的接口或抽象类中, 不应该有两个不同的变化出现在同一个接口或抽象类中。
l 封装变化, 也就是受保护的变化( protected variations), 找出预计有变化或不稳定的点, 我们为这些变化点创建稳定的接口,准确地讲是封装可能发生的变化, 一旦预测到或“第六感”发觉有变化, 就可以进行封装, 23个设计模式都是从各个不同的角度对变化进行封装的。
最佳实践
使用开闭原则时要注意以下几个问题:
l 开闭原则也只是一个原则。
n 遵循这6大设计原则基本上可以应对大多数变化。 因此, 我们在项目中应尽量采用这6大原则, 适当时候可以进行扩充, 例如通过类文件替换的方式完全可以解决系统中的一些缺陷。如果有自动更新功能, 则可以下载一个.class文件直接覆盖原有的class, 重新启动应用( 也不一定非要重新启动)就可以解决问题。当然这种方式也可以应用到项目中, 正在运行中的项目发现需要增加一个新功能, 通过修改原有实现类的方式就可以解决这个问题, 前提条件是: 类必须做到高内聚、 低耦合, 否则类文件的替换会引起不可预料的故障。
l 项目规章非常重要。
n 如果你是一位项目经理或架构师, 应尽量让自己的项目成员稳定, 稳定后才能建立高效的团队文化, 章程是一个团队所有成员共同的知识结晶, 也是所有成员必须遵守的约定。优秀的章程能带给项目带来非常多的好处, 如提高开发效率、 降低缺陷率、 提高团队士气、 提高技术成员水平, 等等。
l 预知变化。
n 在实践中过程中, 架构师或项目经理一旦发现有发生变化的可能, 或者变化曾经发生过, 则需要考虑现有的架构是否可以轻松地实现这一变化。架构师设计一套系统不仅要符合现有的需求, 还要适应可能发生的变化, 这才是一个优良的架构。
n 开闭原则是一个终极目标, 任何人包括大师级人物都无法百分之百做到, 但朝这个方向努力, 可以非常显著地改善一个系统的架构, 真正做到“拥抱变化”。