在讲策略模式之前,我们先举个例子
假设现在要实现一个计算器程序,如果让你来设计,你会不会这么写?校验不是重点所以忽略
package cn.limingcheng.strategy;
import java.math.BigDecimal;
public class Calculator {
/**
* 两个数加减乘除操作
*
* @param val1 第一个数字
* @param val2 第二个数字
* @param calcType 计算方式
* @return
*/
public BigDecimal calc(BigDecimal val1, BigDecimal val2, String calcType) {
if ("+".equals(calcType)) {
return val1.add(val2);
} else if ("-".equals(calcType)) {
return val1.subtract(val2);
} else if ("*".equals(calcType)) {
return val1.multiply(val2);
} else if ("/".equals(calcType)) {
return val1.divide(val2);
}
return null;
}
}
代码可以很稳定的运行,但是如果现在增加求两个数余数怎么办?再增加个if else?很明显不符合开闭原则,修改原来的代码可能会造成想不到的结果,所以先做个小改进,将算法剥离出来
package cn.limingcheng.strategy;
import java.math.BigDecimal;
public class Calculator {
/**
* 两个数加减乘除操作
*
* @param val1 第一个数字
* @param val2 第二个数字
* @param calcType 计算方式
* @return
*/
public BigDecimal calc(BigDecimal val1, BigDecimal val2, String calcType) {
if ("+".equals(calcType)) {
return add(val1, val2);
} else if ("-".equals(calcType)) {
return subtract(val1, val2);
} else if ("*".equals(calcType)) {
return multiply(val1, val2);
} else if ("/".equals(calcType)) {
return divide(val1, val2);
}
return null;
}
/**
* 加法
*
* @param val1 第一个数字
* @param val2 第二个数字
* @return
*/
private BigDecimal add(BigDecimal val1, BigDecimal val2) {
return val1.add(val2);
}
/**
* 减法
*
* @param val1 第一个数字
* @param val2 第二个数字
* @return
*/
private BigDecimal subtract(BigDecimal val1, BigDecimal val2) {
return val1.subtract(val2);
}
/**
* 乘法
*
* @param val1 第一个数字
* @param val2 第二个数字
* @return
*/
private BigDecimal multiply(BigDecimal val1, BigDecimal val2) {
return val1.multiply(val2);
}
/**
* 除法
*
* @param val1 第一个数字
* @param val2 第二个数字
* @return
*/
private BigDecimal divide(BigDecimal val1, BigDecimal val2) {
return val1.divide(val2);
}
}
上面代码相比第一种将算法剥离出来,当只有一个算法改动时只需要修改相应的即可,不过问题依旧存在,当增加求余数方法的时候还要修改原来的类,这时策略模式闪亮登场
策略模式:定义了算法族分别封装起来,它们之间可以相互替换,此模式让算法的变化独立于使用算法的客户。八字总结:分离算法,选择实现
UML类图如下:
UML时序图如下:
结构:
- 策略(Strategy): 定义所有支持的算法的公共接口,Context可持该对象的引用,符合面向接口编程
- 策略实现(ConcreteStrategy):以 Strategy 接口实现某具体算法
- 上下文(Context):负责和具体的策略实现交互,内部会持有一个Strategy接口的引用,给客户端调用
编写步骤:
- 定义抽象策略角色(为策略对象定义一个公共的接口)
- 编写具体策略角色(实际上就是实现上面定义的公共接口)
- 定义上下文角色,内部持有策略接口的引用
代码体现
第一步定义策略接口角色
package cn.limingcheng.strategy;
/**
* 策略接口
*/
public interface IStrategy {
/**
* 定义抽象方法约束具体算法的实现
*/
public void algorithmInterface();
}
第二步编写具体策略角色,我这边定义两个:ConcreteStrategyA 和 ConcreteStrategyB
package cn.limingcheng.strategy;
/**
* 策略的具体实现A
*/
public class ConcreteStrategyA implements IStrategy {
/**
* 具体的算法实实现
*/
@Override
public void algorithmInterface() {
System.out.println("ConcreteStrategyA method");
}
}
package cn.limingcheng.strategy;
/**
* 策略的具体实现B
*/
public class ConcreteStrategyB implements IStrategy {
/**
* 具体的算法实实现
*/
@Override
public void algorithmInterface() {
System.out.println("ConcreteStrategyB method");
}
}
第三步定义上下文角色
package cn.limingcheng.strategy;
/**
* 策略上下文
*/
public class Context {
/**
* 面向接口编程,持有策略接口
*/
private IStrategy strategy;
/**
* 通过构造器注入一个具体的策略实现类
*
* @param strategy 具体的策略
*/
public Context(IStrategy strategy) {
this.strategy = strategy;
}
/**
* 调用策略实现的方法
*/
public void call() {
strategy.algorithmInterface();
}
}
最后看成果吧,创建一个客户端程序测试
package cn.limingcheng.strategy;
/**
* 客户端
*/
public class Client {
public static void main(String[] args) {
// 实例化具体要使用的策略对象
IStrategy strategy = new ConcreteStrategyA();
// 创建策略上下文并且传入具体策略对象
Context context = new Context(strategy);
// 调用上下文提供的方法来完成对具体策略实现的回调
context.call();
}
}
接下来让我们用策略模式改造计算器程序吧
先创建计算器策略接口
package cn.limingcheng.strategy.calc;
import java.math.BigDecimal;
/**
* 计算器策略接口
*/
public interface ICalculatorStrategy {
/**
* 两个数加减乘除操作
*
* @param val1 第一个数字
* @param val2 第二个数字
* @return
*/
public BigDecimal calc(BigDecimal val1, BigDecimal val2);
}
实现具体策略,为了篇幅这边就写加法以及减法的实现类
package cn.limingcheng.strategy.calc;
import java.math.BigDecimal;
/**
* 加法策略实现类
*/
public class AddCalculatorStrategy implements ICalculatorStrategy {
/**
* 实现加法算法
*
* @param val1 第一个数字
* @param val2 第二个数字
* @return
*/
@Override
public BigDecimal calc(BigDecimal val1, BigDecimal val2) {
return val1.add(val2);
}
}
package cn.limingcheng.strategy.calc;
import java.math.BigDecimal;
/**
* 减法策略实现类
*/
public class SubtractCalculatorStrategy implements ICalculatorStrategy {
/**
* 实现减法算法
*
* @param val1 第一个数字
* @param val2 第二个数字
* @return
*/
@Override
public BigDecimal calc(BigDecimal val1, BigDecimal val2) {
return val1.subtract(val2);
}
}
定义计算器策略上下文
package cn.limingcheng.strategy.calc;
import java.math.BigDecimal;
/**
* 计算器策略上下文
*/
public class CalculatorContext {
/**
* 持有计算器策略接口引用
*/
private ICalculatorStrategy calculatorStrategy;
/**
* 通过构造器注入具体计算器策略对象
*
* @param calculatorStrategy
*/
public CalculatorContext(ICalculatorStrategy calculatorStrategy) {
this.calculatorStrategy = calculatorStrategy;
}
/**
* 对外提供方法实现对具体策略的方法调用
*
* @param val1
* @param val2
* @return
*/
public BigDecimal calc(BigDecimal val1, BigDecimal val2) {
return calculatorStrategy.calc(val1, val2);
}
}
创建客户端进行测试
package cn.limingcheng.strategy.calc;
import java.math.BigDecimal;
/**
* 计算器客户端
*/
public class CalculatorClient {
public static void main(String[] args) {
// 实例化加法策略
ICalculatorStrategy add = new AddCalculatorStrategy();
// 实例化计算器策略上下文
CalculatorContext calculatorContext = new CalculatorContext(add);
// 调用具体策略算法
BigDecimal result = calculatorContext.calc(new BigDecimal("10"), new BigDecimal(20));
System.out.println(result);
}
}
使用策略模式改造计算器代码后,试想如果想增加求余数的策略怎么办?我们只需要增加一个求余数的策略实现类
package cn.limingcheng.strategy.calc;
import java.math.BigDecimal;
/**
* 余数策略实现类
*/
public class RemainderCalculatorStrategy implements ICalculatorStrategy {
/**
* 实现余数算法
*
* @param val1 第一个数字
* @param val2 第二个数字
* @return
*/
@Override
public BigDecimal calc(BigDecimal val1, BigDecimal val2) {
return val1.remainder(val2);
}
}
客户端指定策略为余数策略即可
package cn.limingcheng.strategy.calc;
import java.math.BigDecimal;
/**
* 余数客户端
*/
public class RemainderCalculatorClient {
public static void main(String[] args) {
// 实例化余数策略
ICalculatorStrategy remainder = new RemainderCalculatorStrategy();
// 实例化计算器策略上下文
CalculatorContext remainderCalculatorContext = new CalculatorContext(remainder);
// 调用具体策略算法
BigDecimal remaindeResult = remainderCalculatorContext.calc(new BigDecimal("10"), new BigDecimal(3));
System.out.println(remaindeResult);
}
}
这样改造符合三个设计原则
- 开闭原则,增加功能只是扩展,并没有修改原代码
- 依赖导致原则,采用面向接口编程,策略上下文引用策略接口,而不是针对具体实现编程
- 里氏替换原则,基类之间可以相互替换
策略模式设计规范:
- 找出应用中可能需要变化之处,把它们独立出来,不要和那些不需要变化的代码混在一起
- 面向接口编程,不是针对具体实现编程
- 多用组合、少用继承
策略模式优点
- 策略模式的功能就是通过抽象、封装来定义一系列的算法,使得这些算法可以相互替换,所以为这些算法定义一个公共的接口,以约束这些算法的功能实现。如果这些算法具有公共的功能,可以将接口变为抽象类,将公共功能放到抽象父类里面。
- 策略模式的一系列算法是可以相互替换的、是平等的,写在一起就是if-else组织结构,如果算法实现里又有条件语句,就构成了多重条件语句,可以用策略模式,避免这样的多重条件语句
- 扩展性更好:在策略模式中扩展策略实现非常的容易,只要新增一个策略实现类,然后在使用策略实现的地方,使用这个新的策略实现就好了
策略模式缺点
- 客户端必须了解所有的策略,清楚它们的不同
- 增加了对象的数量,由于策略模式将每个具体的算法都单独封装为一个策略类,如果可选的策略有很多的话,那对象的数量也会很多。
- 只适合偏平的算法结构,由于策略模式的各个策略实现是平等的关系(可相互替换),实际上就构成了一个扁平的算法结构。即一个策略接口下面有多个平等的策略实现(多个策略实现是兄弟关系),并且运行时只能有一个算法被使用。这就限制了算法的使用层级,且不能被嵌套