开闭原则的定义:
Software entities like classes,modules and functions should be open for extension but closed for modifications.(一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。) |
---|
在开发软件的过程中,因为变化 、升级和维护等原因需要对软件原有的代码进行修改,可能会将错误引入原本已经测试过的旧代码中,破坏原有的系统,因此,当软件需求变化时,我们应尽量运用扩展的方式来实现变化,而不是修改原来的代码。
以书店销售书籍为例:
6-1:书店售书类图
书籍接口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;
}
//获得作者是谁
public String getAuthor() {
return this.author;
}
//书籍叫什么名字
public String getName() {
return this.name;
}
//获得书籍的价格
public int getPrice() {
return this.price;
}
}
书店售书类:
public class BookStore {
private final static ArrayList bookList = new ArrayList();
//static静态模块初始化数据,实际项目中一般是由持久层完成
static{
bookList.add(new NovelBook("天龙八部",3200,"金庸"));
bookList.add(new NovelBook("巴黎圣母院",5600,"雨果"));
bookList.add(new NovelBook("悲惨世界",3500,"雨果"));
bookList.add(new NovelBook("金*梅",4300,"兰陵笑笑生"));
}
//模拟书店买书
public static void main(String[] args) {
NumberFormat formatter = NumberFormat.getCurrencyInstance();
formatter.setMaximumFractionDigits(2);
System.out.println("-----------书店卖出去的书籍记录如下:-----------");
for(IBook book:bookList){
System.out.println("书籍名称:" + book.getName()+"\t书籍作者:" +book.getAuthor()+"\t书籍价格:"+ formatter.format (book.getPrice()/100.0)+"元");
}
}
}
运行结果:
-----------------书店卖出去的书籍记录如下:--------------
书籍名称:天龙八部 书籍作者:金庸 书籍价格:¥25.60元
书籍名称:巴黎圣母院 书籍作者:雨果 书籍价格:¥50.40元
书籍名称:悲惨世界 书籍作者:雨果 书籍价格:¥28.00元
书籍名称:金*梅 书籍作者:兰陵笑笑生 书籍价格:¥38.70元
现在新的需求来了,受移动互联网发展的影响,书店必须打折来维持书店的生存。所有40元以上的书籍9折销售,其他的8折销售。
该如何实现这个变化,有三种方式:
● 修改接口
在IBook上新增加一个方法getOffPrice(),专门用于进行打折处理,所有的实现类实现该方法。但是这样修改的后果就是,实现类NovelBook要修改,BookStore中的main方法也修改,同时IBook作为接口应该是稳定且可靠的,不应该经常发生变化,否则接口作为契约的作用就失去了效能。因此,该方案否定。
● 修改实现类
修改NovelBook类中的方法,直接在getPrice()中实现打折处理。该方法在项目有明确的章程(团队内约束)或优良的架构设计时,是一个非常优秀的方法,但是该方法还是有缺陷的。例如采购书籍人员也是要看价格的,由于该方法已经实现了打折处理价格,因此采购人员看到的也是打折后的价格,会因信息不对称而出现决策失误的情况。因此,该方案也不是一个最优的方案。
● 通过扩展实现变化
增加一个子类OffNovelBook,覆写getPrice方法,高层次的模块(也就是static静态模块区)通过OffNovelBook类产生新的对象,完成业务变化对系统的最小化开发。好办法,修改也少,风险也小,修改后的类图如图6-2所示。
6-2:书店售书类图
打折销售的小说类OffNovelBook: 仅仅覆写了getPrice方法,通过扩展完成了新增加的业务。
public class OffNovelBook extends NovelBook {
public OffNovelBook(String _name,int _price,String _author){
super(_name,_price,_author);
}
//覆写销售价格
@Override
public int getPrice(){
//原价
int selfPrice = super.getPrice();
int offPrice=0;
if(selfPrice>4000){ //原价大于40元,则打9折
offPrice = selfPrice * 90 /100;
}else{
offPrice = selfPrice * 80 /100;
}
return offPrice;
}
}
店打折销售类: 需要依赖子类,稍作修改。
public class BookStore {
private final static ArrayList bookList = new ArrayList();
//static静态模块初始化数据,实际项目中一般是由持久层完成
static{
bookList.add(new OffNovelBook("天龙八部",3200,"金庸"));
bookList.add(new OffNovelBook("巴黎圣母院",5600,"雨果"));
bookList.add(new OffNovelBook("悲惨世界",3500,"雨果"));
bookList.add(new OffNovelBook("金*梅",4300,"兰陵笑笑生"));
}
//模拟书店买书
public static void main(String[] args) {
NumberFormat formatter = NumberFormat.getCurrencyInstance();
formatter.setMaximumFractionDigits(2);
System.out.println("-----------书店卖出去的书籍记录如下:-----------");
for(IBook book:bookList){
System.out.println("书籍名称:" + book.getName()+"\t书籍作者:" + book.getAuthor()+ "\t书籍价格:" + formatter.format (book.getPrice()/100.0)+"元");
}
}
}
运行结果:
----------------------书店卖出去的书籍记录如下:---------------------
书籍名称:天龙八部 书籍作者:金庸 书籍价格:¥25.60元
书籍名称:巴黎圣母院 书籍作者:雨果 书籍价格:¥50.40元
书籍名称:悲惨世界 书籍作者:雨果 书籍价格:¥28.00元
书籍名称:金*梅 书籍作者:兰陵笑笑生 书籍价格:¥38.70元
开闭原则对扩展开放,对修改关闭,并不意味着不做任何修改,低层模块的变 更,必然要有高层模块进行耦合,否则就是一个孤立无意义的代码片段。
开闭原则是最基础的一个原则,其余的原则都是开闭原则的具体形态, 也就是说其余五个原则就是指导设计的工具和方法。换一个角度 来理解,依照Java语言的称谓,开闭原则是抽象类,其他五大原则是具体的实现类。
可通过以下几个方面来理解其重要性:
以上面提到的书店售书为例,IBook接口写完了,实现类NovelBook也写好了,需要写一个测试类进行测试。
小说类的单元测试:
public class NovelBookTest extends TestCase {
private String name = "平凡的世界";
private int price = 6000;
private String author = "路遥";
private IBook novelBook = new NovelBook(name,price,author);
//测试getPrice方法
public void testGetPrice() {
//原价销售,根据输入和输出的值是否相等进行断言
super.assertEquals(this.price, this.novelBook.getPrice());
}
}
一般一个方法的测试方法一般不少于3种——首先是正常的业务逻辑要保证测试到,其次是边界条件要测试到,然后是异常要测试到,比较重要的方法的测试方法甚至有十多种,而且单元测试是对类的测试,类中的方法耦合是允许的,在这样的条件下,如果再想着通过修改一个方法或多个方法代码来完成变化,是很难做到的。
如果用扩展的方式,新增加的类,新增加的测试方法,只要保证新增加类是正确的就可以了。
在面向对象的设计中,所有的逻辑都是从原子逻辑组合而来的,而不是在一个类中独立实现一个业务逻辑。只有这样代码才可以复用,粒度越小,被复用的可能性就越大。那为什么要复用呢?减少代码量,避免相同的逻辑分散在多个角落,避免日后的维护人员为了修改一个微小的缺陷或增加新功能而要在整个项目中到处查找相关的代码。那怎么才能提高复用率呢?缩小逻辑粒度,直到一个逻辑不可再拆分为止。
一款软件投产后,维护人员的工作不仅仅是对数据进行维护,还可能要对程序进行扩展,维护人员最更意做的事情就是扩展一个类,而不是修改一个类。
万物皆对象,我们需要把所有的事物都抽象成对象,然后针对对象进行操作,但是万物皆运动,有运动就有变化,有变化就要有策略去应对,怎么快速应对呢?这就需要在设计之初考虑到所有可能变化的因素,然后留下接口,等待“可能”转变为“现实”。
开闭原则是一个比较抽象的原则,前面5个原则是对开闭原则的具体解释,但是开闭原则并不局限于这么多,它更多地像一句口号,一个目标,而没有提出具体的实现办法。这就需要自己在工作中领会精神,总结办法。
通过接口或抽象类可以约束一组可能变化的行为,并且能够实现对扩展开放,其包含三层含义:第一,通过接口或抽象类约束扩展,对扩展进行边 界限定,不允许出现在接口或抽象类中不存在的public方法;第二,参数类型、引用对象尽 量使用接口或者抽象类,而不是实现类;第三,抽象层尽量保持稳定,一旦确定即不允许修改。
尽量使用元数据来控制程序的行为,减少重复开发。什么是元数据?用来描述环境和数据的数据,通俗地说就是 配置参数,参数可以从文件中获得,也可以从数据库中获得。举个非常简单的例子,login方 法中提供了这样的逻辑:先检查IP地址是否在允许访问的列表中,然后再决定是否需要到数 据库中验证密码(,该行为就是一个 典型的元数据控制模块行为的例子。
在一个团队中,建立项目章程是非常重要的,因为章程中指定了所有人员都必须遵守的 约定,对项目来说,约定优于配置。
对变化的封装包含两层含义:第一,将相同的变化封装到一个接口或抽象类中;第二, 将不同的变化封装到不同的接口或抽象类中,不应该有两个不同的变化出现在同一个接口或 抽象类中。封装变化,也就是受保护的变化(protected variations),找出预计有变化或不稳 定的点,为这些变化点创建稳定的接口,准确地讲是封装可能发生的变化,一旦预测到 或“第六感”发觉有变化,就可以进行封装,23个设计模式都是从各个不同的角度对变化进行 封装的。
参考:
【1】:《设计模式之禅》 【2】:面向对象六大原则之开闭原则 【3】:为什么要采用开闭原则 【4】:《大话设计模式》