开闭原则的定义
在哲学上,矛盾法则即对立统一法则,是唯物辨证法的最根本法则。开闭原则是Java世界里最基本的设计原则,它指导我们如何建立一个稳定的、灵活的系统,先来看开闭原则的定义:
Software entities like classes,modules and functions should be open for extension but closed for modifications. (一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。)
初看这个定义可能会很疑惑,对扩展开放?开放什么?对修改关闭,怎么关闭?
我们做一件事,或者选择一个方向,一般需要经历三个步骤:What——是什么,why——为什么,How——怎么办(简称3W原则)。对于开闭原则,我们也采用这三步来分析,即什么是开闭原则,为什么要使用开闭原则,怎样使用开闭原则。
开闭原则的庐山真面目
开闭原则的定义已经非常明确地告诉我们:软件实体应该对扩展开发,对修改关闭,其含义是说一个软件实体应该通过扩展来实现变化,而不是通过修改已有代码来实现变化。那什么是软件实体?软件实体包括以下几个部分:
一个软件产品只要在生命期内,都会有变化,既然变化是一个既定的事实,我们就应该在变化时尽量适应这些变化,以提高项目的稳定性和灵活性,真正实现“拥抱变化”。开闭原则告诉我们应尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化,它是为软件实体的未来事件而制定的对现行开发设计进行约束的一个原则。我们举例来说明什么是开闭原则,以书店销售书籍为例,其类图如下:
IBook定义了数据的三个属性:名称、价格和作者。小说NobelBook是一个具体的实现类,是所有小说书籍的总称,BookStore指的是书店。
代码清单1:IBook.java (书籍接口)
/** * * @author Barudisshu */ public interface IBook { //书籍有名称 public String getName(); //书籍有售价 public int getPrice(); //书籍有作者 public String getAuthor(); }
代码清单2:NovelBook.java (小说类)
/** * * @author Barudisshu */ 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; } }
代码清单3:BookStore.java (书店售书类)
import java.text.NumberFormat; import java.util.ArrayList; /** * * @author Barudisshu */ public class BookStore { private final static ArrayList<IBook> 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书籍价格:" + formatter.format(book.getPrice() / 100.0) + "元" + "\t书籍作者:" + book.getAuthor()); } } }
在BookStore中声明了一个静态模块,实现了数据的初始化,这部分应该是从持久层产生的,由持久层框架进行管理,如下结果如下:
项目投产了,书籍正常销售出去,书店也盈利了。从2008年开始,全球经济开始下滑,对零售业影响比较大,书店为了生存开始打折销售;所有40元以上的书籍9折销售,其他的8折销售。对已经投产的项目来说,这就是一个变化,我们应该如何应对这样一个需求变化?有如下三个方法可以解决这个问题:
OffNovelBook类继承了NovelBook,并覆写了getPrice方法,不修改原有的代码。
代码清单4:OffNovelBook.java (打折销售的小说类)
/** * * @author Barudisshu */ 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; //原价大于40元,则打9折,否则打8折 if (selfPrice > 4000) { offPrice = selfPrice * 90 / 100; } else { offPrice = selfPrice * 80 / 100; } return offPrice; } }
代码清单5:BookStore.java (书店打折销售类)
import java.text.NumberFormat; import java.util.ArrayList; /** * * @author Barudisshu */ public class BookStore { private final static ArrayList<IBook> 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书籍价格:" + formatter.format(book.getPrice() / 100.0) + "元" + "\t书籍作者:" + book.getAuthor()); } } }
修改后运行结果如下:
OK,打折销售开发完成了。看到这里,各位可能会有想法:增加一个OffNovelBook类后,你的业务逻辑还是修改了,你修改了static静态模块区域。这部分确实修改了,该部分属于高层次的模块,是由持久层产生的,在业务规则改变的情况下高层模块必须有部分改变以适应新业务,改变要尽量地少,防止变化风险的扩散。
开闭原则对扩展开放,对修改关闭,并不意味着不做任何修改,低层模块的变更,必然要有高层模块进行耦合,否则就是一个孤立无意义的代码片段。
我们可以把变化归纳为以下三种类型:
为什么要采用开闭原则
开闭原则是非常重要的,可通过以下几个方面来理解其重要性。
import junit.framework.TestCase; import ocp.section2.IBook; import ocp.section2.OffNovelBook; /** * * @author Barudisshu */ public class OffNovelBookTest extends TestCase { private IBook below40NovelBook = new OffNovelBook("平凡的世界", 3000, "路遥"); private IBook above40NovelBook = new OffNovelBook("平凡的世界", 6000, "路遥"); //测试低于40元的数据是否打8折 public void testGetPriceBelow40() { TestCase.assertEquals(2400, this.below40NovelBook.getPrice()); } //测试大于40的书籍是否是打9折 public void testGetPriceAbove40() { TestCase.assertEquals(5400, this.above40NovelBook.getPrice()); } }
如何使用开闭原则
开闭原则是一个非常虚的原则,开闭原则并没有具体的解析,它“虚”得没有边界。