×

消息

EU e-Privacy Directive

This website uses cookies to manage authentication, navigation, and other functions. By using our website, you agree that we can place these types of cookies on your device.

View e-Privacy Directive Documents

You have declined cookies. This decision can be reversed.

使用函数对象来定义运行策略

有些语言支持函数指针,代理(delegate),lambda表达式,或者其它类似的机制来存储和传递可执行的"函数"。这些特性允许程序在运行时动态改变行为,而这样的设计被称为"策略"模式(strategy pattern)。比如在c语言标准库中,qsort函数有一个参数是指向comparator函数的函数指针,这个comparator在排序过程中被用来对序列中的两个元素进行比较,通过传递不同的comparator函数,qsort可以动态采用不同的排序(比较)策略,而这个被传入的comparator就可以称为一个排序策略。

Java不支持函数指针,但通过对象引用可以实现类似的功能。通常说来,调用一个对象的方法,是要对这个对象本身进行操作。但如果一个方法接受的参数是指向其它对象的引用,那么这个方法也可以对这些传入的对象进行操作。如果一个类仅开放一个这样(对其他对象进行操作)的方法,那么它的对象就等同于一个指向该方法的函数指针,而这样的对象被称为函数对象。比如下面的类:

class StringLengthComparator {

	public int compare(String s1, String s2) {
		return s1.length() - s2.length();
	}
}

继承优于标签类型

开发者有时会遇到这样的类:其对象有多种不同"类别",每一"类别"的对象都用一个特定的标签来识别。比如下面的类,可以代表"圆圈"或"矩形":

// Tagged class - vastly inferior to a class hierarchy!
class Figure {
	enum Shape { RECTANGLE, CIRCLE };
	
	// Tag field - the shape of this figure
	final Shape shape;
	
	// These fields are used only if shape is RECTANGLE
	double length;
	double width;
	
	// This field is used only if shape is CIRCLE
	double radius;
	
	// Constructor for circle
	Figure(double radius) {
		shape = Shape.CIRCLE;
		this.radius = radius;
	}
	
	// Constructor for rectangle
	Figure(double length, double width) {
		shape = Shape.RECTANGLE;
		this.length = length;
		this.width = width;
	}
	
	double area() {
		switch(shape) {
			case RECTANGLE:
				return length * width;
			case CIRCLE:
				return Math.PI * (radius * radius);
			default:
				throw new AssertionError();
		}
	}
}

接口只用来定义类型

如果一个类实现了某个接口,那么这个接口类型可以成为指向该类对象的引用。因此类与接口之间的"实现"关系,应该能使用户通过接口了解对象的行为。除了这个场景以外,定义接口都是不合适的。违背上述原则的一个反例是常量接口,这样的接口不定义任何方法,仅含有静态final属性,每一个属性都被开放为常量。用户类通过实现这些接口来避免使用常量全名。比如下面的例子:

// Constant interface antipattern - do not use!
public interface PhysicalConstants {
	// Avogadro's number (1/mol)
	static final double AVOGADROS_NUMBER = 6.02214199e23;
	// Boltzmann constant (J/K)
	static final double BOLTZMANN_CONSTANT = 1.3806503e-23;
	// Mass of the electron (kg)
	static final double ELECTRON_MASS = 9.10938188e-31;
}

接口优于抽象类

接口和抽象类是Java语言中的两种多态实现机制。两者最明显的区别在于:抽象类允许其本身直接实现某些方法,而接口则必须保证所有方法都是抽象的。一个更重要的区别是抽象类的使用依赖于特定的继承链结构,而接口则没有这种限制,由于Java不允许多继承,抽象类的这一特点严重限制了它们的使用。

已有类可以很容易被改造为新接口的实现,只需声明实现某接口,并添加所需方法即可。比如当Comparable接口被引入时,大量的已有Java标准类被改造为它的实现。对于抽象类,则没有这样的便利。这是因为,想把两个已有类定义为同一个新抽象类的子类,必须把这个抽象类放在两条继承链的交点位置以上,这需要对两条继承链中间部分的所有类做出同样的改造:继承新抽象类,提供抽象方法的实现。很明显,这样做开销巨大,而且很可能破坏继承逻辑。

类要么被设计成专用于继承(并辅以清楚的文档),要么就完全禁止继承

item 16提到过,继承一个本软件包以外的类是危险的,除非它被设计成专用于继承。这节就是讲一个"专用于继承的类"该是什么样子。

首先,这个类必须用文档注明它自己对"可覆盖方法"的使用情况(所谓"可覆盖方法",就是非final的、并且是public或protected的方法)。比如:某个方法调用了哪些可覆盖方法,调用顺序怎样,它们的返回值对后续处理将产生怎样的影响。还应注明在哪些情况下,可覆盖方法会被调用。比如调用者可能是后台运行的其他线程或静态初始化代码。

相对于继承(inheritance),应更优先使用组合(composition)

继承是一种非常强大的代码重用方式,但它并不总是最好的方式。

在同一个软件包内可以安全地使用继承,这是因为父类和子类都在同一群开发者的维护之下。或者父类是被设计为专用于继承,那么同样可以安全地使用。相反,跨软件包去继承普通的类,则可能是危险的。

不同于方法调用,继承会破坏类的封装。换言之,子类必须依赖于父类的实现细节才能正常运行,而父类的实现完全可能在不同版本之间发生变化,这会破坏子类代码。

比如下面的代码。继承了HashSet类,增加一个计数器,以统计Set中曾加入过的元素总数:

第1页 共4页