设计模式(Java版)
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

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 接口,规定出所有的具体类必须提供的方法的特征作为系统设计的抽象层。作为系统设计的抽象层,要预见所有可能的扩展,因此在任何扩展情况下系统的抽象底层不需要修改,从而满足了开闭原则的第二条:对修改关闭。同时,由于从抽象层导出一个或多个新的具体类可以改变系统的行为,因此系统的设计对扩展是开放的,这就满足了开闭原则的第一条。

在软件开发的过程中,一直都是提倡需求导向的。这就要求在设计的时候,要非常清楚地了解用户需求,判断需求中包含的可能的变化,从而明确在什么情况下使用开闭原则。

在实际开发过程的设计开始阶段,就要罗列出系统所有可能的行为,并把这些行为加入到抽象底层根本就是不可能的,这么去做也是不经济的,费时费力。另外,在设计开始阶段,对所有的可变因素进行预计和封装也不太现实,很难做得到。所以,开闭原则描绘的愿景只是一种理想情况或是极端状态,现实世界中是很难被完全实现的,只能在某些组件,在某种程度上符合开闭原则的要求。

通过以上对于开闭原则的分析,可以得出这样的结论:虽然我们不可能做到百分之百的封闭,但是在系统设计的时候,我们还是要尽量做到这一点。开闭原则是面向对象设计的终极目标,其他设计原则都可以看做是开闭原则的实现方法。