####策略模式介绍
在软件开发中也常常遇到这样的情况:实现某一个功能可以有多种算法或者策略,我们根据实际情况选择不同的算法或者策略来完成该功能。例如,排序算法,可以使用插入排序、归并排序、冒泡排序等。
针对这种情况,一种方法可以将多种算法写在一个类中,比如将多种排序算法写在一个类中,一个方法对应一个具体的排序算法;也可以将这些算法封装在一个统一的方法中,通过if…else等条件判断语句来选择具体的算法。这两种实现方法我们都可以称为硬编码。然而,多个算法集中在一个类中时,这个类就会变得臃肿,这个类的维护成本就会变高,如果需要一个新的算法,就需要修改封装算法类的源代码。这就违反了OCP原则和单一职责原则。
如果将这些算法或者策略抽象出来,提供一个统一的接口,不同的算法或者策略有不同的实现类,这样在程序客户端就可以通过注入不同的实现对象来实现算法或者策略的动态替换,这种模式的可扩展性、可维护性也就更高,也就是我们要说的策略模式。
####策略模式的定义
策略模式定义了一系列算法,并将每一个算法封装起来,而且使它们还可以互相替换。策略模式让算法独立于使用它的客户而独立变化。
####策略模式的使用场景
- 针对同一类型问题的多种处理方式,仅仅是具体行为有差别时。
- 需要安全地封装多种同一类型的操作时。
- 出现同一抽象类有多个子类,而又需要使用if-else或者switch-case来选择具体子类时。
####策略模式的UML类图
- Context—用来操作策略的上下文环境;
- Stragety—策略的抽象;
- ConcreteStragetyA、ConcreteStragetyB—具体的策略实现。
####策略模式的简单实现
通常如果一个问题有多个解决方案时,最简单的方式是用if-else或者switch-case方式选择不同的解决方案,但是会带来一系列问题,例如耦合性高、代码臃肿、难以维护等。如果解决方案中包括大量的处理逻辑需要封装,或者处理方式变动较大的时候则就显得混乱、复杂,当需要增加一种方案时还需要修改类中的代码。这就没有遵循开闭原则。
下面我们以在北京坐公共交通工具的费用计算来演示一个简单示例。
package com.guifa.stragetydemo;
public class PriceCalculator {
/**
* 公交车类型
*/
private static final int BUS = 1;
/**
* 地铁类型
*/
private static final int SUBWAY = 2;
public static void main(String[] args) {
PriceCalculator priceCalculator = new PriceCalculator();
System.out.println("坐16公里的公交车票价为:" + priceCalculator.calculatePrice(16, BUS));
System.out.println("坐16公里的地铁票价为:" + priceCalculator.calculatePrice(16, SUBWAY));
}
/**
* 坐公共交通的费用
*
* @param km 距离
* @param type 乘坐类型
* @return 价格
*/
private int calculatePrice(int km, int type) {
if (type == BUS) {
return busPrice(km);
} else if (type == SUBWAY) {
return subwayPrice(km);
} else {
return 0;
}
}
/**
* 公交车价格:10公里(含)内2元,10公里以上部分,每增加1元可乘坐5公里
*
* @param km 距离
* @return 价格
*/
private int busPrice(int km) {
// 超过10公里的距离
int extraTotal = km - 10;
// 超过的距离是5公里的倍数
int extraFactor = extraTotal / 5;
// 超过的距离对5公里取余
int fraction = extraTotal % 5;
// 价格计算
int price = 2 + extraFactor * 1;
return fraction > 0 ? ++price : price;
}
/**
* 地铁价格:6公里(含)内3元;6公里至12公里(含)4元;12公里至22公里(含)5元;
* 22公里至32公里(含)6元;32公里以上部分,每增加1元可乘坐20公里。
*
* @param km 距离
* @return 价格
*/
private int subwayPrice(int km) {
if (km <= 6) {
return 3;
} else if (km > 6 && km <= 12) {
return 4;
} else if (km > 12 && km <= 22) {
return 5;
} else if (km > 22 && km <= 32) {
return 6;
} else {
// 其他距离简化为7元
return 7;
}
}
}
PriceCalculator类很明显的问题就是并不是单一职责,首先它承担了计算公交车和地铁乘坐价格的职责;另一个问题就是通过if-else的形式来判断使用哪种计算形式。当我们增加一种出行方式时,如出租车,那么我们就需要在PriceCalculator中增加一个方法来计算出租车出行的价格,并且在calculatePrice(int km, int type)函数中增加一个判断来计算出租车的价格,这时代码就比较混乱了,当价格的计算方式变化时,需要直接修改这个类中的代码,那么很可能有一段代码是其他几个计算方法所共同使用的,就容易引起错误。另外,在增加出行方式时,我们还需要继续添加if-else,它会使得代码变得越来越臃肿,难以维护。为了解决这个问题,可以使用策略模式(当然,也可以把每个计算方法独立成一个函数,让外部调用对应的方法即可,但是这也是另一种耦合的形式,对于可变性较大的算法族来说还是不合适使用这种方式。)
下面我们对于上述示例用策略模式进行重构。
首先我们需要定义一个抽象的价格计算接口,这里命名为CalculateStrategy,具体代码如下:
package com.guifa.stragetydemo;
public interface CalculateStrategy {
/**
* 按距离来计算价格
*
* @param km 距离
* @return 价格
*/
int calculatePrice(int km);
}
对于每一种出行方式我们都有哦一个独立的计算策略类,这些策略类都实现了CalculateStrategy接口,例如下面是公交车和地铁的计算策略类:
package com.guifa.stragetydemo;
/**
* 公交车价格计算策略
*/
public class BusStrategy implements CalculateStrategy {
/**
* 公交车价格:10公里(含)内2元,10公里以上部分,每增加1元可乘坐5公里
*
* @param km 距离
* @return 价格
*/
@Override
public int calculatePrice(int km) {
// 超过10公里的总距离
int extraTotal = km - 10;
// 超过的距离是5公里的倍数
int extraFactor = extraTotal / 5;
// 超过的距离对5公里取余
int fraction = extraTotal % 5;
// 计算价格
int price = 2 + extraFactor * 1;
return fraction > 0 ? ++price : price;
}
}
package com.guifa.stragetydemo;
/**
* 地铁价格计算策略
*/
public class SubwayStrategy implements CalculateStrategy {
/**
* 地铁价格:6公里(含)内3元;6公里至12公里(含)4元;12公里至22公里(含)5元;
* 22公里至32公里(含)6元;32公里以上部分,每增加1元可乘坐20公里。
*
* @param km 距离
* @return 价格
*/
@Override
public int calculatePrice(int km) {
if (km <= 6) {
return 3;
} else if (km > 6 && km <= 12) {
return 4;
} else if (km > 12 && km <= 22) {
return 5;
} else if (km > 22 && km <= 32) {
return 6;
} else {
// 其他距离简化为7元
return 7;
}
}
}
我们再创建一个扮演Context角色的类,这里将它命名为TranficCalculator,代码如下:
package com.guifa.stragetydemo;
/**
* 公交出行价格计算器
*/
public class TranficCalculator {
CalculateStrategy strategy;
public static void main(String[] args) {
TranficCalculator calculator = new TranficCalculator();
// 设置计算策略
calculator.setStrategy(new BusStrategy());
// 计算价格
System.out.println("公交车乘16公里的价格:" + calculator.strategy(16));
}
public int strategy(int km) {
return strategy.calculatePrice(km);
}
public void setStrategy(CalculateStrategy strategy) {
this.strategy = strategy;
}
}
这种方案在隐藏实现的同时,可扩展性变得很强,例如,当我们需要增加出租车的计算策略时,只需要添加一个出租车计算策略类,然后将该策略设置给TranficCalculator,最好直接通过TranficCalculator对象的计算方法即可。示例代码如下:
package com.guifa.stragetydemo;
/**
* 出租车计算策略
*/
public class TaxiStrategy implements CalculateStrategy {
@Override
public int calculatePrice(int km) {
// 价格我们简单计算为公里数*2
return km * 2;
}
}
将策略注入到TranficCalculator中。
package com.guifa.stragetydemo;
/**
* 公交出行价格计算器
*/
public class TranficCalculator {
CalculateStrategy strategy;
public static void main(String[] args) {
TranficCalculator calculator = new TranficCalculator();
// 设置计算策略
calculator.setStrategy(new TaxiStrategy());
// 计算价格
System.out.println("公交车乘16公里的价格:" + calculator.strategy(16));
}
public int strategy(int km) {
return strategy.calculatePrice(km);
}
public void setStrategy(CalculateStrategy strategy) {
this.strategy = strategy;
}
}
通过上述示例我们可以清晰地看出二者的区别所在。前者通过if-else来解决问题,虽实现简单,类型层级单一,但暴漏的问题也很明显,即代码臃肿,逻辑复杂,难以升级和维护,没有结构可言;后者则是通过建立抽象,将不同的策略构建成一个具体的策略实现,通过不同的策略实现算法替换。在简化逻辑、结构的同时,增强了系统的可读性、稳定性、可扩展性,这对于较为复杂的业务逻辑显得更为直观,扩展也更为方便。
####总结
策略模式主要用来分离算法,在相同的行为抽象下有不同的具体实现策略。这个模式很好地演示了开闭原则,也就是定义抽象,注入不同的实现,从而达到很好的可扩展性。
#####优点
- 结果清晰明了、使用简单直观;
- 耦合度相对而言较低,扩展方便;
- 操作封装也更为彻底,数据更为安全。
#####缺点 - 随着策略的增加,子类也会变得繁多。