第二章 策略模式 (Strategy)
问题引入
假设说,现在我们需要给超市的收银员写一款计算器,能让他们计算出顾客所购买商品的总价格,其中每种商品有不同的价格和数量。需求只有这些的话,这个计算器比较容易写:
public class Cashier {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
double totalPrice = 0, price = 0, number = 0;
do {
System.out.print("请输入商品单价:");
price = scanner.nextDouble();
System.out.print("请输入商品数目:");
number = scanner.nextInt();
totalPrice += price * number;
System.out.print("是否结束(y/n):");
scanner.nextLine();
} while ("n".equals(scanner.nextLine()));
System.out.print("总价格为:" + totalPrice);
}
}
运行结果如下:
请输入商品单价:1
请输入商品数目:2
是否结束(y/n):n
请输入商品单价:4
请输入商品数目:6
是否结束(y/n):y
总价格为:26.0
Process finished with exit code 0
Cashier就简单的实现了收银员的功能。如果此时想加入打折的功能,比如说商品打9折,那么只需要再增加一个参数就可以了:
public class Cashier2 {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
double totalPrice = 0, price = 0, number = 0, discount = 0;
do {
System.out.print("请输入商品单价:");
price = scanner.nextDouble();
System.out.print("请输入商品数目:");
number = scanner.nextInt();
System.out.print("请输入打折(小数):");
discount = scanner.nextDouble();
discount = discount <= 0 ? 1 : discount;
totalPrice += price * number * discount;
System.out.print("是否结束(y/n):");
scanner.nextLine();
} while ("n".equals(scanner.nextLine()));
System.out.print("总价格为:" + totalPrice);
}
}
运行结果如下:
请输入商品单价:1
请输入商品数目:2
请输入打折(小数):0.5
是否结束(y/n):n
请输入商品单价:2
请输入商品数目:5
请输入打折(小数):0.5
是否结束(y/n):y
总价格为:6.0
Process finished with exit code 0
但是如果有其他的优惠呢,比如像是满200减50,满500打8折。这样每种的优惠都有不同的计算方式,或者称作是算法,就不能每次有新的优惠就去改Cashier的源码。可以把算法抽象出一个接口,具体的每种算法就是各个实现类。利用策略模式,代码如下:
策略接口:
public interface Strategy {
/**
* 计算优惠之后的价格
*
* @param money
* @return
*/
double figureUp(double money);
}
正常不打折,打折,满减的实现类如下:
public class NormalStrategy implements Strategy {
public double figureUp(double money) {
return money;
}
}
public class RebateStrategy implements Strategy {
private double percent;
public RebateStrategy(double percent) {
this.percent = percent;
}
public double figureUp(double money) {
return money * percent;
}
}
public class ReturnStrategy implements Strategy {
private double conditionNumber;
private double discountNumber;
public ReturnStrategy(double conditionNumber, double discountNumber) {
this.conditionNumber = conditionNumber;
this.discountNumber = discountNumber;
}
public double figureUp(double money) {
if (money < conditionNumber) {
return money;
}
double number = Math.floor(money / conditionNumber);
return money - number * discountNumber;
}
}
策略上下文:
public class StrategyContext {
private Strategy strategy;
public StrategyContext(Strategy strategy){
this.strategy = strategy;
}
public double compute(double money){
return strategy.figureUp(money);
}
}
测试类:
public class Cashier3 {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
double totalPrice = 0, price = 0, number = 0;
int discount = 0;
StrategyContext strategyContext = null;
do {
System.out.print("请输入商品单价:");
price = scanner.nextDouble();
System.out.print("请输入商品数目:");
number = scanner.nextInt();
System.out.print("请输入优惠策略(1:正常,2:打9折,3:满200减50):");
discount = scanner.nextInt();
switch (discount) {
case 1:
strategyContext = new StrategyContext(new NormalStrategy());
price = strategyContext.compute(price);
break;
case 2:
strategyContext = new StrategyContext(new RebateStrategy(0.9));
price = strategyContext.compute(price);
break;
case 3:
strategyContext = new StrategyContext(new ReturnStrategy(200, 50));
price = strategyContext.compute(price);
break;
}
totalPrice += price * number;
System.out.print("是否结束(y/n):");
scanner.nextLine();
} while ("n".equals(scanner.nextLine()));
System.out.print("总价格为:" + totalPrice);
}
}
运行结果:
请输入商品单价:260
请输入商品数目:1
请输入优惠策略(1:正常,2:打9折,3:满200减50):3
是否结束(y/n):n
请输入商品单价:100
请输入商品数目:1
请输入优惠策略(1:正常,2:打9折,3:满200减50):2
是否结束(y/n):y
总价格为:300.0
Process finished with exit code 0
这样就将Cashier和具体的算法策略进行了解耦。不同的打折或者满减只需要传入不同的参数就可以了。相同类型的优惠抽象成了算法策略类,并且他们都继承自算法策略接口。但是还有点不足的是,还是需要在Cashier类中进行条件判断,如果增加新的算法策略还是需要修改Cashier类。可以通过融合简单工厂模式来改造,让Cashier只需要依赖StrategyContext类即可,有新的算法加入,对于Cashier来说也是无感知的。代码如下:
修改StrategyContext类:
public class StrategyContext2 {
private Strategy strategy;
public StrategyContext2(int strategyType){
switch (strategyType) {
case 1:
strategy = new NormalStrategy();
break;
case 2:
strategy = new RebateStrategy(0.9);
break;
case 3:
strategy = new ReturnStrategy(200, 50);
break;
}
}
public double compute(double money){
return strategy.figureUp(money);
}
}
Cashier类修改:
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
double totalPrice = 0, price = 0, number = 0;
int discount = 0;
StrategyContext2 strategyContext2 = null;
do {
System.out.print("请输入商品单价:");
price = scanner.nextDouble();
System.out.print("请输入商品数目:");
number = scanner.nextInt();
System.out.print("请输入优惠策略(1:正常,2:打9折,3:满200减50):");
discount = scanner.nextInt();
strategyContext2 = new StrategyContext2(discount);
price = strategyContext2.compute(price);
totalPrice += price * number;
System.out.print("是否结束(y/n):");
scanner.nextLine();
} while ("n".equals(scanner.nextLine()));
System.out.print("总价格为:" + totalPrice);
}
这样Cashier类只依赖于StrategyContext类,而无需依赖于具体的策略类。如果有新的优惠策略,对于调用者也就是Cashier来说是无感知的,它只需要传入新的参数即可。
小结
策略(Strategy)模式的定义:该模式定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用算法的客户。策略模式属于对象行为模式,它通过对算法进行封装,把使用算法的责任和算法的实现分割开来,并委派给不同的对象对这些算法进行管理。
策略模式的主要优点如下。
- 多重条件语句不易维护,而使用策略模式可以避免使用多重条件语句。
策略模式提供了一系列的可供重用的算法族,恰当使用继承可以把算法族的公共代码转移到父类里面,从而避免重复的代码。
策略模式可以提供相同行为的不同实现,客户可以根据不同时间或空间要求选择不同的。 - 策略模式提供了对开闭原则的完美支持,可以在不修改原代码的情况下,灵活增加新算法。
- 策略模式把算法的使用放到环境类中,而算法的实现移到具体策略类中,实现了二者的分离。
其主要缺点如下。
- 客户端必须理解所有策略算法的区别,以便适时选择恰当的算法类。
- 策略模式造成很多的策略类。