ITEM 38: 使用接口模拟扩展枚举
ITEM 38: EMULATE EXTENSIBLE ENUMS WITH INTERFACES
enum类型在几乎所有方面,都优于本书第一版[Bloch01]中描述的 typesafe enum 模式。从表面上看,一个例外是涉及可扩展性,扩展在原始模式下是可能的,但不受语言构造的支持。换句话说,使用该模式,可以让一个枚举类型扩展另一个枚举类型;使用语言特性就无法做到这一点。这并非偶然。在大多数情况下,枚举的可扩展性是一个坏主意。令人困惑的是,扩展类型的元素是基类型的实例,反之亦然。枚举基类型及其扩展的所有元素没有好的实现方法。最后,可扩展性将使设计和实现的许多方面变得复杂。
也就是说,对于可扩展枚举类型至少有一个引人注目的用例,即操作代码,也称为操作码。操作码是一种枚举类型,其元素表示某些机器上的操作,例如 item 34 中的操作类型,它表示简单计算器上的函数。有时候,让API的用户提供他们自己的操作是可取的,这样可以有效地扩展API提供的操作集。
幸运的是,有一种很好的方法可以使用枚举类型来实现这种效果。基本思想是利用 enum 类型可以实现任意接口的事实,方法是为操作码类型定义一个接口,并定义一个 enum (接口的标准实现)。例如,这里有一个可扩展版本的item 34 的操作类型:
// Emulated extensible enum using an interface
public interface Operation {
double apply(double x, double y);
}
public enum BasicOperation implements Operation {
PLUS("+") {public double apply(double x, double y) { return x + y; } },
MINUS("-") {public double apply(double x, double y) { return x - y; }},
TIMES("*") {public double apply(double x, double y) { return x * y; } },
DIVIDE("/") {public double apply(double x, double y) { return x / y; }};
private final String symbol;
BasicOperation(String symbol) { this.symbol = symbol;}
@Override public String toString() { return symbol;}
}
虽然enum类型(BasicOperation)是不可扩展的,但是接口类型(Operation)是可扩展的,而且它是用于表示 api 中的操作的接口类型。您可以定义另一个实现此接口的 enum 类型,并使用该新类型的实例来替代基本类型。例如,假设您想定义前面显示的操作类型的扩展,包括求幂和余数操作。你所要做的就是编写一个 enum 类型来实现操作接口:
// Emulated extension enum
public enum ExtendedOperation implements Operation {
EXP("^") {public double apply(double x, double y) { return Math.pow(x, y);} },
REMAINDER("%") {public double apply(double x, double y) {return x % y; }};
private final String symbol;
ExtendedOperation(String symbol) { this.symbol = symbol;}
@Override public String toString() { return symbol;}
}
现在,您可以在任何可以使用基本操作的地方使用新操作,前提是编写 api 是为了获取接口类型(操作),而不是实现(BasicOperation)。注意,您不必像在具有特定于实例的方法实现的非扩展枚举中那样在枚举中声明抽象 apply 方法(P 162)。这是因为抽象方法(apply)是接口(Operation)的成员。
不仅可以在任何需要“基本枚举”的地方传递“扩展枚举”的单个实例,而且还可以传递整个扩展枚举类型,并在基类型的元素之外或之外使用它的元素。例如,在 P163 有一个测试程序的版本,它执行前面定义的所有扩展操作:
public static void main(String[] args) {
double x = Double.parseDouble(args[0]);
double y = Double.parseDouble(args[1]);
test(ExtendedOperation.class, x, y);
}
private static <T extends Enum<T> & Operation> void test( Class<T> opEnumType, double x, double y) {
for (Operation op : opEnumType.getEnumConstants())
System.out.printf("%f %s %f = %f%n",x, op, y, op.apply(x, y));
}
注意,扩展操作类型(ExtendedOperation.class)的类文本从 main 传递到 test,以描述扩展操作集。类文字用作有界类型令牌(item 33)。opEnumType 参数的复杂声明( <T extends Enum<T> & Operation> Class<T>)确保类对象同时表示 Enum 和操作的子类型,这正是遍历元素并执行与每个元素关联的操作所需要的。
第二个替代方法是传递一个 Collection<? extends Operation> ,这是一个有界通配符类型(Item 31),而不是传递一个类对象:
public static void main(String[] args) {
double x = Double.parseDouble(args[0]);
double y = Double.parseDouble(args[1]);
test(Arrays.asList(ExtendedOperation.values()), x, y);
}
private static void test(Collection<? extends Operation> opSet, double x, double y) {
for (Operation op : opSet)
System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x, y));
}
生成的代码稍微简单些,测试方法也更加灵活:它允许调用者组合来自多个实现类型的操作。另一方面,您放弃了在指定操作上使用 EnumSet (item 36项)和 EnumMap(item 37)的功能。
当运行命令行参数4和2时,前面显示的两个程序都将产生这个输出:
4.000000 ^ 2.000000 = 16.000000
4.000000 % 2.000000 = 0.000000
使用接口来模拟可扩展枚举的一个小缺点是实现不能从一种枚举类型继承到另一种枚举类型。如果实现代码不依赖于任何状态,则可以使用缺省实现(item 20)将其放置在接口中。在我们的操作示例中,存储和检索与操作关联的符号的逻辑必须在 BasicOperation 和 ExtendedOperation 中复制。在这种情况下,这并不重要,因为只有很少的代码是重复的。如果有大量的共享功能,可以将其封装在 helper 类或静态 helper 方法中,以消除代码重复。
此项中描述的模式用于 Java库。例如 java.nio.file.LinkOption 实现了 CopyOption and OpenOption 接口。
总之,虽然不能编写可扩展的枚举类型,但是可以通过编写接口来模拟它,以便与实现该接口的基本枚举类型一起使用。这允许客户端编写自己的枚举(或其他类型)来实现接口。然后,只要可以使用基本 enum 类型的实例,就可以使用这些类型的实例,如果api是根据接口编写的。