插图来自
看到这篇文章的标题时您可能会觉得有些奇怪,这个系列不是讲Spring吗?OCP(Open-Closed Principle)是什么东西?先别急,在进行Spring的讲解之前,有必要对一些基础知识做出讲解。首先,便已经进入了Java的世界。相信您已经很清楚Java是一门纯面向对象的语言。既然我们在使用一门设计得非常棒的面向对象语言Java,并准备使用它来进行实际的工作,那么我们就不得不提面向对象设计领域(简称OOD,即Object-Oriented Design)最重要的设计原则:OCP原则,全称就是The Open-Closed Principle,中文翻译为开-闭原则。OCP在面向对象程序设计中是一项非常重要的指导原则。有些人甚至在讨论面向对象时,将这个原则比拟为牛顿力学中的三项基本定理。可见其重要性。换句话说,如果您在写C++ 或是Java这类的面向对象程序语言,却不晓得何谓OCP 。恐怕写出来的程序只有面向对象的表象而未能得精髓。Bertrand Meyer在1988年时曾经对于OOD写下了一段话,而这句话正是今日所要讨论的OCP之精髓所在:
SOFTWARE ENTITIES (CLASSES, MODULES, FUNCTIONS, ETC.) SHOULD BE OPEN FOR EXTENSION, BUT CLOSED FOR MODIFICATION.
这句话翻译字面上的意思要理解起来并不困难。简单的说就是保持扩充的弹性及开放性,而对于修改要加以封装。要如何将这个观念应用在实际的程序中呢?请想想看,如果今天有个程序库让您用现有的面向对象程序语言撰写,并且告诉您说这个程序库的需求有可能在进行中或是完成后继续扩充。您要如何来设计这个程序呢? 若是使用非面向对象语言,这个地方很可能会变得很麻烦。但是,对于面向对象语言而言,您所要做的就是设计适当的类别,并且在需要扩充功能的时候给予适当的 继承,然后将继承下来的部分加以扩充。 至于Closed for modification 呢?其实,这部分的要求就是要让程序各模块间对于另一方的修改所引发的影响能够降到最低。简单地说,如果您有一个模块修改其行为,结果却造成程序中所有用到该模块的部分必需全部修正其程序码。那么您就可说您的程序并没有达到OCP中的Closed for modification的要求。稍微眼尖一点的人可能会注意到一件事情,要求程序具有扩充性,可是又不能让部分程序修改时造成对其它模块的影响,这是个矛盾的问题。若对面向对象设计有所了解者,可能会想到,这不就是一般Class的基本要求吗?是否一般Class真的能够满足我们的需求呢?且让我们看看一个非常简单的范例。我们来做一个报告打印程序,这个程序有两Class,一个是报告Report,一个是打印机Printer。
public class Printer { public void output(String data) { System.out.println("Printer is printing: " + data); } } public class Report { private String content; private Printer printer; public Report(String content) { super(); printer = new Printer(); this.content = content; } public void print() { printer.output(content); } }
?
?
?
这样的设计与OCP原则有什么抵触呢?首先,因为在Report类中,直接使用了Printer类别及相应的output方法。当Printer这个类别变动时,如output方法发生了变更,Printer的变动将直接影响到Report类,这就不符合对修改要加以封装的原则。此外更严重的是,如果直接在Report中嵌入Printer,当业务需求发生变动,如Report的print方法改为向大屏幕输出,而不是输出至打印机打印时,这样的设计就会造成很大的问题。这时新手会做的事情会是首先创建一个新的大屏幕的类别:
public class Screen { public void output(String data) { System.out.println("Screen output: " + data); } }
?
然后在Report里使用它:
public class Report { private String content; private Screen screen; private Printer printer; public Report(String content) { super(); screen = new Screen(); printer = new Printer(); this.content = content; } public void printWithPrinter() { printer.output(content); } public void printWithScreen() { screen.output(content); } }
?
上面的程序似乎解决了我们的问题,其实不然!它反而制造了更多的问题。如果我们对于Report又有了别的终端输出设备,这样一个一个加上去,Report里就会有越来越多的输出方法。并且这样的设计完全地违背了OCP的设计原则。因为,新增的Screen造成了Report内部的大变动。在面向对象的程序设计中,要维护OCP原则,最重要的是要掌握抽象化介面的制作,对于Java就是要学会使用接口类interface。让我们对上面的例子稍做修改,首先要新建一个interface接口类,叫做OutputDevice,这个接口定义一个output方法:
public interface OutputDevice { public void output(String data); }
?
然后是改造Screen及Printer,使之实现这个接口:
public class Printer implements OutputDevice { public void output(String data) { System.out.println("Printer is printing: " + data); } } public class Screen implements OutputDevice { public void output(String data) { System.out.println("Screen output: " + data); } }
?
最后,我们要让Report从对Printer及Screen的依赖中解脱出来:?
public class Report { private String content; private OutputDevice outputDevice; public Report() { }; public Report(OutputDevice outputDevice, String content) { super(); this.outputDevice = outputDevice; this.content = content; } public void print() { outputDevice.output(content); } }
?
?
在这个修改过的例子中,Report不再去关心它用什么设备去输出,通过接口,Report只需要知道系统会分配给它一个合适的outputDevice,而它只需要调用outputDevice的output方法就可以了。而outputDevice是在运行时根据业务需要来分配的,下面来看一个使用的例子:
public class Reporting { /** * @param args */ public static void main(String[] args) { Printer printer = new Printer(); Screen screen = new Screen(); Report report = new Report(printer, "Hello, world!"); Report report2 = new Report(screen, "Hi, world!"); report.print(); report2.print(); } }
?
?
程序输出如下:
1 Printer is printing: Hello, world!?
2 Screen output: Hi, world!
如果今后再多一种终端输出设备,也不会导致Report的变动。我们可以说这样的程序设计满足了OCP的设计要求。此外,为了满足OCP的设计原则,我们所有的类别的内部成员都是private类型,如果我们在类别内部使用了public类型的变量成员,那么当这个类的这个成员发生变动时,必然导致外部调用这个类的这个成员的代码发生变动。因此在OCP的原则下,将仅允许类别中存在private或是protected的变量成员,而public往往和static final一起使用,用于定义放在内存中的静态只读常量,我们在整本书中都将遵守这一原则。那么对于那些private变量成员,外部如果确实需要调用,这种情况该怎么办呢?答案是使用getter及setter属性来进行封装:
public class Report { private String content; private OutputDevice outputDevice; public Report() { }; public Report(OutputDevice outputDevice, String content) { super(); this.outputDevice = outputDevice; this.content = content; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } public OutputDevice getOutputDevice() { return outputDevice; } public void setOutputDevice(OutputDevice outputDevice) { this.outputDevice = outputDevice; } public void print() { outputDevice.output(content); } }
?
?
而在外部调getter及setter方法来操作这些类成员:?
public class Reporting { /** * @param args */ public static void main(String[] args) { Printer printer = new Printer(); Screen screen = new Screen(); Report report = new Report(); report.setOutputDevice(printer); report.setContent("Hello, World!"); report.print(); report.setOutputDevice(screen); report.print(); } }
?
程序输出如下:?
1 Printer is printing: Hello, world!?
2 Screen output: Hi, world!
或许您会说这个观念我在学习抽象类别及Java的Interface时就知道了。其实,您所学的仅是工具的用法。但是,是否学习到使用的时机呢? 大部分学过C++的人都会使用抽象类别,学Java的人也都知道Interface为何及如何使用,是许多人在实际写程序时,却都不使用他们。并不是他们不会用,而 是不知道要在何时使用。 时时将OCP谨记于心,编程时处处不要忘记这个原则。多看一些OCP的实例,借以了解使用OCP的适当时机,这样写出来的程序自然就可符合OCP。唯有符合OCP原则的程序才能称的上是一个面向对象的程序。如果程序处处违反OCP原则,一个模块的变动,会牵动许许多多的模块,这样的程序将完全失去面向对象的精神。网络上,有许多深入讨论OCP的相关文章,很多对于面向对象设计的研究都直接将OCP的想法融入其中。因此,建议您先清楚的了解OCP的精神,再去看一些关于面向对象设计资料,您会发现许多研究所做的努力都仅仅是要达成OCP的要求而已,而Spring就是最好的例子。
下面,我们将上面实现OCP原则所使用的技术整理成一句口诀1:
1 层层之间用接口,类的属性要封装。
我们会在随后的学习过程中处处渗透OCP原则,并遵守这一口诀
?