TIP34 用接口模拟可伸缩的枚举
原书表达的太绕口了,还是直接上代码吧,这样直观:
public interface Operation {
double apply(double x, double y);
}
public enum BasicOperation implements Operation{
PLUS("+"){
@Override
public double apply(double x, double y) {
return x + y;
}
},
MINUS("-"){
@Override
public double apply(double x, double y) {
return x - y;
}
},
TIMES("*"){
@Override
public double apply(double x, double y) {
return x * y;
}
},
DEVIDE("/"){
@Override
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;
}
}
在这里,我们重新写了Tip 30 中的四则运算的实现。
他们的区别在于, TIP 30 中枚举类将apply方法声明为abstract, 而此处直接抽象为Operation接口,并让枚举类实现这个接口。
似乎没什么区别啊?
如果我们再增加一个扩展运算类型:
public enum ExtendedOperation implements Operation {
//求x的y次幂
EXP("^"){
@Override
public double apply(double x, double y) {
return Math.pow(x, y);
}
},
//求x模y的值
REMAINDER("%"){
@Override
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;
}
}
想想看, 在任何使用基础运算的地方,都可以使用新的运算,只要将对象类型写成接口类型(Operation)
。
这就是面向接口编程的特点:
public class OperationTest {
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> opSet,double x, double y){
for (Operation op :opSet.getEnumConstants()) {
System.out.println(x+" "+op+" "+y+" = "+op.apply(x,y));
}
}
}
运行一下,看看结果:
cmd->java douvril.effect.OperationTest 2 3
2.0 ^ 3.0 = 8.0
2.0 % 3.0 = 2.0
原书上就是这个例子。豆爷用的是idea,建议直接在终端进入/out目录,使用java命令运行程序。
另外,请仔细品味test
方法的泛型使用。ExtendedOperation的字面文字(ExtendedOperation.class)从main传递给了test方法,来描述被扩展操作的集合。这个类的字面文字充当了有限制的类型令牌(TIP 29)。
至于参数 <T extends Enum<T> & Operation>
,则确保了Class对象既表示枚举,又表示Operation的子类型(然而这还是太复杂了)。
下面有另外一个版本:
public class OperationTest2 {
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.println(x+" "+op+" "+y+" = "+op.apply(x,y));
}
}
}
没有了结构复杂的泛型参数,而且test方法也更加灵活一些:它允许调用者将多个实现类型的操作合并在一起。比如,在调用test时,可以这样:
public class OperationTest2 {
public static void main(String args[]){
double x = 3;
double y = 4;
List<Operation> operations = new LinkedList<>();
List<Operation> basicOperations = Arrays.asList(BasicOperation.values());
List<Operation> extendedOperations = Arrays.asList(ExtendedOperation.values());
operations.addAll(basicOperations);
operations.addAll(extendedOperations);
test(operations,x ,y);
}
private static void test(Collection<? extends Operation> opSet, double x, double y){
for (Operation op :opSet) {
System.out.println(x+" "+op+" "+y+" = "+op.apply(x,y));
}
}
/**
* 错误的示范,这段代码将会抛出一个运行时错误...
*/
public static void testOp(){
double x = 3;
double y = 4;
List<Operation> operations = Arrays.asList(BasicOperation.values());
//java.lang.UnsupportedOperationException, 请注意values()方法返回的类型
operations.addAll(Arrays.asList(ExtendedOperation.values()));
test(operations,x ,y);
}
}
运行结果一切正常:
3.0 + 4.0 = 7.0
3.0 - 4.0 = -1.0
3.0 * 4.0 = 12.0
3.0 / 4.0 = 0.75
3.0 ^ 4.0 = 81.0
3.0 % 4.0 = 3.0
这套机制的不足之处,在于toString() 和symbol相关的逻辑代码没法复用,只能一个个编写或拷贝。在这个例子中,此类代码较少而已;如果共同的代码太多,则可以封装在一个静态类或静态辅助方法中,避免代码的复制工作。(参考<<重构-改善既有代码的设计>>, page 76, Duplicated Code相关章节).
总之,虽然无法编写可扩展的枚举类型,却可以通过编写接口以及实现该接口的基础枚举类型,对它进行模拟。这样允许客户端编写自己的枚举来实现接口。如果API是根据接口编写的,那么在任何可以使用基础枚举类型的地方,也都可以使用这些(客户端扩展的)枚举。