个人博客:打开链接
开闭原则(Open Closed Principle, OCP)的定义
Software entities like classes, modules and functions should be open for extension but closed for modifications.(一个软件实体如类,模块和函数应该对扩展开放,对修改关闭。)
开闭原则告诉我们应尽量通过扩展实体的行为来实现变化,而不是通过 修改已有的代码来实现变化,它是为软件实体的未来事件而制定的对现行开发设计进行约束的一个原则。
以书店销售书籍为例,其类图为:
IBook定义了数据的三个属性:名称、价格和作者。小说类NovelBook是一个具体的实现类,是所有小说书籍的总称,BookStore指的是书店,IBook接口代码如下:
public interface IBook {
//书籍有名称
public String getName();
//书籍有售价
public int getPrice();
//书籍有作者
public String getAuthor();
}
小说书籍的源代码如下:
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<IBook> bookList = new ArrayList<IBook>();
//静态模块初始化,项目中一般是从持久层初始化产
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)+"元");
}
}
}
项目投产,书店盈利,但为扩大市场,书店决定,40元以上打9折,40元以下打8 折。如何解决这个问题呢?
修改接口。在IBook上新增加一个方法getOffPrice(),专门进行打折,所有实现类实现这个方法。但是这样修改的后果就是实现类NovelBook要修改,BookStore中的main方法也修改,同时Ibook作为接口应该是稳定且可靠的,不应该经常发生变化,否则接口做为契约的作用就失去了效能,因此该方案被否定。
修改实现类。修改NovelBook 类中的方法,直接在getPrice()中实现打折处理,这个应该是大家在项目中经常使用的就是这样办法,通过class文件替换的方式可以完成部分业务(或是缺陷修复)变化,该方法在项目有明确的章程(团队内约束)或优良的架构设计时,是一个非常优秀的方法,但是该方法还是有缺陷的,例如采购书籍人员也是要看价格的,由于该方法已经实现了打折处理价格,因此采购人员看到的也是打折后的价格,这就产生了信息的蒙蔽效果,导致信息不对称而出现决策失误的情况。该方案也不是一个最优的方案。
通过扩展实现变化。增加一个子类 OffNovelBook,覆写getPrice方法,高层次的模块(也就是static静态模块区)通过OffNovelBook类产生新的对象,完成对业务变化开发任务。好办法,风险也小,我们来看类图:
OffNovelBook类继承了NovelBook,并覆写了getPrice方法,不修改原有的代码。我们来看看新增加的子类OffNovelBook:
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;
}
}
很简单,仅仅覆写了getPrice方法,通过扩展完成了新增加的业务。 然后我们来看BookStore类的修改:
public class BookStore {
private final static ArrayList<IBook> bookList = new ArrayList<IBook>();
//静态模块初始化,项目中一般是从持久层初始化产
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)+"元");
}
}
}
归纳变化:
逻辑变化。只变化一个逻辑,而不涉及到其他模块,比如原有的一个算法是a*b+c,现在要求a*b*c,可能通过修改原有类中的方法方式来完成,前提条件是所有依赖或关联类都按此相同逻辑处理。
子模块变化。一个模块变化,会对其他模块产生影响,特别是一个低层次的模块变化必然引起高层模块的变化,因此在通过扩展完成变化时,高层次的模块修改是必然的,刚刚的书籍打折处理就是类似的处理模块,该部分的变化甚至引起界面的变化。
可见视图变化。可见视图是提供给客户使用的界面,该部分的变化一般会引起连锁反应(特别是在国内做项目,做欧美的外包项目一般不会影响太大),如果仅仅是界面上按钮、文字的重新排布倒是简单,最司空见惯的是业务耦合变化,什么意思呢?一个展示数据的列表,按照原有的需求是六列,突然有一天要增加一列,而且这一列要跨度N张表,处理M个逻辑才能展现出来,这样的变化是比较恐怖的,但是我们还是可以通过扩展来完成变化,这就依赖我们原有的设计是否灵活。
拥抱变化
在上例中,书店又增加了计算机类书籍,该类书还有一个独特特性:面向的是什么领域,修改后的类图如下:
增加了一个接口IcomputerBook和实现类ComputerBook,而BookStore不用做任何修改就可以完成书店销售计算机书籍的业务,我们来看源代码:
计算机书籍接口:
public interface IComputerBook extends IBook{
//计算机书籍是有一个范围
public String getScope();
}
很简单,计算机数据增加了一个方法,就是获得该书籍的范围,同时继承IBook接口,毕竟计算机书籍也是书籍。其实现类如下:
public class ComputerBook implements IComputerBook {
private String name;
private String scope;
private String author;
private int price;
public ComputerBook(String _name,int _price,String _author,String _scope){
this.name=_name;
this.price = _price
this.author = _author;
this.scope = _scope;
}
public String getScope() {
return this.scope;
}
public String getAuthor() {
return this.author;
}
public String getName() {
return this.name;
}
public int getPrice() {
return this.price;
}
}
也很简单,实现IcomputerBook就可以,而BookStore类没有做任何的修改,只是在static静态模块中增加一条数据,代码如下:
public class BookStore {
private final static ArrayList<IBook> bookList = new ArrayList<IBook>();
// 静态模块初始化,项目中一般是从持久层初始化产
static {
bookList.add(new OffNovelBook("天龙八部", 3200, "金庸"));
bookList.add(new OffNovelBook("巴黎圣母院", 5600, "雨果"));
bookList.add(new OffNovelBook("悲惨世界", 3500, "雨果"));
bookList.add(new OffNovelBook("金瓶梅", 4300, "兰陵笑笑生"));
// 增加计算机书籍
bookList.add(new ComputerBook("Think in Java", 4300, "Bruce Eckel",
"编程语言"));
}
// 模拟书店买书
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) + "元");
}
}
}
可以看出,这样做简单而且不需要与其他业务进行耦合。