应用泛型的策略模式
作者:Dr. Heinz M. Kabutz
摘要
策略模式相当的简单。通过使用这个模式,我们可以把一些内聚的状态转化为一些外在的状态,这样可以实现共享策略对象。当每一个策略对象为了实现它的功能而需要不同批次的信息的时候,策略模式将变得相当的灵活。在这篇文章里,我们将看到我们如何使用Java5的泛型把正确的上下文子类型传递到每一个策略对象中。
欢迎来到1、2、3版的《Java(TM)专家实事通讯》,在这里你将看到,当我们把策略模式和泛型结合到一起的时候,会发生一些什么事情。这篇文章写起来相当的困难,并且时间也很紧。我非常感谢下面的这些人,他们提供了有价值的参考:Angelika Langer, Philip Wadler, Maurice Naftalin 和 Kirk Pepperdine.
在我们开始详细分析这个模式之前,我需要增加一个提示:你可以使用一些比我在这里提供的例子更加复杂的示例作为策略模式的例子。这是解决这个特殊问题的另外一种方法。你可以按照我在这篇文章里所展示的步骤把策略模式介绍到你的现有代码中去。
我的目标如下:
。把行为移出一个大类
。设计算法来计算税收
。使得外在的状态对象最少
这能够帮助我,使得我可以通过继承而不必修改现有代码来获得代码。
使用泛型的策略模式
我们以一个简单的例子开始,这个例子是通过不同的纳税人来计算税费。纳税人的类型由类内部的一个int型的变量来定义,我们正是使用它来计算纳税人该缴多少税的。
public class TaxPayer {
public static final int COMPANY = 0;
public static final int EMPLOYEE = 1;
public static final int TRUST = 2;
private static final double COMPANY_RATE = 0.30;
private static final double EMPLOYEE_RATE = 0.45;
private static final double TRUST_RATE = 0.35;
private double income;
private final int type;
public TaxPayer(int type, double income) {
this.type = type;
this.income = income;
}
public double getIncome() {
return income;
}
public double extortCash() {
switch (type) {
case COMPANY: return income * COMPANY_RATE;
case EMPLOYEE: return income * EMPLOYEE_RATE;
case TRUST: return income * TRUST_RATE;
default: throw new IllegalArgumentException();
}
}
}
下面是这个大类的类图:

上面的类被ReceiverOfRevenue类所使用,如下:
public class ReceiverOfRevenue {
public static void main(String[] args) {
TaxPayer heinz = new TaxPayer(TaxPayer.EMPLOYEE, 50000);
TaxPayer maxsol = new TaxPayer(TaxPayer.COMPANY, 100000);
TaxPayer family = new TaxPayer(TaxPayer.TRUST, 30000);
System.out.println(heinz.extortCash());
System.out.println(maxsol.extortCash());
System.out.println(family.extortCash());
}
}
最初的TaxPlayer类的计算相当的简单。然而,无论什么时候计算发生变化的话,你将不得不修改最初的那个类。还有,如果你有好几个
switch
语句的话,当你引入一种新的纳税类型的话,你将不得不更新每一个switch语句。一旦我使用这种方法写了一个case公举的话,当我增加一个新的结构,我将不得不更改五种不同的switch语句来适应这种改变。如果我忘记输入switch语句的话,那么新的结构也将没有得到输入。
一种更好的方法用来避免使用switch或者多条件的if…else …if…是使用策略模式。它允许我们为每一个纳税人类型的税费计算创建一个独立的类。我们可以很容易的新增不同的税费类型。另外,一个更小的类意味着你的单元测试可以更加专注某一个特别的方面。
为了将原始的switch语句转化为策略对象,我们首先定义一个TaxStrategy接口:
public interface TaxStrategy {
public double extortCash(double income);
}
接着我们将为每一个计算提供一个独立的策略实现(像前面所提到的,现在这些实现看起来相当的简单,后面它们将变得比较复杂):
public class CompanyTaxStrategy implements TaxStrategy {
private static final double RATE = 0.30;
public double extortCash(double income) {
return income * RATE ;
}
}
public class EmployeeTaxStrategy implements TaxStrategy {
private static final double RATE = 0.45;
public double extortCash(double income) {
return income * RATE;
}
}
public class TrustTaxStrategy implements TaxStrategy {
private static final double RATE = 0.40;
public double extortCash(double income) {
return income * RATE;
}
}
现在TaxPayer类实用TaxStrategy对象来代替int类型的纳税人类型,我们使用多态代替多条件语句,如下:
public class TaxPayer {
public static final TaxStrategy EMPLOYEE =
new EmployeeTaxStrategy();
public static final TaxStrategy COMPANY =
new CompanyTaxStrategy();
public static final TaxStrategy TRUST =
new TrustTaxStrategy();
private final TaxStrategy strategy;
private final double income;
public TaxPayer(TaxStrategy strategy, double income) {
this.strategy = strategy;
this.income = income;
}
public double getIncome() {
return income;
}
public double extortCash() {
return strategy.extortCash(income);
}
}
因为我们在TaxPayer类的内部定义了纳税人类型为常量,我们可以保持ReceiverOfRevenue类的正确性。
使用图例来演示这个解决方案,类图如下:

这个解决方案里面有一个问题。策略类的extortCash方法只有TaxPayer类的income一个输入参数。如果对于一个特定的TaxPayer,需要有多个因素来计算税费,那将怎么办呢?例如,当我结婚以后,南非的税法有一个可笑的条款是,结了婚的妇女比未结婚的妇女要交纳更多的税费。(为了演示这个税收的原因,我变成了妻子。即使我只是个学生,我妻子才是养家活口的人
J)
对于这个问题,有好几种解决方案。一种是将TaxStrategy的实例和一个TaxPayer对象相关联。这意味着状态是内在的,而我们将不能够共享TaxStrategy的实例。
另一种方法是,把所有的变量都传递到extortCash方法中。然而,这个方案又不是可维护的,因为当一个新的税费类型被定义时,你必须每一次更改该方法。你的政府可能会为小公司提供特种营业税分配。
还有一个方法是,传递TaxPayer 到extortCash()方法里去。如果你有一个Employee类是TaxPayer的子类,EmployeeTaxStrategy将需要强制类型转换成Employee的实例,去看一看它是不是一个已婚的。
TaxStrategy接口需要做如下修改:
public interface TaxStrategy {
public double extortCash(TaxPayer p);
}
TaxPayer类里面的extortCash()方法需要做如下的修改:
public class TaxPayer {
// the rest of the class is the same
public double extortCash() {
return strategy.extortCash(this);
}
}
让我们实现那个特别的例子——已婚的妇女需要交纳更多的税费。我们不希望TaxPayer类包含税费的详细信息,因为它只需要关注employees的信息。和其他的类相比较,重点是计算机算税费。相信没有得益的话,定价是零。
public class Employee extends TaxPayer {
public enum Gender { MALE, FEMALE };
private final boolean married;
private final Gender gender;
public Employee(TaxStrategy strategy, double income,
boolean married, Gender gender) {
super(strategy, income);
this.married = married;
this.gender = gender;
}
public boolean isMarried() {
return married;
}
public Gender getGender() {
return gender;
}
}
EmployeeTaxStrategy类需要找出一个特定的TaxPayer是不是一个已婚的妇女。为了达到这个目的,它需要强制类型转化为Employee。而转化错误只能在运行期内显示出来。我们需要在转化前
instanceof
检测它是否为Employee的实例。但是如果不是的话,我们会做什么呢?
public class EmployeeTaxStrategy implements TaxStrategy {
private static final double NORMAL_RATE = 0.40;
private static final double MARRIED_FEMALE_RATE = 0.48;
public double extortCash(TaxPayer p) {
Employee e = (Employee) p; // here we need to downcast!!!
if (e.isMarried() &&
e.getGender() == Employee.Gender.FEMALE) {
return e.getIncome() * MARRIED_FEMALE_RATE;
}
return e.getIncome() * NORMAL_RATE;
}
}
类图如下:

这个方法可以操作,但是还不是令人满意的。让我们看看其他的办法。
使用泛型避免强制类型转换
泛型是用来解决强制类型转换的,不错。但不幸的是,泛型也使得代码难以被阅读。如果你还在和“
<? super Object>
”这样符号斗争,那么你会发现我们的
Java 5 Delta Course课程是有用的,在这个课程的第三章,我们特别讲到了泛型。
我试着加入泛型,但是它比我想象的还要复杂。经过一段时间的尝试,我开始向Philip Wadler、Maurice Naftalin和Angelika Langer寻求帮助,他们都是Java的支持者,精彩的
Java Generics FAQ的作者。我们一起做出了如下的解决方法。
我们以对TaxStrategy的泛型开始,它被限制在TaxPayer和它的子类范围内。
public interface TaxStrategy<P extends TaxPayer> {
public double extortCash(P p);
}
我们接着来写被限制在Company范围的CompanyTaxStrategy类。换句话说,参数P是一个Company范围。在这个国家,一个小公司交纳更少的税费,这样的小公司被定义为收入小于一百万美元但是雇员超过5个。
public class CompanyTaxStrategy implements TaxStrategy<Company> {
private static final double BIG_COMPANY_RATE = 0.30;
private static final double SMALL_COMPANY_RATE = 0.15;
public double extortCash(Company company) {
if (company.getNumberOfEmployees() > 5
&& company.getIncome() < 1000000) {
return company.getIncome() * SMALL_COMPANY_RATE;
}
return company.getIncome() * BIG_COMPANY_RATE;
}
}
我们不得不修改了TaxPayer类,使得它包含一个泛型参数P,这个参数是TaxPayer的一个子类。表达式:
class
TaxPayer<P extends TaxPayer<P>>
看起来有点古怪,并且好像是一个习惯用法。它和让Ken Arnold困惑的
Enum<E extends Enum<E>>
相类似。经过考虑,我认为
Enum<E extends Enum<E>>
有充分的理由。然而,就像Philip Wadler指出的那样,既然Enum是一个泛型类,我们应该仅仅使用它来关联一个类型,(而不是一个泛型类)。将来,Java将会变得更加严格,那些夹生的类型将变得非法。因而我们必须选择是写
Enum<E extends Enum<E>>
还是
<E extends Enum<?>>
,它们中间第一个选择要正确些。即使编译器现在还不能显示它们之间的差别,不久的将来,可能会看到警告信息,所以我在我的代码中遵从了上面的结论。
在TaxPayer中,我想在extortCash()方法中直接使用类P的实现。然而,唯一取得一个实例而且没有编译器警告的方法是传递一个子类。我们可以通过一个叫
getDetailedType()
的工厂方法返回子类来解决这个问题。
public abstract class TaxPayer<P extends TaxPayer<P>> {
public static final TaxStrategy<Employee> EMPLOYEE =
new EmployeeTaxStrategy();
public static final TaxStrategy<Company> COMPANY =
new CompanyTaxStrategy();
public static final TaxStrategy<Trust> TRUST =
new TrustTaxStrategy();
private double income;
private TaxStrategy<P> strategy;
public TaxPayer(TaxStrategy<P> strategy, double income) {
this.strategy = strategy;
this.income = income;
}
protected abstract P getDetailedType();
public double getIncome() {
return income;
}
public double extortCash() {
return strategy.extortCash(getDetailedType());
}
}
当我们在写Employee类的时候,我们必须指出,Employees类只能创建一个TaxStrategy,任何的其他的TaxStrategy都会让编译器警告。
public class Employee extends TaxPayer<Employee> {
public enum Gender { MALE, FEMALE };
private final boolean married;
private final Gender gender;
public Employee(TaxStrategy<Employee> strategy, double income,
boolean married, Gender gender) {
super(strategy, income);
this.married = married;
this.gender = gender;
}
protected Employee getDetailedType() {
return this;
}
public boolean isMarried() {
return married;
}
public Gender getGender() {
return gender;
}
}
在这里,使用泛型的美妙之处是EmployeeTaxStrategy再也不需要强制类型转化。它通过泛型类紧紧地限制了Employees。
public class EmployeeTaxStrategy implements TaxStrategy<Employee> {
private static final double NORMAL_RATE = 0.40;
private static final double MARRIED_FEMALE_RATE = 0.48;
public double extortCash(Employee e) {
if (e.isMarried() &&
e.getGender() == Employee.Gender.FEMALE) {
return e.getIncome() * MARRIED_FEMALE_RATE;
}
return e.getIncome() * NORMAL_RATE;
}
}
你可以为employees使用不同的策略,但是EmployeeTaxStrategy只能仅仅使用在employees上面,这是通过编译器强迫的。
亲切的问候,
Heinz