最大化JAVA代码的可重用性
【打印文章】
——克服传统OO方法在重用方面的缺陷
摘要:不要放弃编写可重用代码的努力!本文介绍了三种对现有代码进行修改以提高其可重用性的方法。
在程序员中似乎存在着一种日益普遍的观点,认为重用只是一个神话。或许是传统的面向对象编程方法中所存在的不足增加了重用的困难。本文介绍了从另外一种不同的途径使重用成为可能的三个步骤。
第一步:将功能实现从类实例的方法中移出
由于缺乏精确性,类继承不是非常理想的代码重用机制。换句话说,如果不继承一个类的数据成员和其他的方法,那么你就无法重用这个类的某个单独的方法。这些额外的不必要的负担使方法重用的代码变得复杂。派生类对其父类的依赖性也以入了额外的复杂性:对父类的改动会对子类造成影响;当修改任意一个类的时候,我们很难记得清哪个方法被覆盖,哪个没有;而且被覆盖的方法是否会调用父类中相应的方法并不非常清晰地显现。
任何执行单一概念任务的方法应该能够成为代码用的首选而独立存在。为了达到这个目标,我们必须会到过程化的编程模式,将代码从类实例的方法中移出,形成具有全局可见性的过程。为了提高这种过程的可重用性,过程代码应该象静态的通用方法一样编写:每个过程只能使用自己的输入参数,只能调用其他全局性的过程完成其工作,不能使用任何非本地的变量。这种对外部依赖的简化降低了过程使用的复杂性,也增加了在其他地方使用此过程的可能性。当然,由于其结构通常会变得更为清晰,即使抛开重用的目的不谈我们也可以从这种代码的组织方式中受益。
在Java中,方法不能脱离类而单独存在。因此,你可以对相关的过程进行组织并使它们成为一个独立的类中的公共静态方法。例如,对于如下所示的一个类:
class Polygon{
…
public int getPerimeter(){…}
public boolean isConvex(){…}
public Boolean containsPoint(Point p){…}
…
}
可以将它改写成下面的形式:
class Polygon {
…
public int getPerimeter() { return pPolygon.computePerimeter(this);}
public boolean isConvex() { return pPolygon.isConvex(this);}
public boolean containsPoint() { return pPolygon.containsPoint(this, p);}
…
}
在此处,nPolygon应该是这个样子:
class pPolygon {
static public int computePerimeter(Polygon polygon) {...}
static public boolean isConvex(Polygon polygon) {...}
static public boolean containsPoint(Polygon polygon, Point p) {...}
}
从类的名字pPolygon可以看出,该类所封装的过程主要与Polygon类型的对象有关。名字前面的p表示该类的唯一目的是组织公共静态过程。在Java中,类的名字以小写字母开头不是一种标准的做法,但象pPloygon这样的类事实上并不执行普通类的功能。也就是说,它并不代表着一类对象,它只是语言本身所需要的用于代码组织的实体。
在上面这个例子中,改动代码的总体影响是使得客户代码不必为了重用其功能而从Polygon继承。Polygon类的功能现在已经由pPolygon类以过程为单位提供。客户代码只使用自己需要的代码,无需关心自身并不需要的功能。
这并不意味着在这种新型的过程化编程模式中,类不服务于更有用的目的。恰恰相反,类执行组织和封装对象数据成员的必要工作。而且它们通过多重接口实现多态性的能力也为代码重用提供了显著的支持,这将在下一个步骤中讨论。然而,由于将功能实现包含在实例方法中无法实现理想的代码重用,所以通过类继承实现代码重用和多态性支持也不应成为最佳的技术选择。
在一本被广为阅读的书《Design Patterns》中曾简要地提及一种略有不同的技术。策略模式(Strategy Pattern)提倡将相关算法的每个成员封装在一个通用的接口下,以便于客户端代码可交换地使用其算法。由于一个算法通常被作为一个或几个独立的过程进行编码,这种封装更注重执行单独任务的过程的重用,而不是执行多种任务的、包含代码和数据的对象的重用。这一步骤体现了相同的基本思想。
然而,将一个算法封装在一个接口下意味着将算法作为实现接口的对象进行编码。这意味着我们仍然依赖于一个与所包装的对象的数据和其他方法相耦合的过程,这样便会使其复用变得复杂。此外还存在这样一个问题,每次需要使用这个算法的时候都必须实例化这些对象,这便会降低程序的性能。值得庆幸的是,设计模式提供了针对这两个问题的解决方法。可以在对策略对象进行编码时应用享元模式(Flyweight Pattern,译者注:还存在一种译法为轻量模式),这样每个对象只会存在一个被共知共享的实例(这针对程序性能的问题),而且每个共享对象在访问间隔中并不维持状态(于是对象将没有数据成员,这针对大多数的耦合问题)。由此产生的享元--策略模式非常类似于在这一步骤中所提到的将功能实现封装在全局可见的、无状态的过程中的技术。(译者注:以上这两段文字读起来可能有些晦涩难解,建议有兴趣的读者参阅文中所提到《设计模式》一书,Erich Gamma等著、李英军等译、机械工业出版社出版。)
第二步:将非原始的输入参数类型改为接口类型
在面向对象编程中,代码重用的真正基础在于通过接口参数类型利用多态性,而不是通过类继承,正如Allen Holub在 “Build User Interfaces for Object-Oriented System, Part 2”中所述:
“……你应该通过对接口而不是类编程实现重用。如果一个方法的所有参数都是某个已知接口的引用,这个接口由一些你所不知道的类实现,那么这个方法就能够操作这样一些对象:当编写方法的代码时,这些对象的类甚至还不存在。从技术上讲,可重用的是方法,而不是传递给方法的对象。”
将Holub所讲的方法应用于第一步所得到的结果,只要某块功能代码能够作为一个全局可见的过程而独立存在,你就可以将其每个类类型(class-type)的输入参数改为一个接口类型,这样便能进一步提高其重用的潜力。那么,实现此接口类型的任何类的对象都可以作为参数使用,而不仅仅局限于原始类。由此,这个过程对可能存在的大量的对象类型都成为可用的。
例如,有这样一个全局可见的静态过程
static public boolean contains(Rectangle rect, int x, int y) {…}
这个方法用于检查给定的矩形是否包含某个给定的点。在这个例子中,rect参数的类型可以从Rectangle类改变为接口类型,如下所示:
static public boolean contains(Rectangular rect, int x, int y){…}
Rectangular可以是下面形式的接口:
public interface Rectangular{
Rectangle getBounds();
}
现在,所有可以被描述为矩形的类(也就意味着实现了Rectangular接口)的对象都可以作为传递给pRectangular.contains()的rect参数。通过放宽所传递的参数类型的限制,我们使方法具有更好的可重用性。
不过,在上面这个例子中,Rectangular接口的getBounds方法返回一个Rectangle类型,你可能会怀疑使用这个接口是否具有真正的价值;换句话说,如果我们知道传入过程的对象会在被调用时返回一个Rectangle,为什么不直接传入Rectangle取代接口类型呢?不这样做的最重要原因与集合有关,假设有这样一个方法:
static public boolean areAnyOverlapping(Collection rects) {…}
这个方法的目的在于检查给定集合中的任意矩形对象是否存在重叠。那么,在方法内部遍历集合中的每个对象时,如果无法将对象造型(cast)成如Rectangular这样的接口类型,那么将如何能够访问对象的矩形区域呢?唯一的选择是将对象造型成为其特定的类型(我们直到它有一个能够返回rectangle的方法),这意味着方法必须事先知道其所要操作的是什么类型。这恰恰是这一步骤力图首先要避免的问题!
第三步:选择低耦合的输入参数接口类型
完成第二步之后,应该选择什么样的接口类型来取代给定的类型呢?答案是能够通过参数完全描述过程的需求,同时又具有最少的额外负担的接口类型。参数对象所要实现的接口越简单,其他特定类实现此接口的机会就越大——由此,其对象可以作为参数使用的类也就越多。通过下面的例子可以很容易地看到这点:
static public boolean areOverlapping(Window window1, Window window2) {...}
这个方法用于检查两个窗口(假定是矩形窗口)是否重叠,如果这个方法只要求从参数获得两个窗口的矩形坐标,那么简化参数的类型使其能反映这个事实是一种更好的选择:
static public boolean areOverlapping(Rectangular rect1, Rectangular rect2) {...}
以上的代码假设先前的Window类型的对象同样可以实现Rectangular接口。现在对于所有的矩形对象,都可以重用第一个方法所包含的功能了。
你可能多次体验到当一个接口能够完全确定需要通过参数获哪那些内容时,会存在太多不必要的方法。在这种情况下,应该在全局命名空间中定义一个新的公共接口以供其他可能面临同一困境的方法重用。
你可能还会不止一次地发现,在确定需要通过单一过程的一个参数获取哪些内容时,最好创建一个单独的接口。你应该只为这个参数使用此接口。这通常会在你希望如同C语言中的函数指针一样使用参数的情况下出现。例如下面的过程:
static public void sort(List list, SortComparison comp) {...}
此过程使用参数所提供的比较对象comp,通过比较给定列表中的所有对象而对其进行排序,sort对comp的全部要求是调用一个单独的方法进行比较。因此,SortComparison应该是只带有一个方法的接口:
public interface SortComparison {
boolean comesBefore(Object a, Object b);
}
这个接口的唯一目的是为sort提供一个与其完成任务所需功能相联系的钩子(hook),因此SortComparison无法在其他地方重用。
结束语
以上所述的三个步骤用于现有的、按照相对传统的面向对象方法所编写的代码。这些步骤与面向对象编程技术结合就形成了一种可以运用于今后代码编写中的新方法,它可以提高代码的可重用性和内聚性,同时降低了耦合度及复杂性。
很显然,这些步骤无法运用于那些在本质上就不适合于重用的代码。这类代码通常出现在应用程序的表示层(presentation layer)。例如程序中用于创建用户界面的代码,以及将输入事件与完成实际工作的过程相联系的控制代码,都是属于那种其功能在不同的程序中差别很大的代码,这种代码的重用几乎是不可能的。
摘要:不要放弃编写可重用代码的努力!本文介绍了三种对现有代码进行修改以提高其可重用性的方法。
在程序员中似乎存在着一种日益普遍的观点,认为重用只是一个神话。或许是传统的面向对象编程方法中所存在的不足增加了重用的困难。本文介绍了从另外一种不同的途径使重用成为可能的三个步骤。
第一步:将功能实现从类实例的方法中移出
由于缺乏精确性,类继承不是非常理想的代码重用机制。换句话说,如果不继承一个类的数据成员和其他的方法,那么你就无法重用这个类的某个单独的方法。这些额外的不必要的负担使方法重用的代码变得复杂。派生类对其父类的依赖性也以入了额外的复杂性:对父类的改动会对子类造成影响;当修改任意一个类的时候,我们很难记得清哪个方法被覆盖,哪个没有;而且被覆盖的方法是否会调用父类中相应的方法并不非常清晰地显现。
任何执行单一概念任务的方法应该能够成为代码用的首选而独立存在。为了达到这个目标,我们必须会到过程化的编程模式,将代码从类实例的方法中移出,形成具有全局可见性的过程。为了提高这种过程的可重用性,过程代码应该象静态的通用方法一样编写:每个过程只能使用自己的输入参数,只能调用其他全局性的过程完成其工作,不能使用任何非本地的变量。这种对外部依赖的简化降低了过程使用的复杂性,也增加了在其他地方使用此过程的可能性。当然,由于其结构通常会变得更为清晰,即使抛开重用的目的不谈我们也可以从这种代码的组织方式中受益。
在Java中,方法不能脱离类而单独存在。因此,你可以对相关的过程进行组织并使它们成为一个独立的类中的公共静态方法。例如,对于如下所示的一个类:
class Polygon{
…
public int getPerimeter(){…}
public boolean isConvex(){…}
public Boolean containsPoint(Point p){…}
…
}
可以将它改写成下面的形式:
class Polygon {
…
public int getPerimeter() { return pPolygon.computePerimeter(this);}
public boolean isConvex() { return pPolygon.isConvex(this);}
public boolean containsPoint() { return pPolygon.containsPoint(this, p);}
…
}
在此处,nPolygon应该是这个样子:
class pPolygon {
static public int computePerimeter(Polygon polygon) {...}
static public boolean isConvex(Polygon polygon) {...}
static public boolean containsPoint(Polygon polygon, Point p) {...}
}
从类的名字pPolygon可以看出,该类所封装的过程主要与Polygon类型的对象有关。名字前面的p表示该类的唯一目的是组织公共静态过程。在Java中,类的名字以小写字母开头不是一种标准的做法,但象pPloygon这样的类事实上并不执行普通类的功能。也就是说,它并不代表着一类对象,它只是语言本身所需要的用于代码组织的实体。
在上面这个例子中,改动代码的总体影响是使得客户代码不必为了重用其功能而从Polygon继承。Polygon类的功能现在已经由pPolygon类以过程为单位提供。客户代码只使用自己需要的代码,无需关心自身并不需要的功能。
这并不意味着在这种新型的过程化编程模式中,类不服务于更有用的目的。恰恰相反,类执行组织和封装对象数据成员的必要工作。而且它们通过多重接口实现多态性的能力也为代码重用提供了显著的支持,这将在下一个步骤中讨论。然而,由于将功能实现包含在实例方法中无法实现理想的代码重用,所以通过类继承实现代码重用和多态性支持也不应成为最佳的技术选择。
在一本被广为阅读的书《Design Patterns》中曾简要地提及一种略有不同的技术。策略模式(Strategy Pattern)提倡将相关算法的每个成员封装在一个通用的接口下,以便于客户端代码可交换地使用其算法。由于一个算法通常被作为一个或几个独立的过程进行编码,这种封装更注重执行单独任务的过程的重用,而不是执行多种任务的、包含代码和数据的对象的重用。这一步骤体现了相同的基本思想。
然而,将一个算法封装在一个接口下意味着将算法作为实现接口的对象进行编码。这意味着我们仍然依赖于一个与所包装的对象的数据和其他方法相耦合的过程,这样便会使其复用变得复杂。此外还存在这样一个问题,每次需要使用这个算法的时候都必须实例化这些对象,这便会降低程序的性能。值得庆幸的是,设计模式提供了针对这两个问题的解决方法。可以在对策略对象进行编码时应用享元模式(Flyweight Pattern,译者注:还存在一种译法为轻量模式),这样每个对象只会存在一个被共知共享的实例(这针对程序性能的问题),而且每个共享对象在访问间隔中并不维持状态(于是对象将没有数据成员,这针对大多数的耦合问题)。由此产生的享元--策略模式非常类似于在这一步骤中所提到的将功能实现封装在全局可见的、无状态的过程中的技术。(译者注:以上这两段文字读起来可能有些晦涩难解,建议有兴趣的读者参阅文中所提到《设计模式》一书,Erich Gamma等著、李英军等译、机械工业出版社出版。)
第二步:将非原始的输入参数类型改为接口类型
在面向对象编程中,代码重用的真正基础在于通过接口参数类型利用多态性,而不是通过类继承,正如Allen Holub在 “Build User Interfaces for Object-Oriented System, Part 2”中所述:
“……你应该通过对接口而不是类编程实现重用。如果一个方法的所有参数都是某个已知接口的引用,这个接口由一些你所不知道的类实现,那么这个方法就能够操作这样一些对象:当编写方法的代码时,这些对象的类甚至还不存在。从技术上讲,可重用的是方法,而不是传递给方法的对象。”
将Holub所讲的方法应用于第一步所得到的结果,只要某块功能代码能够作为一个全局可见的过程而独立存在,你就可以将其每个类类型(class-type)的输入参数改为一个接口类型,这样便能进一步提高其重用的潜力。那么,实现此接口类型的任何类的对象都可以作为参数使用,而不仅仅局限于原始类。由此,这个过程对可能存在的大量的对象类型都成为可用的。
例如,有这样一个全局可见的静态过程
static public boolean contains(Rectangle rect, int x, int y) {…}
这个方法用于检查给定的矩形是否包含某个给定的点。在这个例子中,rect参数的类型可以从Rectangle类改变为接口类型,如下所示:
static public boolean contains(Rectangular rect, int x, int y){…}
Rectangular可以是下面形式的接口:
public interface Rectangular{
Rectangle getBounds();
}
现在,所有可以被描述为矩形的类(也就意味着实现了Rectangular接口)的对象都可以作为传递给pRectangular.contains()的rect参数。通过放宽所传递的参数类型的限制,我们使方法具有更好的可重用性。
不过,在上面这个例子中,Rectangular接口的getBounds方法返回一个Rectangle类型,你可能会怀疑使用这个接口是否具有真正的价值;换句话说,如果我们知道传入过程的对象会在被调用时返回一个Rectangle,为什么不直接传入Rectangle取代接口类型呢?不这样做的最重要原因与集合有关,假设有这样一个方法:
static public boolean areAnyOverlapping(Collection rects) {…}
这个方法的目的在于检查给定集合中的任意矩形对象是否存在重叠。那么,在方法内部遍历集合中的每个对象时,如果无法将对象造型(cast)成如Rectangular这样的接口类型,那么将如何能够访问对象的矩形区域呢?唯一的选择是将对象造型成为其特定的类型(我们直到它有一个能够返回rectangle的方法),这意味着方法必须事先知道其所要操作的是什么类型。这恰恰是这一步骤力图首先要避免的问题!
第三步:选择低耦合的输入参数接口类型
完成第二步之后,应该选择什么样的接口类型来取代给定的类型呢?答案是能够通过参数完全描述过程的需求,同时又具有最少的额外负担的接口类型。参数对象所要实现的接口越简单,其他特定类实现此接口的机会就越大——由此,其对象可以作为参数使用的类也就越多。通过下面的例子可以很容易地看到这点:
static public boolean areOverlapping(Window window1, Window window2) {...}
这个方法用于检查两个窗口(假定是矩形窗口)是否重叠,如果这个方法只要求从参数获得两个窗口的矩形坐标,那么简化参数的类型使其能反映这个事实是一种更好的选择:
static public boolean areOverlapping(Rectangular rect1, Rectangular rect2) {...}
以上的代码假设先前的Window类型的对象同样可以实现Rectangular接口。现在对于所有的矩形对象,都可以重用第一个方法所包含的功能了。
你可能多次体验到当一个接口能够完全确定需要通过参数获哪那些内容时,会存在太多不必要的方法。在这种情况下,应该在全局命名空间中定义一个新的公共接口以供其他可能面临同一困境的方法重用。
你可能还会不止一次地发现,在确定需要通过单一过程的一个参数获取哪些内容时,最好创建一个单独的接口。你应该只为这个参数使用此接口。这通常会在你希望如同C语言中的函数指针一样使用参数的情况下出现。例如下面的过程:
static public void sort(List list, SortComparison comp) {...}
此过程使用参数所提供的比较对象comp,通过比较给定列表中的所有对象而对其进行排序,sort对comp的全部要求是调用一个单独的方法进行比较。因此,SortComparison应该是只带有一个方法的接口:
public interface SortComparison {
boolean comesBefore(Object a, Object b);
}
这个接口的唯一目的是为sort提供一个与其完成任务所需功能相联系的钩子(hook),因此SortComparison无法在其他地方重用。
结束语
以上所述的三个步骤用于现有的、按照相对传统的面向对象方法所编写的代码。这些步骤与面向对象编程技术结合就形成了一种可以运用于今后代码编写中的新方法,它可以提高代码的可重用性和内聚性,同时降低了耦合度及复杂性。
很显然,这些步骤无法运用于那些在本质上就不适合于重用的代码。这类代码通常出现在应用程序的表示层(presentation layer)。例如程序中用于创建用户界面的代码,以及将输入事件与完成实际工作的过程相联系的控制代码,都是属于那种其功能在不同的程序中差别很大的代码,这种代码的重用几乎是不可能的。
本栏文章均来自于互联网,版权归原作者和各发布网站所有,本站收集这些文章仅供学习参考之用。任何人都不能将这些文章用于商业或者其他目的。( Pfan.cn )
【编程爱好者论坛】