Java枚举类-行为模式最佳实践

阅读更多

下面的主要内容是读了《Effective Java》第二版第30条之后的一些看法和总结。

 

     在面对一大篇的叙述性的知识点介绍时,往往觉得太过乏味,抓不住重点甚至有些力不从心。而采用对比的学习方式,可以明了孰优孰劣,关键特性是什么。第30条关于枚举的介绍,带给我一些感触。

曾和同事讨论如何使用常量,说就用过public static final,不知道枚举,唉╮(╯▽╰)╭是多不关注Java的特性更新啊!

     言归正传,首先就是介绍int枚举模式:

//The int enum pattern - severely deficient
public static final int APPLE_FUJI = 1;
public static final int APPLE_PIPPIN = 2;
public static final int APPLE_GRANNY_SMITH = 3;

public static final int ORANGE_NAVEL = 1;
public static final int ORANGE_TEMPLE = 2;
public static final int ORANGE_BLOOD = 3;

 

     这种方式存在诸多不便,你可以在需要APPLE_FUJI的地方使用ORANGE_NAVEL而不引起任何的编译和运行时异常,一旦常量值发生变化客户端必须重新编译,如果是混用的情况,运行时的行为即使重新编译也是无法确定的,而且往往在数据传递和使用时看到的只是个magic number,遍历int枚举常量也没有可靠的方法。

 

     还有String枚举模式,主要这种方式存在性能问题,因为依赖于字符串的比较操作。

     

     但有一种方式,就比如Integer.MAX_VALUE,Integer.MIN_VALUE,MATH.PI等,分别表示整型数的最大最小值和圆周率,是和具体的类相关联一个属性或者是一个有具体意义的常量值,这样表示是合适的。

   

    Java枚举类型背后的基本想法非常简单:它们就是通过共有的静态final域为每个枚举常量导出实例的类。它们是单例的泛型化,本质上时单元素的枚举。所以使用枚举实现单例是一种有效方式。相对于int枚举模式,Java枚举类型有以下优势:1、枚举类型拥有自己的命名空间,可以允许同名常量(不同命名空间);2、可以增加和重排枚举类型中常量,而无需重新编译客户端;(新增常量自然无法使用)3、toString可以获取常量的字面值;4、常量可任意添加方法和字段。

下面主要围绕文中提到的枚举常量的方法运用,进行下说明,并且发现文中提到的这些使用方式和设计模式的行为模式有些许相似,我称之为枚举的行为模式。

 

一、枚举常量的共同行为

以太阳系的8大行星为例,每颗行星都有质量和半径,通过这两个属性可以计算出表面重力,从而通过物体质量可以获取在某行星上重量,例子中枚举常量两个参数分别表示行星的质量和半径:

public enum Planet {
    MERCURY(3.302e+23, 2.439e6),
    VENUS  (4.869e+24, 6.052e6),
    EARTH  (5.975e+24, 6.378e6),
    MARS   (6.419e+23, 3.393e6),
    JUPITER(1.899e+27, 7.149e7),
    SATURN (5.685e+26, 6.027e7),
    URANUS (8.683e+25, 2.556e7),
    NEPTUNE(1.024e+26, 2.477e7);
    private final double mass;           // In kilograms
    private final double radius;         // In meters
    private final double surfaceGravity; // In m / s^2
 
    // Universal gravitational constant in m^3 / kg s^2
    private static final double G = 6.67300E-11;
 
    // Constructor
    Planet(double mass, double radius) {
        this.mass = mass;
        this.radius = radius;
        surfaceGravity = G * mass / (radius * radius);
    }
 
    public double mass()           { return mass; }
    public double radius()         { return radius; }
    public double surfaceGravity() { return surfaceGravity; }
 
    public double surfaceWeight(double mass) {
        return mass * surfaceGravity;  // F = ma
    }
}

 

下面是计算表面重量的主方法:

public class WeightTable {
	public static void main(String[] args) {
		double earthWeight = Double.parseDouble(args[0]);
		double mass = earthWeight / Planet.EARTH.surfaceGravity();
		for (Planet p : Planet.values())
		System.out.printf("Weight on %s is %f%n", p, p.surfaceWeight(mass));
	}
}

 

结果如下:

Weight on MERCURY is 66.133672

Weight on VENUS is 158.383926

Weight on EARTH is 175.000000

Weight on MARS is 66.430699

Weight on JUPITER is 442.693902

Weight on SATURN is 186.464970

Weight on URANUS is 158.349709

Weight on NEPTUNE is 198.846116

 

行为分析:

每个行星都要计算其表面重量,而行星计算表面重力的公式是不变的,所以每个常量的这个行为是统一的,抽象为一个方法即可,而巧妙之处在于在初始化常量时把表面重力值一同计算出来,计算时直接取值即可。

 

二、枚举常量的不同行为

 

以操作码为例,加减乘除的实现是不同的。首先是第一种实现方法(一种不好的方法):

// Enum type that switches on its own value - questionable
public enum Operation {
	PLUS, MINUS, TIMES, DIVIDE;
	// Do the arithmetic op represented by this constant
	double apply(double x, double y) {
		switch(this) {
		case PLUS: return x + y;
		case MINUS: return x - y;
		case TIMES: return x * y;
		case DIVIDE: return x / y;
		}
		throw new AssertionError("Unknown op: " + this);
	}
}

 

这段代码看似可行,但是却很脆弱,如果添加了新的常量,忘了给switch添加判断条件,编译没有问题,运行时却会报异常。而且以面向对象编程的角度来看,面对大量的switch case语句或者if else语句,那一般就是有改进的余地的。

文中提出了一种叫做“(特定常量方法实现)constant-specific method implementations”的一种方法,就是在枚举类型中声明一个抽象方法,在常量中进行实现,代码如下:

// Enum type with constant-specific method implementations
public enum Operation {
	PLUS { double apply(double x, double y){return x + y;} },
	MINUS { double apply(double x, double y){return x - y;} },
	TIMES { double apply(double x, double y){return x * y;} },
	DIVIDE { double apply(double x, double y){return x / y;} };
	abstract double apply(double x, double y);
}

 

这样添加新常量也不会忘记方法实现了,因为编译器会提醒你的。进一步改进就是与具体的常量数值结合起来,利用toString方便的打印算术表达式:

// Enum type with constant-specific class bodies and data
public enum Operation {
	PLUS("+") {
	double apply(double x, double y) { return x + y; }
	},
	MINUS("-") {
	double apply(double x, double y) { return x - y; }
	},
	TIMES("*") {
	double apply(double x, double y) { return x * y; }
	},
	DIVIDE("/") {
	double apply(double x, double y) { return x / y; }
	};
	private final String symbol;
	Operation(String symbol) { this.symbol = symbol; }
	@Override public String toString() { return symbol; }
	abstract double apply(double x, double y);
}
public static void main(String[] args) {
	double x = 2;
	double y = 4;
	for (Operation op : Operation.values())
	System.out.printf("%f %s %f = %f%n",x, op, y, op.apply(x, y));
}

 

打印结果:

2.000000 + 4.000000 = 6.000000

2.000000 - 4.000000 = -2.000000

2.000000 * 4.000000 = 8.000000

2.000000 / 4.000000 = 0.500000

还建议写一个fromString()方法将字符串常量转为枚举常量。下面是可以参考的方式,利用Map方便实现字符串向枚举常量转换:

// Implementing a fromString method on an enum type
private static final Map stringToEnum = new HashMap();
static { // Initialize map from constant name to enum constant
	for (Operation op : values())
	stringToEnum.put(op.toString(), op);
}
// Returns Operation for string, or null if string is invalid
public static Operation fromString(String symbol) {
	return stringToEnum.get(symbol);
}

 

三、基于策略的行为实现

3.1

上面的方式“constant-specific method implementations”有个不足之处,就是无法共享代码(似乎也很难共享代码),下面是一个可以共享代码的枚举例子:

以工资计算为例,五个工作日,八小时之外都算加班(T_T),算加班工资,下面是通过switch实现的一个方法:

// Enum that switches on its value to share code - questionable
enum PayrollDay {
	MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY;
	private static final int HOURS_PER_SHIFT = 8;
	double pay(double hoursWorked, double payRate) {
		double basePay = hoursWorked * payRate;
		double overtimePay; // Calculate overtime pay
		switch(this) {
			case SATURDAY: case SUNDAY:
				overtimePay = hoursWorked * payRate / 2;
			default: // Weekdays
				overtimePay = hoursWorked <= HOURS_PER_SHIFT ? 0 : (hoursWorked - HOURS_PER_SHIFT) * payRate / 2;
				break;
		}
		return basePay + overtimePay;
	}
}

 

不知道看到上面代码,有没有发现问题啊,就是周末加班的代码没有加break啊(坑)。从代码维护角度看,该代码很危险,如果添加一个新的枚举值(比如病假),忘了修改switch语句,计算工资肯定出错。

从面向对象的角度看,上面代码当然可以改进,文中就提出了一种“策略枚举”的方式实现工资计算,看着还真有点像策略模式呢,代码如下:

// The strategy enum pattern
enum PayrollDay {
	MONDAY(PayType.WEEKDAY), TUESDAY(PayType.WEEKDAY),
	WEDNESDAY(PayType.WEEKDAY), THURSDAY(PayType.WEEKDAY),
	FRIDAY(PayType.WEEKDAY),
	SATURDAY(PayType.WEEKEND), SUNDAY(PayType.WEEKEND);
	private final PayType payType;
	PayrollDay(PayType payType) { this.payType = payType; }
	double pay(double hoursWorked, double payRate) {
		return payType.pay(hoursWorked, payRate);
	}
	// The strategy enum type
	private enum PayType {
		WEEKDAY {
			double overtimePay(double hours, double payRate) {
			return hours <= HOURS_PER_SHIFT ? 0 : (hours - HOURS_PER_SHIFT) * payRate / 2;
			}
		},
		WEEKEND {
			double overtimePay(double hours, double payRate) {
			return hours * payRate / 2;
			}
		};
		private static final int HOURS_PER_SHIFT = 8;
		abstract double overtimePay(double hrs, double payRate);
		double pay(double hoursWorked, double payRate) {
			double basePay = hoursWorked * payRate;
			return basePay + overtimePay(hoursWorked, payRate);
		}
	}
}

 

 

     PayType就是策略枚举,负责工资的计算,没有了Switch,增加工作日类型,选择计算策略很容易实现代码修改。

3.2

     还有一种方式实现,就和策略模式一模一样了,那就是基于对枚举类的扩展。枚举的扩展是通过接口实现的。比如以工资支付为例,已经有了工作日和周末的支付方式,我们想添加国庆加班工资支付方式,接口及实现如下:

//定义支付接口
public interface ShallPay {
	public double pay(double hours, double payRate);
}

 

//工作日周末支付实现
public enum PayType implements ShallPay {
	...
}

 

//国庆支付实现
public enum PayTypeHoliday  implements ShallPay {
	TRIPLE {
		public double pay(double hour, double payRate) {
			return 3*hour*payRate;
		}
	};
}

 

//具体的工作时间
public enum PayrollDay {
	public enum PayrollDay {
	MONDAY(PayType.WEEKDAY), TUESDAY(PayType.WEEKDAY),
	WEDNESDAY(PayType.WEEKDAY), THURSDAY(PayType.WEEKDAY),
	FRIDAY(PayType.WEEKDAY),
	SATURDAY(PayType.WEEKEND), SUNDAY(PayType.WEEKEND),
	HOLIDAY(PayTypeHoliday.TRIPLE);
	
	private final ShallPay payType;
	PayrollDay(ShallPay payType) { this.payType = payType; }
	double pay(double hoursWorked, double payRate) {
		return payType.pay(hoursWorked, payRate);
	}	
}

 

     PayrollDay就是需要支付工资时的上下文环境,ShallPay就是策略接口,PayType,PayTypeHoliday的常量可看做具体策略的实现类。

这种方式,在原用枚举类的基础上,扩展自己的枚举类,就可以直接添加新的枚举类型实现相同接口,使用很方便。

 

注:

     用了上述方法后switch语句是否对枚举来说没有用了呢?文中提到对外部不受控的枚举使用switch是合适的,比如Operation枚举,不受你控制,希望有个方法返回运算符的反运算,可以用下列方式:

// Switch on an enum to simulate a missing method
public static Operation inverse(Operation op) {
	switch(op) {
		case PLUS: return Operation.MINUS;
		case MINUS: return Operation.PLUS;
		case TIMES: return Operation.DIVIDE;
		case DIVIDE: return Operation.TIMES;
		default: throw new AssertionError("Unknown op: " + op);
	}
}

 

你可能感兴趣的:(枚举,模式,enum)