设计模式六大原则(六)---开闭原则

150 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第16天,点击查看活动详情

开闭原则

开闭原则是对另外五个原则的总结,也可以说另外五个原则是服务于开闭原则。

定义

一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。

软件实体指的是什么?

  • 项目或产品按一定逻辑划分的模块
  • 抽象(抽象类、接口)和类(实现类、具体类)
  • 方法

对扩展开放

当需求变更或同一类的需求添加时,当前软件实体可以通过扩展来适应新的需求

对修改关闭

一个软件实体必然会与其他软件实体耦合,如果修改一个低层次模块往往会影响高层次模块的功能,所以一个软件实体尽量是可拓展的,通过拓展避免修改带来的危害。

通过继承拓展

新建的类可以通过继承的方式来重用原类的代码,并在之上做扩展。

此方式可以认为违反了里氏替代原则。但是某种意义上来说,不应该从多态的角度来看父子关系,而是从扩展的角度来看,这两个类是不同的。

通过抽象拓展

通过抽象拓展这里指的是抽象类或接口,此方式倡导对抽象类或接口的继承。接口可以通过继承来重用,但是实现不必重用。已存在的接口对于修改是封闭的。

这样实现就很好,原投入生产使用的类不用做任何修改,自然也不会出现错误。这也是提倡的写法。

如何实现开闭原则

需求总是变化的

首先需求总是变化的,不可能将需求全部确定才会进行编码,所以在软件设计的时候就应该预见修改的可能,并且使得软件实体可以适应变化。多扩展、少修改。

抽象&依赖

面向抽象编程,而不是面向细节编程。通过抽象方法制定契约,并且契约不必要时不要修改。

既然需求是永远变化的,而我们又不想让软件实体频繁改动,那么我们就得制定‘契约’。接口和抽象类(实现类必须重写所有的抽象方法),并且一旦接口中的抽象方法制定,实现类是不可以随意改动的。相反如若想添加一个功能,就可以直接在接口中添加,在实现类实现就行,易于拓展。

如何实现开闭原则

  • 面向抽象

    通过接口或者抽象类约束扩展,对扩展进行边界限定,不允许出现在接口或抽象类中不存在的public方法

    参数类型、引用对象尽量使用接口或者抽象类,而不是实现类

    抽象层尽量保持稳定,一旦确定即不允许修改。

  • 制定项目章程

    一个团队中,约定大于配置。

案例分析

书本、书店、采购者。

定义一个书本接口:

public interface IBook {
    //约束书本基本功能
    /**
     * 价格
     */
    Integer getPrice();

    /**
     * 作者
     */
    String getAuthor();

    /**
     * 名称
     */
    String getName();
}

这是一个小说实现类:

@Data
@AllArgsConstructor
public class NovelBook implements IBook {

    private Integer price;
    private String author;
    private String name;

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

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

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

一个书店类:

public class BookStore {
    private List<IBook> list = new ArrayList<>();
    public void init(){
        list.add(new NovelBook(100,"bookname1","bookauthor1"));
        list.add(new NovelBook(100,"bookname2","bookauthor2"));
        list.add(new NovelBook(100,"bookname3","bookauthor3"));
        list.add(new NovelBook(100,"bookname4","bookauthor4"));

    }
    public void sell(List<IBook> list){
        list.forEach(book -> {
            System.out.println("卖书:"
                    +book.getName()
                    +"\t\t" +book.getAuthor()
                    +"\t\t"+book.getPrice());
        });
    }
    @Test
    public void test(){
        BookStore bookStore = new BookStore();
        bookStore.init();
        bookStore.sell(bookStore.list);
    }
}

采购者:

public class Buyer {
    private List<IBook> list = new ArrayList<>();
    public void init(){
        list.add(new NovelBook(100,"bookname1","bookauthor1"));
        list.add(new NovelBook(100,"bookname2","bookauthor2"));
        list.add(new NovelBook(100,"bookname3","bookauthor3"));
        list.add(new NovelBook(100,"bookname4","bookauthor4"));
    }
    public void buy(List<IBook> list){
        list.forEach(book -> {
            System.out.println("采购书:"
                    +book.getName()
                    +"\t\t" +book.getAuthor()
                    +"\t\t"+book.getPrice());
        });
    }
}

此刻书点需要添加一个新的业务,大于100块钱的小说打九折销售,其余原价。我们如何实现?

方式一:直接修改getPrice()方法逻辑。

这样书店确实可以实现,但是对于采购者来说呢?采购者希望看到的是打折前的价格。

方式二:在IBook接口中新增获取打折后的价格的方法:getDiscountPrice()。

首先对于上层模块来说,只需要调整上层模块的调用方式即可。但是对于接口而言呢?接口是一个约定,非必要情况下投入生产使用的接口不要随意改变。

方式三:通过扩展实现

通过继承实现扩展

我i们可以写一个DiscountNovelBook继承NovelBook复写里面的getPrice()方法。这样我们就改变集合内的书籍实现类修改为DiscountNovelBook即可,其调用方式也不变。且不会影响采购者的代码逻辑。

public class DiscountNovelBook extends NovelBook {
    public DiscountNovelBook(Integer price, String author, String name) {
        super(price, author, name);
    }

    //复写getPrice方法
    @Override
    public Integer getPrice() {
        final Integer price = super.getPrice();
        Integer rPrice = 0;
        if (price > 50) {
            rPrice = price / 10 * 9;
        } else rPrice = price;
        return rPrice;
    }
}