2.6 开闭原则
开闭原则的英文名称是Open-Closed Principle,简称OCP。
2.6.1 开闭原则的定义
开闭原则的英文原文是:
Software entities should be open for extension,but closed for modification.
意思是:一个软件实体应当对扩展开放,对修改关闭。
这个原则说的是,在设计一个模块的时候,应当使这个模块可以在不被修改的前提下被扩展,即应当可以在不必修改源代码的情况下改变这个模块的行为。
在面向对象的编程中,开闭原则是最基础的原则,起到总的指导作用,其他原则(单一职责、里氏替换、依赖倒置、接口隔离、迪米特法则)都是开闭原则的具体形态,即其他原则都是开闭原则的手段和工具。开闭原则的重要性可以通过以下几个方面来体现。
■ 开闭原则提高复用性。在面向对象的设计中,所有的逻辑都是从原子逻辑组合而来的,而不是在一个类中独立实现一个业务逻辑,代码粒度越小,被复用的可能性就越大,避免相同的逻辑重复增加。开闭原则的设计保证系统是一个在高层次上实现了复用的系统。
■ 开闭原则提高可维护性。一个软件投产后,维护人员的工作不仅仅是对数据进行维护,还可能对程序进行扩展,就是扩展一个类,而不是修改一个类。开闭原则对已有软件模块,特别是最重要的抽象层模块要求不能再修改,这就使变化中的软件系统有一定的稳定性和延续性,便于系统的维护。
■ 开闭原则提高灵活性。所有的软件系统都有一个共同的性质,即对系统的需求都会随时间的推移而发生变化。在软件系统面临新的需求时,系统的设计必须是稳定的。开闭原则可以通过扩展已有的软件系统,提供新的行为,能快速应对变化,以满足对软件新的需求,使变化中的软件系统有一定的适应性和灵活性。
■ 开闭原则易于测试。测试是软件开发过程中必不可少的一个环节。测试代码不仅要保证逻辑的正确性,还要保证苛刻条件(高压力、异常、错误)下不产生“有毒代码”(Poisonous Code),因此当有变化提出时,原有健壮的代码要尽量不修改,而是通过扩展来实现。否则,就需要把原有的测试过程回笼一遍,需要进行单元测试、功能测试、集成测试,甚至是验收测试。开闭原则的使用,保证软件是通过扩展来实现业务逻辑的变化,而不是修改。因此,对于新增加的类,只需新增相应的测试类,编写对应的测试方法,只要保证新增的类是正确的就可以了。
2.6.2 开闭原则的应用
下述内容用于实现任务描述 2.D.6,以书店售书为例,演示开闭原则的应用。书店售书的类图如图2-11所示。
图2-11 书店售书类图
书籍接口IBook的源代码如下所示。
【描述2.D.6】 IBook.java
public interface IBook { // 获取书名 public String getName(); // 获取书的价格 public double getPrice(); // 获取书的作者 public String getAuthor(); }
小说类NovelBook实现IBook接口,其代码如下所示。
【描述2.D.6】 NovelBook.java
public class NovelBook implements IBook { // 书名 private String name; // 书的价格 private double price; // 书的作者 private String author; // 构造函数 public NovelBook(String name, double price, String author) { this.name = name; this.price = price; this.author = author; } public String getAuthor() { return this.author; } public String getName() { return this.name; } public double getPrice() { return this.price; } }
书店BookStore售书的过程代码如下所示。
【描述2.D.6】 BookStore.java
public class BookStore { // 图书列表 private ArrayList<IBook> bookList = new ArrayList<IBook>(); //构造函数 public BookStore() { // 对图书列表进行初始化 bookList.add(new NovelBook("西游记",79.20,"吴承恩")); bookList.add(new NovelBook("红楼梦",93.80,"曹雪芹")); bookList.add(new NovelBook("三国演义",67.00,"罗贯中")); bookList.add(new NovelBook("水浒传",54.00,"施耐庵")); } public void showBooks(){ System.out.println("-------------书店售书列表------------------"); System.out.println("书名\t\t价格\t\t作者"); for(IBook book:bookList){ System.out.println(book.getName()+"\t\t¥"+book.getPrice() +"元\t\t"+book.getAuthor()); } } public static void main(String args[]){ BookStore bstore=new BookStore(); bstore.showBooks(); } }
运行结果如下所示。
-------------书店售书列表------------------ 书名 价格 作者 西游记 ¥79.2元 吴承恩 红楼梦 ¥93.8元 曹雪芹 三国演义 ¥67.0元 罗贯中 水浒传 ¥54.0元 施耐庵
为了扩大图书的销售量,书店要按照9折销售图书,这就需要对现有的售书系统进行修改。遵照“开闭原则”中对修改关闭的原则,此时不能直接修改IBook接口和NovelBook类,而是通过增加一个子类OffNovelBook来完成,如图2-12所示。
图2-12 扩展后的书店售书类图
OffNovelBook类继承NovelBook,其代码如下所示。
【描述2.D.6】 NovelBook.java
public class OffNovelBook extends NovelBook { //构造函数 public OffNovelBook(String name,double price,String author){ super(name,price,author); } //重写getPrice()方法 public double getPrice(){ //图书的价格打9折 return super.getPrice()*0.9; } }
在BookStore类中,只需要将原来的“new NovelBook”修改为“new OffNovelBook”即可,修改的代码如下所示。
【描述2.D.6】 BookStore.java中修改的代码
...... public BookStore() { bookList.add(new OffNovelBook("西游记",79.20,"吴承恩")); bookList.add(new OffNovelBook("红楼梦",93.80,"曹雪芹")); bookList.add(new OffNovelBook("三国演义",67.00,"罗贯中")); bookList.add(new OffNovelBook("水浒传",54.00,"施耐庵")); } ......
修改后的运行结果如下所示。
-------------书店售书列表------------------ 书名 价格 作者 西游记 ¥71.28元 吴承恩 红楼梦 ¥84.42元 曹雪芹 三国演义 ¥60.3元 罗贯中 水浒传 ¥48.6元 施耐庵
这样通过增加一个OffNovelBook类,修改BookStore中少量的代码,就可以实现图书价格的9折销售,而其他部分没有任何变动,体现了开闭原则的应用。
注意 开闭原则对扩展开放,对修改关闭,并不意味着不做任何修改,低层模块的变更,必然要有高层模块进行耦合,否则就是一个孤立无意义的代码片段。
开闭原则解决问题的关键在于抽象化,把系统所有可能的行为抽象成一个抽象底层,这个抽象底层规定出所有的具体实现必须提供的方法的特征,给系统定义出一个一劳永逸、不再更改的抽象设计,此设计允许有无穷尽的行为在实现层被实现。在Java中,可以定义一个或多个抽象 Java 类或 Java 接口,规定出所有的具体类必须提供的方法的特征作为系统设计的抽象层。作为系统设计的抽象层,要预见所有可能的扩展,因此在任何扩展情况下系统的抽象底层不需要修改,从而满足了开闭原则的第二条:对修改关闭。同时,由于从抽象层导出一个或多个新的具体类可以改变系统的行为,因此系统的设计对扩展是开放的,这就满足了开闭原则的第一条。
在软件开发的过程中,一直都是提倡需求导向的。这就要求在设计的时候,要非常清楚地了解用户需求,判断需求中包含的可能的变化,从而明确在什么情况下使用开闭原则。
在实际开发过程的设计开始阶段,就要罗列出系统所有可能的行为,并把这些行为加入到抽象底层根本就是不可能的,这么去做也是不经济的,费时费力。另外,在设计开始阶段,对所有的可变因素进行预计和封装也不太现实,很难做得到。所以,开闭原则描绘的愿景只是一种理想情况或是极端状态,现实世界中是很难被完全实现的,只能在某些组件,在某种程度上符合开闭原则的要求。
通过以上对于开闭原则的分析,可以得出这样的结论:虽然我们不可能做到百分之百的封闭,但是在系统设计的时候,我们还是要尽量做到这一点。开闭原则是面向对象设计的终极目标,其他设计原则都可以看做是开闭原则的实现方法。