持续创作,加速成长!这是我参与「掘金日新计划 · 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;
}
}