一、基本概念
什么是策略模式?
策略这个词应该怎么理解呢,打个比方说,我们出门的时候选择不同的出行方式,比如步行、骑自行车、坐公交、坐火车、坐飞机、坐火箭等等,这些出行方式,每一种都是一个策略。
再比如我们去逛商场,商场现在正在搞活动,有打折的、满减的、返利的等等,其实不管商场如何进行促销,说到底都是一些算法,这些算法本身就是一种策略,并且这些算法是随时可能互相替换的,比如针对同一件商品,今天打八折、明天满100减30,这些策略间是可以互换的。
策略模式(Strategy),定义了一组算法,将每个算法都封装起来,并且使它们之间可以互换。
二、策略模式的优缺点
1、优点
(1)算法可以自由切换
(2)避免使用多重条件判断
(3)扩展性良好,增加一个策略只需实现接口即可
2、缺点
(1)策略类数量会增多,每个策略都是一个类,复用性很小
(2)所有的策略类都需要对外暴露
三、策略模式的应用场景
1、何时使用
对于业务开发来说,业务逻辑的复杂是必然的,随着业务发展,需求只会越来越复杂,为了考虑到各种各样的情况,代码中不可避免的会出现很多if-else。
一旦代码中if-else过多,就会大大的影响其可读性和可维护性,而且代码显得很low。
策略模式完美的解决了ifelse的烦恼!
2、方法
将这些算法封装成一个一个的类,任意的替换
3、应用实例
- 出行方式,自行车、汽车等,每一种出行方式都是一个策略
- 商场促销方式,打折、满减等
- java AWT中 LayoutManager ,即布局管理器
4、注意事项
如果一个系统的策略多于四个,就需要考虑使用混合模式解决策略类膨胀的问题
四、代码实例
下面就以商场促销为例使用策略模式实现商场促销算法。UML图如下:
1、上下文类
首先声明一个 CashSuper 对象,通过构造方法,传入具体的收费策略, getResult() 方法的功能为根据收费策略的不同获取计算结果。
package designMode.strategy;
public class CashContext {
private CashSuper cashSuper;
public CashContext(CashSuper cashSuper) {
this.cashSuper = cashSuper;
}
public double getResult(double money){
return cashSuper.acceptCash(money);
}
}
2、现金收费抽象类
策略类,为抽象类,抽象出收费的方法供子类实现。
package designMode.strategy;
public abstract class CashSuper {
public abstract double acceptCash(double money);
}
3、正常收费子类
package designMode.strategy;
public class CashNormal extends CashSuper {
@Override
public double acceptCash(double money) {
return money;
}
}
4、打折收费子类
package designMode.strategy;
public class CashRebate extends CashSuper {
private double moneyRebate = 0.8;
public CashRebate(double moneyRebate) {
this.moneyRebate = moneyRebate;
}
@Override
public double acceptCash(double money) {
return money*moneyRebate;
}
}
5、返利收费子类
package designMode.strategy;
public class CashReturn extends CashSuper {
private double moneyConditation = 0.0;
private double moneyReturn = 0.0d;
public CashReturn(double moneyConditation, double moneyReturn) {
this.moneyConditation = moneyConditation;
this.moneyReturn = moneyReturn;
}
@Override
public double acceptCash(double money) {
double result = money;
if(money>moneyConditation){
result = money-Math.floor(money/moneyConditation)*moneyReturn;
}
return result;
}
}
6、client客户端
package designMode.strategy;
import java.util.Scanner;
public class Client {
public static void main(String[] args) {
CashContext cashContext = null;
Scanner scanner = new Scanner(System.in);
System.out.println("请输入打折方式(1/2/3):");
int in = scanner.nextInt();
String type = "";
switch (in){
case 1:
cashContext = new CashContext(new CashNormal());
type += "正常收费";
break;
case 2:
cashContext = new CashContext(new CashReturn(300,100));
type +="满300返100";
break;
case 3:
cashContext = new CashContext(new CashRebate(0.8));
type += "打八折";
break;
default:
System.out.println("请输入1/2/3");
break;
}
double totalPrices = 0;
System.out.print("请输入单价:");
double price = scanner.nextDouble();
System.out.println("请输入数量:");
double num = scanner.nextDouble();
totalPrices = cashContext.getResult(price * num);
System.out.println("单价:" + price + ",数量:" + num + ",类型:" + type + ",合计:" + totalPrices);
scanner.close();
}
}
7、运行结果
五、策略模式与工程模式的区别
1、策略模式是行为性模式,适应行为的变化 ,强调父类的调用子类的特性 。
工厂模式是创建型模式,适应对象的变化,强调统一接口 。
2、策略模式封装行为,调用的时候必须先制定实例化具体的类,再调用抽象的方法; 策略模式的作用是让一个对象在许多行为中选择一种行为。
工厂模式封装对象,实例化对象后调用的时候要知道具体的方法。
3、策略模式是调用不同类方法, 工厂模式是对父类进行重写。
这俩个模式本来就是解决类似的问题,可以说是孪生兄弟,且内部实现都差不多,都是通过子类来覆盖父类而已,不过简单工厂是把父类直接摆在客户端,而策略模式是将父类隐藏在Context里面,这样封装更好。
4、举个例子
(1)产品之于加减乘除,水果之于苹果梨橘子香蕉,文具之于笔尺刀,这时产品比较具体、有限和没有多个算法重叠,这时实用简单工厂模式。
(2)产品之于商场促销中的返利(可为300返100、500返200、10000返500等等无数)、折扣(2折、2.5折、6折、9折、9.1折等等无数)、正常购买、消费积分(100元10积分、200元30积分等等无数),这时产品构造又多次重叠,且有在不同时刻应用不同的规则时使用策略模式。
5、总结
简单工厂模式只是解决了对象的创建问题,工厂需要包括所有的产品对象的创建,如果产品对象形式经常变化,就需要经常改动工厂,以致代码重新编译。所以策略模式就诞生了,策略模式---它定义了算法家族,分别封装起来,而不是像简单产品模式一样定义所有的产品类,让他们之间可以互相替换,此模式让算法的变化,不会影响到使用算法的客户,使客户拥有相同的访问过程。
简单工厂模式的核心是“简单工厂模式就是用来封装所有的产品对象的”。
策略模式理解核心是“策略模式就是用来封装算法的,但在实践中,我们发现可以用它来封装几乎任何类型的规则,只要在分析过程中遇到需要在不同时间应用不同的业务规则,就可以考虑使用策略模式处理这种变化的可能性”。
在基本的策略模式中,选择所用的具体实现的算法的职责由客户端对象承担,并转给策略模式的Context对象。这是策略模式本身纯粹的定义,所以,“选择所用最终怎样处理”还有很多文章可做。
看了课本之后对于这两个模式还是有很多不理解的地方,但是相信随着对设计模式进一步的学习,能够更好地体会到这其中的奥妙,学习是一个循序渐进的过程。
六、SpringMVC中的策略模式
1、getDefaultStrategies
在DispatchServlet中的初始化组件中,用到了getDefaultStrategies方法,来决定不同组件的默认类型以实现组件的初始化操作。
// 传入ApplicationContext上下文和策略接口的Class类型
protected <T> List<T> getDefaultStrategies(ApplicationContext context, Class<T> strategyInterface) {
// 相应组件的类名
String key = strategyInterface.getName();
// 从property中获取当前策略接口实现类的类名集合
String value = defaultStrategies.getProperty(key);
if (value != null) {
// 获取策略接口所有实现类的类名
String[] classNames = StringUtils.commaDelimitedListToStringArray(value);
List<T> strategies = new ArrayList<T>(classNames.length);
for (String className : classNames) {
try {
// 创建相应实现类的bean,并放入集合中
Class<?> clazz = ClassUtils.forName(className, DispatcherServlet.class.getClassLoader());
Object strategy = createDefaultStrategy(context, clazz);
strategies.add((T) strategy);
}
catch (ClassNotFoundException ex) {
throw new BeanInitializationException(
"Could not find DispatcherServlet's default strategy class [" + className +
"] for interface [" + key + "]", ex);
}
catch (LinkageError err) {
throw new BeanInitializationException(
"Error loading DispatcherServlet's default strategy class [" + className +
"] for interface [" + key + "]: problem with class file or dependent class", err);
}
}
// 返回策略接口实现类的集合
return strategies;
}
else {
return new LinkedList<T>();
}
}
// 初始化真正调用的重载方法,默认返回策略实现类的第一个
protected <T> T getDefaultStrategy(ApplicationContext context, Class<T> strategyInterface) {
List<T> strategies = getDefaultStrategies(context, strategyInterface);
if (strategies.size() != 1) {
throw new BeanInitializationException(
"DispatcherServlet needs exactly 1 strategy for interface [" + strategyInterface.getName() + "]");
}
return strategies.get(0);
}
我们可以看到,DispatchServlet在初始化组件时,会传入相应组件的接口,获取到该组件的实现类集合,并将第一个实现类作为默认的组件使用,例如我们来看initLocaleResolver方法,它初始化了一个默认的本地化处理组件。
private void initLocaleResolver(ApplicationContext context) {
try {
this.localeResolver = context.getBean(LOCALE_RESOLVER_BEAN_NAME, LocaleResolver.class);
if (logger.isDebugEnabled()) {
logger.debug("Using LocaleResolver [" + this.localeResolver + "]");
}
}
catch (NoSuchBeanDefinitionException ex) {
// We need to use the default.
this.localeResolver = getDefaultStrategy(context, LocaleResolver.class);
if (logger.isDebugEnabled()) {
logger.debug("Unable to locate LocaleResolver with name '" + LOCALE_RESOLVER_BEAN_NAME +
"': using default [" + this.localeResolver + "]");
}
}
}
它传入了LocaleResolver.class,这个类有多个实现类,包括AcceptHeaderLocaleResolver、CookieLocaleResolver、FixedLocaleResolver等,对应了多种不同的处理方式,你可以决定用哪一种处理方式(绑定对应的组件就好了)。但试想一下,如果用if-else来决定用那种处理方式,光一个LocaleResolver,代码就将变得又长又臭,更何况springMVC还要初始化这么多其他组件。策略模式就用了面向对象的思想,用接口、继承、多态来代替if-else,增加了代码的可读性和可维护性。
2、其他使用到策略模式的地方
springMVC在决定request的media types时也用到了策略模式。其中的ContentNegotiationManager是最核心的一个类,其中有一个方法我们可以重点来看一下:
@Override
// 处理请求的mediaTypes
public List<MediaType> resolveMediaTypes(NativeWebRequest request) throws HttpMediaTypeNotAcceptableException {
// 遍历绑定的strategy来处理请求,其中ContentNegotiationStrategy是一个接口,它的实现类包含了多种决定mediaTypes的策略
for (ContentNegotiationStrategy strategy : this.strategies) {
// 如果某一策略实现类识别的请求的mediaTypes,则返回,未识别则继续遍历
List<MediaType> mediaTypes = strategy.resolveMediaTypes(request);
if (mediaTypes.isEmpty() || mediaTypes.equals(MEDIA_TYPE_ALL)) {
continue;
}
return mediaTypes;
}
return Collections.emptyList();
}
ContentNegotiationStrategy接口只有一个方法:
public interface ContentNegotiationStrategy {
// 返回请求的mediaTypes集合,如果无法识别则抛出异常
List<MediaType> resolveMediaTypes(NativeWebRequest webRequest)
throws HttpMediaTypeNotAcceptableException;
}
跟上面的LocaleResolver接口一样,ContentNegotiationStrategy也有多种不同的实现类,这些实现类大都对应了不同的处理方式。策略模式在此情形下,使得在处理request的mediaTypes时,省去了大量if-else判断,实现了策略算法与调用逻辑上的解耦。
3、总结
从上面两个简单的例子中我们可以看到,使用策略模式的实现方式是:
(1)定义策略接口->实现不同的策略类->利用多态或其他方式调用策略;
(2)从springMVC处理request的media types中,我们又可以学到:当我们遇到的问题时,如果无法事先知道哪种处理方式合适,可以使用策略模式。当某一种策略模式匹配时,返回正确结果,以此解决问题;
此外,策略模式使用的场景,实现的方式还有很多,这里就不一一赘述了。
总之,使用策略模式对我们处理和解决问题、算法(解决方式)和使用解耦、代码的可读性和可维护性方面都有极大的好处。