开闭原则【Open Close Principle】
一、定义
Softwareentities like classes, modules and functions should be open for extension butclosed for modifications.
一个软件实体应该对扩展开放,对修改关闭。
二、什么是开闭原则
开闭原则的定义已经非常明确告诉我们:软件实体应该对扩展开放,对修改关闭,其含义是说一个软件实体应该通过扩展来实现变化,而不是通过修改已有的代码来实现变化。那什么又是软件实体呢?软实体包括以下部分:
- 项目或软件产品中按照一个的逻辑规则划分的模块
- 抽象或类
- 方法
一个软件产品只要在生命期内,都会发生变化,变化既然是一个既定的事实,我们就应该在设计时候尽量适应这些变化,以提高项目的稳定性和灵活性,真正实现“拥抱变化”,开闭原则告诉我们通过尽量通过扩展软件实体的行为来实现变化,而不通过修改来已有的代码来完成变化,它是为软件实体的未来事件而制定的对现行开发设计进行约束的一个原则,
IBook是定义了数据的三个属性:名称、价格和作者,小说类NovelBook是一个具体的实现类,所有小说书籍的总称,BookStore指的是书店(这里仅给出书店的源代码,其他的就是最基本的javabean)
public class BookStore { private final static ArrayList<IBook> bookList = new ArrayList<IBook>(); // 静态模块初始化,项目中一般是从持久层初始化产生 static { bookList.add(new NovelBook("天龙八部", 3200, "金庸")); bookList.add(new NovelBook("巴黎圣母院", 5600, "雨果")); bookList.add(new NovelBook("悲惨世界", 3500, "雨果")); bookList.add(new NovelBook("金瓶梅", 4300, "兰陵笑笑生")); } // 模拟书店买书 public static void main(String[] args) { NumberFormat formatter = NumberFormat.getCurrencyInstance(); formatter.setMaximumFractionDigits(2); System.out.println("------------书店买出去的书籍记录如下:---------------------"); for (IBook book : bookList) { System.out.println("书籍名称:" + book.getName() + "\t书籍作者:" + book.getAuthor() + "\t书籍价格:" + formatter.format(book.getPrice() / 100.0) + "元"); } } }
变化:打折销售
书店为了生存开始打折销售:所有40元以上的书籍9折销售,其他的8折销售。对已经投产的项目来说,这就是一个变化,我们来看看这样的一个需求变化,我们该怎么去应对,有三种方法可以解决这个问题:
- 修改接口。在IBook上新增加一个方法getOffPrice(),专门进行打折处理,所有的实现类实现该方法。但是这样修改的后果就是实现类NovelBook要修改,BookStore中的main方法也修改,同时IBook作为接口应该是稳定且可靠的,不应该经常发生变化,否则接口做为契约的作用就失去了效能,——因此,该方案否定。
- 修改实现类。修改NovelBook类中的方法,直接在getPrice()中实现打折处理,好办法,我相信大家在项目中经常使用的就是这样办法,通过class文件替换的方式可以完成部分业务(或是缺陷修复)变化,该方法在项目有明确的章程(团队内约束)或优良的架构设计时,是一个非常优秀的方法,但是该方法还是有缺陷的,例如采购书籍人员也是要看价格的,由于该方法已经实现了打折处理价格,因此采购人员看到的也是打折后的价格,这就产生了信息的蒙蔽效果,导致信息不对称而出现决策失误的情况。——因此,该方案也不是一个最优的方案。
- 通过扩展实现变化。增加一个子类OffNovelBook,覆写getPrice方法,高层次的模块(也就是static静态模块区)通过OffNovelBook类产生新的对象,完成对业务变化开发任务。——好办法,修改也少,风险也小,我们来看类图:
public class BookStore { private final static ArrayList<IBook> bookList = new ArrayList<IBook>(); // 静态模块初始化,项目中一般是从持久层初始化产生 static { bookList.add(new OffNovelBook("天龙八部", 3200, "金庸")); bookList.add(new OffNovelBook("巴黎圣母院", 5600, "雨果")); bookList.add(new OffNovelBook("悲惨世界", 3500, "雨果")); bookList.add(new OffNovelBook("金瓶梅", 4300, "兰陵笑笑生")); } // 模拟书店买书 public static void main(String[] args) { NumberFormat formatter = NumberFormat.getCurrencyInstance(); formatter.setMaximumFractionDigits(2); System.out.println("------------书店买出去的书籍记录如下:---------------------"); for (IBook book : bookList) { System.out.println("书籍名称:" + book.getName() + "\t书籍作者:" + book.getAuthor() + "\t书籍价格:" + formatter.format(book.getPrice() / 100.0) + "元"); } } }
打折销售开发完成了。看到这里,各位可能有想法了:增加了一个OffNoveBook类后,你的业务逻辑还是修改了,你修改了static静态模块区域,这部分确实修改了,该部分属于高层次的模块,是由持久层产生的,在业务规则改变的情况下高层模块必须有部分改变以适应新业务,改变时尽量的少,压制变化风险的扩散。注意:开闭原则说是对扩展开放,对修改关闭,并不意味着不做任何的修改,我们可以把变化归纳为以下几个类型:
- 逻辑变化。只变化一个逻辑,而不涉及到其他模块,比如原有的一个算法是a*b+c,现在需要修改为a*b*c,可以通过修改原有的类中的方法方式来完成,前提条件是所有依赖或关联类都按照相同的逻辑处理。
- 子模块变化。一个模块变化,会对其他的模块产生影响,特别是一个低层次的模块变化必然引起高层模块的变化,因此在通过扩展完成变化时,高层次的模块修改是必然的,刚刚的书籍打折处理就是类似的处理模块,该部分的变化甚至会引起界面的变化。
- 可见视图变化。可见视图是提供给客户使用的界面,如jsp程序,swing界面等,该部分的变化一般会引起连锁反应
三、为什么要使用开闭原则
开闭原则是最基础的一个原则,前边五个章节介绍的原则(单一职责原则、里氏替换原则、依赖倒置原则、接口隔离原则、迪米特法则)都是开闭原则的具体形态,也就是说前五个原则就是指导设计的工具和方法,而开闭原则才是其精神领袖,换一个角度来理解,依照Java语言的称谓,开闭原则是抽象类,其他五大原则则是具体的实现类,开闭原则在面向对象设计领域中的地位就类似于的牛顿第一定律在力学界、勾股定律在几何学、质能方程在狭义相对论中的地位,其地位无人能及
3.1、开闭原则对测试的影响
有变化提出时,我们就需要考虑一下,原有健壮的代码是否可以不修改,仅仅通过扩展开实现变化呢?新增加的类,只要保证新增加类就是正确的就可以了。否则,就需要把原有的测试过程回笼一遍,所以,我们需要通过扩展来实现业务逻辑的变化,而不是修改。上面的例子中通过增加一个子类OffNovelBook来完成了业务需求的变化,对测试有什么好处呢?我们重新生成一个测试文件OffNovelBookTest,然后对getPrice进行测试,单元测试是孤立测试,我只要保证我提供的方法正确就成了,其他的我不管
public class NovelBookTest extends TestCase { private String name = "平凡的世界"; private int price = 6000; private String author = "路遥"; private IBook novelBook = new NovelBook(name, price, author); // 测试getPrice方法 public void testGetPrice() { // 原价销售,判断输入和输出的值是否相等进行断言 super.assertEquals(this.price, this.novelBook.getPrice()); } }
3.2、开闭原则可以提高可维护性
一个软件投产后,维护人员的工作不仅仅是对数据进行维护,还可能对程序进行扩展,那维护人员最乐意做的事情,就是扩展一个类,而不是修改一个类,甭管原有的代码写的多么优秀还是写的多么糟糕,让维护人员读懂原有的代码,然后再修改是一件很痛苦的事情,
3.3、面向对象开发的要求
万物皆对象,我们需要把所有的事物都抽象成对象,然后针对对象进行操作,但是万物皆运动,有运动就有变化,有变化就要有策略去应对,怎么快速的应对?就需要在设计之初考虑到所有可能变化的因素,然后留下接口,等待可变因素转变真正的变化时轻松应对。
四、怎么使用开闭原则
4.1、抽象约束。
抽象是对一组事物的通用描述,没有具体的实现,也就表示它可以有非常多可能性,可以跟随需求的变化而变化,因此通过接口或抽象类可以约束一组行为,并且能够实现扩展开放,其包含三层含义:一是通过接口或抽象类约束扩展,对扩展进行边界限定,不允许出现在接口或抽象类中不存在的public方法;二是在参数类型定义、输入输出参数尽量使用接口或者抽象类,而不是实现类,三是抽象层尽量保持稳定,一旦确定即不允许修改。
4.2、参数控制模块行为
程序员是一个很苦很累的活,那怎么才能减轻我们的压力呢?答案是尽量是参数来控制我们的程序的行为,减少重复开发。参数可以从文件中获得,也可以从数据库中获得。(如瓦片路径配置)
4.3、制定项目章程
章程中指定了所有人员都必须遵守的约束,而对项目来说约定是优于配置。举个简单的例子,以SSH项目开发为例,一个项目中的Bean配置文件是非常多的,管理非常麻烦,如果需要扩展就需要增加子类,并修改SpringContext文件,而如果你在项目中指定这样一个章程:所有的Bean都自动注入,使用Annotation进行装配,进行扩展时,甚至只用写一个子类,然后由持久层生成对象,其他的都不需要修改,这就需要项目内约束,每个项目成员都必须遵守,该方法需要一个团队有较高的自觉性,需要一个较长时间的磨合,一旦项目成员都熟悉这样的规则,比通过接口或抽象类进行约束效率更高,而且扩展性一点也没有减少。
4.4、封装变化
对变化的封装包含两层含义:一是对相同的变化封装到一个接口或抽象类中,二是对不同的变化封装到不同的接口或抽象类中,不应该出现两个不同的变化出现同一个接口或抽象类中。封装变化,准确的讲就是封装可能发生的变化,一旦预测到或“第六感”发觉有变化,就可以进行封装,23个设计模式都是从各个不同的角度对变化进行封装。