6.3 开放封闭原则(OCP)
开闭原则(Open-Closed Principle, OCP)指的是,一个类或者模块,如果在业务修改或者功能需要扩展时,应尽可能保证只通过新添加代码,而不是修改原有代码的情况下完成。
该原则是Eiffel 语言的设计者,法国计算机科学家Meyer提出的,最开始的描述是:当向模块添加字段或方法时,不可避免地需要对调用这个模块的程序进行修改,解决这问题的方法是采用依赖于面向对象继承(特别是从实际父类进行的实现继承)的方法,而不是直接修改原来的程序。
从20世纪90年代开始,OCP的原则依然被大家遵循,只是解决的方式变了。随着抽象类和接口等方式的大量应用,开放封闭原则开始提倡采用抽象类或接口的方法,而不是实现继承来解决问题。
解决方式是现存的接口或抽象类对于修改原有的方法和属性是封闭的,但对新添加的方法和属性则是开放和允许的。
为什么要对原有的代码进行保护,对新加的代码开放呢?
所有的软件和程序都有一个生命周期,当需求和业务发生扩展和更新时,需要更新软件(修复缺陷和软件重构时的更新除外),要尽可能保证软件的基本框架不变,尽可能不修改现有的代码,而是添加新的实现代码,使得软件具有好的稳定性和可维护性。
实现扩展而不修改原有代码的基础就是采用接口或者抽象类等机制完成的。经过良好的定义,系统可以拥有一个相对稳定的抽象层,将业务行为下沉到具体的实现层中。
如果业务有扩展,可以将扩展的业务放到实现层中,只添加新的代码,不修改已有的代码,通过抽象层,将新的业务逻辑指定到新添加的业务实现层中。
如果业务有修改,无须对抽象层进行任何改动,添加实现代码,替换固定的实现层代码,而不会影响实现层其他模块的行为。
设计实例:
一个公司中有多种物料类型,每种物料都要根据物料类型打印不同的检验说明单据,这个检验说明是由一个非面向对象的程序来控制的,当公司中的物料类型增加一种时,IT部门就被要求为这个程序添加一种打印方法。IT部门每次修改这个打印程序后,都要为以前的每一种的打印都做一个回退测试,保证新加的代码没有影响以往的业务。
如图6-4所示,程序流程图如下,当添加一种新物料类型时,程序就又多一组判断:
代码如示例程序6.1所示。
"示例程序6.1 |
REPORT zrep_cls_021. |
当每次公司出现一个新的物料类型时,就要添加一个针对这个新类型的相关检验的要求,我们就需要去修改这个IF ELSE代码添加一个新的逻辑,主程序就要每一次都进行修改,这就不符合开放封闭原则。
对于不断变化的业务,如果我们每次修改原有的代码,除了要对增加的功能进行测试,对原有的功能还必须要做回退测试(Regression Test),保证新的逻辑没有影响以前的业务逻辑。但如果软件能够做到仅增加新的代码,不修改,或基于一定的规则的修改原有代码,这样就能保证工作量和代码出错的几率大幅下降。这就是"开放-封闭"原则,即软件扩展时只添加新代码而不是修改原有代码。
我们用面向对象的程序设计,并采用一个"改进的简单工厂" 设计模式(也就是基于配置表的简单工厂模式,具体结构见第7章第3节)来对系统进行重构,说明一下如何做到重构中的开放封闭原则。(例子中的这个程序因为太简单,其实是没必要进行面向对象重构的,但这个例子是足够能说明实际业务中的重构过程和如何做到的遵循开放封闭原则。重构后,代码量会增加,但却是十分值得的。)
如图6-5所示,首先将业务抽象为三层,第一层为物料对象生成工厂类,相当于主程序中的操作者,用于创建物料类的对象实例。第二层为抽象的物料类,由物料对象生成工厂类直接调用。第三层是具体的物料类,代表具体的物料类型,可以不断从抽象类中继承和添加新的子类,具体的解释如表6-2所示。
其中第二层抽象类隔离开了操作类和具体物料类,使得主程序可以不必修改,就可以处理新添加的业务逻辑。
程序和实体列表 |
解释 |
第一层类:物料对象生成工厂类 |
一个类,用于创建(生产)物料类的对象实例。 |
第二层抽象类:抽象的物料类 |
一个类,抽象的物料类。 |
第三层类:具体的物料类 |
可以有多个类,每个代表一个具体的物料类型和相应的处理方法。 |
数据表:物料类型和相应具体物理类的表 |
表中记录了SAP物料的类型和对应具体类的类名。 |
我们逐步介绍如何创建这些类和实体:
第一步,如图6-6所示,创建抽象的物料类ZCL_MATERIAL,这是第二层的抽象类,将多样的具体业务抽象为一种模式。设定类的类型为"Abstract"抽象类。
如图6-7所示,创建一个属性MV_MATERIAL,为受保护可见度, 类型为物料号码(MARA-MATNR)。
如图6-8所示,设定方法PRINT_INSP_DESC,可见类型为Public,无参数。不必定义具体逻辑,该方法表明了物料类拥有打印检验说明的能力,但具体实现代码是由其子类各自实现的。