【OO设计原则】——02开闭原则

 

开闭原则OCP(Open Close Principle)

“Closed for Modification;Open for Extension"

一个软件实体应该对扩展开放,对修改关闭。

这里所说的软件实体包括项目中按照一定逻辑规则划分的模块、类、抽象类以及方法。

开闭原则的动机很简单:软件是变化的。一个软件实体应当对修改关闭,对扩展开放。也就是说,在设计一个模块的时候,应当对这个模块可以在不被修改的前提下被扩展。换言之,应当可以在不必修改源代码的情况下改变这个模块的行为,在保持系统一定稳定性的基础上,对系统进行扩展。这是面向对象设计(OOD)的基石,也是最重要的原则。OCP说明了软件设计应该尽可能地是架构稳定而又容易满足不同的需求。

开闭原则通俗点来说,尽量通过扩展软件的实体来实现变化,而不是通过修改已有的代码来完成变化。可以说开闭原则是对软件实体的未来变化的一种约束性的原则。

例01

以书店售书为例,其UML类图如下:


其中BookStore是书店,IBook是书籍的抽象接口,它拥有三个方法分别返回书的名称、售价和作者,NovelBook是实现IBook接口的的小说类书籍。

接口 IBook

public interface IBook {
    /**获取书籍名称*/
    public String getName();
    /**获取书籍价格*/
    public int getPrice();
    /**获取书籍作者*/
    public String getAuthor();
}

实现类NovelBook

public class NovelBook implements IBook {
    private String name;
    private int price;
    private String author;

    public NovelBook(String name, int price, String author) {
        this.name = name;
        this.price = price;
        this.author = author;
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public int getPrice() {
        return price;
    }

    @Override
    public String getAuthor() {
        return author;
    }
}

书店类BookStore

public class BookStore {
    private final List<IBook> mBookList = new ArrayList<>();

    public BookStore() {
        setBooks();
    }

    private void setBooks() {
        mBookList.add(new NovelBook("天龙八部", 3200, "金庸"));
        mBookList.add(new NovelBook("巴黎圣母院", 5600, "雨果"));
        mBookList.add(new NovelBook("悲惨世界", 3500, "雨果"));
    }

    public void printSellingBooks() {
        for (IBook iBook : mBookList) {
            System.out.println( "书籍名称:" + iBook.getName() + ", 书籍售价:" 
                    + iBook.getPrice()/100.0 + "元, 书籍作者:" + iBook.getAuthor() );
        }
    }
}

public class Main {
    public static void main(String[] args) {
        BookStore bookStore = new BookStore();
        System.out.println("---------------书店卖出的书籍记录如下---------------");
        bookStore.printSellingBooks();
    }
}

新的需求出现:

现在代码写好了,书店开始按照这个代码销售书籍,过了一段时间之后,营销状况不是很好,书店决定对书籍进行打折促销活动:40元以上的书9折销售,其它的8折销售。那么,我们的代码该如何来修改呢:

修改接口?

 在IBook上添加新的方法getOffPrice, 所有的实现类实现该方法,结果就是NovelBook要修改,我们的BookStore也要修改,显然这不是一个好办法,这破坏了设计的稳定性。

修改实现类?

 在NovelBook中修改getPrice方法进行打折处理,这也是多数人会采取的一种方法,因为简单直接,但是这样我们就需要反复的修改这一个类,如果有正常的不需要打折的书籍添加进来,那么我们getPrice方法判断逻辑就会变得非常复杂。

扩展实现类 !

通过添加一个子类OffNovelBook重写getPrice()方法来处理变化,BookStore只需生成OffNovelBook类的对象就可以,修改最小,风险也最小。类图设计如下:

子类OffNovelBook

public class OffNovelBook extends NovelBook {

    public OffNovelBook(String name, int price, String author) {
        super(name, price, author);
    }

    /**
     * 重写getPrice()实现打折处理
     */
    @Override
    public int getPrice() {
        int price = super.getPrice();
        int offPrice = 0;
        if (price > 4000) {
            //原价大于40元打9折
            offPrice = price * 90 / 100;
        } else {
            offPrice = price * 80 / 100;
        }
        return offPrice;
    }
}
public class BookStore {
    private final List<IBook> mBookList = new ArrayList<>();

    public BookStore() {
        setBooks();
    }

    private void setBooks() {
        mBookList.add(new OffNovelBook("天龙八部", 3200, "金庸"));
        mBookList.add(new OffNovelBook("巴黎圣母院", 5600, "雨果"));
        mBookList.add(new OffNovelBook("悲惨世界", 3500, "雨果"));
    }

    public void printSellingBooks() {
        for (IBook iBook : mBookList) {
            System.out.println( "书籍名称:" + iBook.getName() + ", 书籍售价:"
                    + iBook.getPrice()/100.0 + "元, 书籍作者:" + iBook.getAuthor() );
        }
    }
}

可以看到代码修改比较简单,在不影响原来NovelBook的前提下我们只是增加子类覆写了getPrice()方法进行打折处理,通过扩展完成了任务,相应的BookStore中将NovelBook替换成OffNovelBook就可以。

开闭原则告诉我们的思想是:一旦我们写出了可以正常工作的代码,就要努力保证这段代码可以一直正常工作,而不被破坏掉。当我们的编程水平和代码质量到了一定的水平,就需要提高代码自我的克制能力,当遇到问题时,优先保证之前的系统是可以work的然后再去扩展,而不是采取比较浪的方法去任意的修改,一旦你破坏了原有的结构,就引入了风险,而一旦你陷入了修改原有代码的思维模式,当你改的越来越多,在原有结构中插入越来越多的逻辑和碎片时,你会发现系统稳定性变得十分脆弱,不堪一击,稍有bug系统就会陷入尴尬

如何应用开闭原则:

抽象约束

通过接口或抽象类约束一组可能变化的行为,这包括三层含义:第一,通过接口或抽象类进行约束扩展,即对扩展进行边界限定,不允许出现在接口或抽象类中不存在的public方法;第二,抽象层即API,尽量保持稳定,一旦确定就不允许修改;第三,参数类型、引用对象尽量使用接口或抽象类,而不是实现类。

在实际开发中,大多代码结构简单,这时你会发现,一个高层调用层new创建低层类的实例,使用低层类的方法完成逻辑,以后需求有改变,只要新的低层类的方法名和旧类一样,那高层只需要修改一条new语句就可以来,类似的达到了不修改原代码的效果,多写一个sevice接口反而感觉往往多余。但是,这样完全是个人人为作用,完全不存在公约性,也无法应对更复杂业务场景。其实,我认为关键在于约束,接口是为了显性的制定规章、规则,约定了只能使用这些pulbic方法,也就制约了高层已有代码不会因低层实现类的改变而随意被更改。

举例:现在书店的例子上我们添加一种计算机书籍,它除了名称、作者、价格以外还有一个属性就是面向的领域范围。类图设计如下:

增加一个IComputerBook接口和Computer实现类之后,BookStore就可以销售计算机书籍了。相关代码:

public interface IComputerBook extends IBook {
    //范围领域
    public String getScope();
}
public class ComputerBook implements IComputerBook {
    private String name;
    private int price;
    private String author;
    private String scope;

    @Override
    public String getName() {
        return name;
    }

    @Override
    public int getPrice() {
        return price;
    }

    @Override
    public String getAuthor() {
        return author;
    }

    @Override
    public String getScope() {
        return scope;
    }
}
public class BookStore {
    private final List<IBook> mBookList = new ArrayList<>();

    public BookStore() {
        setBooks();
    }

    private void setBooks() {
        mBookList.add(new NovelBook("天龙八部", 3200, "金庸"));
        mBookList.add(new NovelBook("巴黎圣母院", 5600, "雨果"));
        mBookList.add(new NovelBook("悲惨世界", 3500, "雨果"));
        mBookList.add(new ComputerBook("Think in Java", 4300, "Bruce Eckel", "编程语言"));
    }

    public void printSellingBooks() {
        for (IBook iBook : mBookList) {
            System.out.println( "书籍名称:" + iBook.getName() + ", 书籍售价:"
                    + iBook.getPrice()/100.0 + "元, 书籍作者:" + iBook.getAuthor() );
        }
    }
}

这个代码中我们可以看到ComputerBook必须实现来自IBook接口的三个方法约束,因为IComputerBook继承了IBook接口,也就是说IBook对我们新增加的ComputerBook类产生了约束力,还有我们的mBookList是定义成接口的持有而不是实现类,如果你改成List<NovelBook> mBookList = new ArrayList<>()你会发现根本无法往里面添加ComputerBook对象,再者我们的getScope()方法能不能直接添加到IBook接口上而不用新增ComputerBook子类呢,当然不能,因为IBook接口影响的子类NovelBook已经在销售运行中,如果动IBook那么所有NovelBook势必要受到影响。所以这就是所谓的通过抽象约束来实现开闭原则。

封装变化

这个在单一职责中就提到过,封装变化包含两层含义:

第一,将相同的变化封装到一个接口或抽象类当中,第二,将不同的变化封装到不同的接口或抽象类当中,不应该有两个不同的变化出现在同一个接口或抽象类当中。

总结

开闭原则体现的OO特征:封装、继承、抽象、多态

开闭原则的好处:

  • 提高可复用性 , 因为我们是对修改关闭,也就是说可以工作的逻辑代码高度集中并且基本不会变的,这部分代码就可以拿来复用的,这也正是我们封装的目标之一。
  • 提高可维护性,遵循开闭原则的一个结果就是每次的修改都不会对之前的代码造成任何影响,那么维护人员也就无需关心之前的代码会出问题,只需要把精力放到本次的扩展修改的代码上。
  • 提高软件的安定性、稳定性。既然每次的修改都不会对之前的代码造成任何影响,那么对原软件机能影响很小,确保了安全性,同时也保证了原有机能的稳定性。

 

 

猜你喜欢

转载自blog.csdn.net/qq_42022528/article/details/86560396