策略模式
动机
在软件构建过程中,某些对象使用的算法可能多种多样,经常改
变,如果将这些算法都编码到对象中,将会使对象变得异常复杂;而且有时候支持不使用的算法也是一个性能负担。
如何在运行时根据需要透明地更改对象的算法?将算法与对象本身解耦,从而避免上述问题?
如何在运行时根据需要透明地更改对象的算法
要在运行时根据需要透明地更改对象的算法,并将算法与对象本身解耦,可以采取以下步骤:
1.定义策略接口:首先,需要定义一个策略接口或抽象类,该接口或抽象类声明了所有可能算法的共同行为。这确保了所有具体的算法实现都有相同的接口,从而可以互换使用1。
2.实现具体策略:创建具体的策略类,每个类都实现了上述策略接口,并且每个类都封装了一种特定的算法实现。这些具体的策略类可以相互替换,而不会影响使用它们的客户端代码1。
3.使用上下文类:创建一个上下文类,该类持有一个策略对象的引用。上下文类负责执行策略对象中的算法,并在运行时根据需要动态地设置策略对象1。
4.动态选择策略:客户端代码在运行时根据特定的条件或需求选择并设置上下文类中的策略对象。一旦策略对象被设置,上下文类就可以执行该策略对象所代表的算法1。
5.解耦对象与算法:通过将算法封装在独立的策略对象中,对象本身不再依赖于具体的算法实现。这意味着在不修改对象代码的情况下,可以更换算法实现,从而提高了系统的灵活性和可维护性12。
6.透明地更改算法:由于客户端代码只与策略接口交互,而不直接与具体的策略实现交互,因此可以在不修改客户端代码的情况下更换策略对象,从而透明地更改对象的算法。
定义
定义一系列算法,把它们一个个封装起来,并且使它们可互相替换(变化)。该模式使得算法可独立于使用它的客户程序(稳定)而变化(扩展,子类化)。
实战重构到模式
无模式
下面是一个简单的示例,演示了如何将一个使用条件语句选择算法的类重构为策略模式。需求为数量小于10使用插入排序,否则使用快速排序
public class SortAlgo {
public void sort(List<Integer> numbers) {
if (numbers.size() < 10) {
// 使用插入排序算法
insertionSort(numbers);
} else {
// 使用快速排序算法
quickSort(numbers);
}
}
private void insertionSort(List<Integer> numbers) {
System.out.println("插入排序实现");
}
private void quickSort(List<Integer> numbers) {
System.out.println("快速排序实现");
}
}
测试类代码如下:
public class SortAlgoApp {
public static void main(String[] args) {
SortAlgo algo = new SortAlgo();
// nums中数量小于10 使用插入排序
List<Integer> nums = Arrays.asList(1,3,5,2);
algo.sort(nums);
// nums中数量大于等于10 使用快速排序
nums = Arrays.asList(1,2,5,6,7,8,9,1,1,1);
algo.sort(nums);
}
}
运行日志如下:
插入排序实现
快速排序实现
分析不用模式有何问题
上面的写法很简单,也很容易想到,但是仔细想想有很大的问题。
问题1 SortAlgo类中包好了所有的排序算法,对于排序的方法比较庞杂,难以维护。
问题2 增加新的排序算法需要修改排序这个方法,违反了设计原则中的开闭原则,系统的灵活性和扩展性很差。
问题3 算法的复用性很差,加入另一系统也需要这个排序的算法,只能通过复制代码来重用。
针对上述问题,我们应该如何解决,为什么上述短短的几行代码,会出现这么多的问题,
是因为SortAlgo的职责过重导致,首先我们把SortAlgo这个类的职责做划分,将排序的方法定义和使用分离开,这就是策略模式要解决的问题。
策略模式重构
步骤1.首先定义一个策略接口
public interface SortingStrategy {
void sort(List<Integer> numbers);
}
步骤2.创建具体的类 InsertionSortStrategy类和QuickSortStrategy类
public class InsertionSortStrategy implements SortingStrategy {
@Override
public void sort(List<Integer> numbers) {
System.out.println("插入排序的实现");
}
}
public class QuickSortStrategy implements SortingStrategy {
@Override
public void sort(List<Integer> numbers) {
System.out.println("快速排序的实现");
}
}
步骤3.创建策略上下文
public class SortingContext {
private SortingStrategy strategy;
public void setStrategy(SortingStrategy strategy) {
this.strategy = strategy;
}
public void sort(List<Integer> numbers) {
if (strategy != null) {
strategy.sort(numbers);
}
}
}
步骤4. 客户端使用策略模式
public class App {
public static void main(String[] args) {
// nums中数量小于10 使用插入排序
// List<Integer> nums = Arrays.asList(1,3,5,2);
// nums中数量大于等于10 使用快速排序
List<Integer> nums = Arrays.asList(1,2,5,6,7,8,9,1,1,1);
// 创建上下文对象
SortingContext context = new SortingContext();
// 根据需要选择策略
if (nums.size() < 10) {
context.setStrategy(new InsertionSortStrategy());
} else {
context.setStrategy(new QuickSortStrategy());
}
context.sort(nums);
}
}
运行日志如下:
快速排序的实现
在这个例子中,SortAlgo类被重构为SortingContext类,它接受一个实现了SortingStrategy接口的策略对象。
这样,客户端代码就可以根据需要设置不同的排序策略,而不需要修改SortingContext类的代码。
这种重构提高了代码的可维护性和灵活性,因为添加新的排序算法只需要创建一个新的策略类,而不需要修改现有的代码。
从上述案例中,我们对策略模式已经有个深入的认知:
1.策略模式功能
策略模式的重心不是如何来实现算法,而是如何组织、调用这些算法,从而让程序结构更灵活,具有更好的维护性和扩展性
2.算法的平等性
所有的算法在实现上都是相互之间独立的,相互之间没有依赖。所以可以这样描述一系列策略算法:策略算法是相同行为的不同实现
3.谁来选择具体的策略模式
在策略模式中,可以在两个地方来进行策略的选择;一个是客户端也就是案例1中App类,另一个是策略上下文中也可以进行策略的选择
4.策略模式实现方式
可以是接口,也可以是抽象类,如果多个算法具有公共功能的话,可以把Strategy做成抽象类
使用
优化庞大的IF-ELSE语句
们在开发过程中IF-ELSE语句是必不可少的条件判断语句,如果判断的逻辑层数只有一两层,我们还可以接受,如果嵌套的层数过多,
会对代码的可读性和可扩展性造成负面影响。那么问题来了,如何解决代码中过多的if-else语句呢。就要用到我们的主角策略模式。
【需求】支持加减的程序计算器,注意只支持加减
public int calculate(int a,int b,String operator) {
int res = 0;
if ("add".equals(operator)) {
res = a+b;
} else if ("sub".equals(operator)) {
res = a-b;
}
return res;
}
上述代码虽然能满足业务需求,但是不够优雅,我们引入策略模式进行改造
【重构】
步骤1.定义一个Operation接口,用于逻辑计算。示例代码如下
public interface Operation {
/***
* 计算
* @param a
* @param b
* @return
*/
int calcute(int a,int b);
}
步骤2.定义策略类,这里定义两个一个是加法,一个是减法
/***
* 加法逻辑
* @author qq
*
*/
public class AddOperation implements Operation{
@Override
public int calcute(int a, int b) {
return a+b;
}
}
/***
* 减法
* @author qq
*
*/
public class SubOperation implements Operation{
@Override
public int calcute(int a, int b) {
return a-b;
}
}
步骤3.定义一个工厂
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
/***
* 策略工厂类
* @author qq
*
*/
public class OperationFactory {
private static Map<String,Operation> operations = new HashMap<>();
static {
operations.put("add", new AddOperation());
operations.put("sub", new SubOperation());
}
/***
* 获取对应的策略实现类
* @param operator
* @return
*/
public static Optional<Operation> getOpe(String operator) {
return Optional.ofNullable(operations.get(operator));
}
}
步骤4.客户端使用
public class App {
public static void main(String[] args) {
Operation ope =OperationFactory.getOpe("add").orElseThrow(()->new IllegalArgumentException("参数异常"));
int rst = ope.calcute(1, 2);
System.out.println(rst);
}
}
至此策略模式改造完成,核心是使用打表法,把逻辑判断去除
【延伸】引入规则引擎,来去掉if-else判断
步骤1.首先创建一个规则引擎接口,并将条件判断方法进行抽象
public interface Rule {
/***
* 检查是否进入规则判断
* @param expression
* @return
*/
boolean evaluate(Expression expression);
/***
* 执行规则计算
* @param expression
* @return
*/
int exe(Expression expression);
}
步骤2.表达式封装类。代码示例如下
public class Expression {
private int num1;
private int num2;
/***
* 计算操作符
*/
private String ope;
public Expression(int num1,int num2,String ope) {
this.num1 = num1;
this.num2 = num2;
this.ope = ope;
}
public String getOpe() {
return ope;
}
public int getNum1() {
return num1;
}
public int getNum2() {
return num2;
}
}
步骤3.具体的规则类这里包含加法规则和减法规则
/***
* 加法规则
* @author qq
*
*/
public class AddRule implements Rule{
@Override
public boolean evaluate(Expression expression) {
return "add".equals(expression.getOpe())?true:false;
}
@Override
public int exe(Expression expression) {
int rst = expression.getNum1()+expression.getNum2();
return rst;
}
}
public class SubRule implements Rule{
@Override
public boolean evaluate(Expression expression) {
return "sub".equals(expression.getOpe())?true:false;
}
@Override
public int exe(Expression expression) {
int rst = expression.getNum1()-expression.getNum2();
return rst;
}
}
步骤4.创建规则引擎类,用于业务逻辑处理
import java.util.ArrayList;
import java.util.List;
/***
* 规则引擎,用于逻辑处理
* @author qq
*
*/
public class RuleEngine {
private static List<Rule> rules = new ArrayList<>();
static {
rules.add(new AddRule());
rules.add(new SubRule());
}
public int process(Expression expression) {
Rule rule = rules.stream().filter(r->r.evaluate(expression)).findFirst().orElseThrow(()->new IllegalArgumentException("参数异常"));
return rule.exe(expression);
}
}
步骤5.客户端使用
public class App {
public static void main(String[] args) {
Expression expression = new Expression(1,2,"sub");
RuleEngine engine = new RuleEngine();
int rst = engine.process(expression);
System.out.println(rst);
}
}
【总结】
策略模式和规则引擎,都能满足实现我们的需求,本质的区别是将条件逻辑判断参数化,由具体的实现类来判断是不是满足要求,如果满足要求就执行负责就不执行。
使用策略模式解决容错恢复机制
【引题】
日志存储的问题,一般我们的日志信息都会存放在数据库中,但是我们不能保证我们的数据库一直没出现问题,那么问题来了,
如果我们的持久化崩溃不可用了咋办,别着急,我们需要有备用的持久化方案,简单的方法就是切换到了文件,以文件的形式进行持久化,
然后数据库恢复之后进行定时的将文件持久化到数据库中,保证了日志的不丢失;而如果数据库恢复正常了又可以直接持久化到数据库中,不用繁琐的进行人工的操作
【策略模式解决】
- 先定义日志策略接口,很简单,就是一个记录日志的接口
/***
* 日志记录策略的接口
* @author qq
*
*/
public interface LogStrategy {
/***
* 记录日志
* @param msg 需要记录的日志信息
*/
public void log(String msg);
}
- 实现日志策略接口
先实现默认的数据库实现,假如日志长度过长就报错,模拟日志入库出错,代码示例如下
/***
* 把日志记录到数据库
* @author qq
*
*/
public class DbStrategy implements LogStrategy{
@Override
public void log(String msg) {
if (msg != null && msg.trim().length()>5) {
throw new RuntimeException("数据入库出现异常");
}
System.out.println("现在把"+msg+"记录到数据库中");
}
}
接下来实现日志记录到文件中,代码示例如下
/***
* 把日志记录到文件中
* @author qq
*
*/
public class FileStrategy implements LogStrategy{
@Override
public void log(String msg) {
System.out.println("现在把"+msg+"记录到文件中");
}
}
- 定义使用策略的上下文。注意这次是在上下文中实现具体策略算法的选择,所以不需要客户端来指定具体的策略算法,代码示例如下
/***
* 日志记录上下文
* @author qq
*
*/
public class LogContext {
public void log(String msg) {
// 在上下文中,自行实现对具体策略的选择
// 优先选用策略:记录到数据库
LogStrategy strategy = new DbStrategy();
try {
strategy.log(msg);
}catch (Exception e) {
// 出错了,那就记录到文件中
strategy = new FileStrategy();
strategy.log(msg);
}
}
}
注意:在这里进行具体策略算法的选择,把try-catch变相当成了if-else来使用
- 客户端没有了选择具体实现策略算法的工作,变得非常简单,故意多调用一次,可以看出不同的效果,代码示例如下
public class App {
public static void main(String[] args) {
LogContext context = new LogContext();
context.log("策略模式");
context.log("策略模式解决容错机制");
}
}
策略模式+模板模式使用场景
案例场景,比如我们在容错恢复机制策略算法中记录日志的例子,我们现在需求是要在打印日志的前面加上日志时间,也就是说现在记录日志的步骤变成了:
- 第一步为日志消息添加日志时间
- 第二步具体记录日志
那么我们该如何实现呢?示例代码如下:
步骤1记录日志的策略接口没有变化如下代码所示
/***
* 日志记录策略的接口
* @author qq
*
*/
public interface LogStrategy {
/***
* 记录日志
* @param msg 需要记录的日志信息
*/
public void log(String msg);
}
步骤2.增加一个实现策略接口的抽象类,在其中定义记录日志的算法骨架,相当于模板方法模式的模板,代码示例如下:
/***
* 实现策略模式的抽象模板,实现为消息添加时间
* @author qq
*
*/
public abstract class LogStrategyTemplate implements LogStrategy{
public final void log(String msg) {
// 第一步:为消息添加记录日志时间
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
msg = df.format(new Date())+" 内容是:"+msg;
// 第二步:真正执行日志记录
doLog(msg);
}
/***
* 真正执行日志记录,让子类去具体实现
* @param msg
*/
protected abstract void doLog(String msg);
}
步骤3.此时具体的策略实现类也不在实现LogStrategy接口,需要继承LogStrategyTemplate模板类。代码示例如下
/***
* 把日志记录到数据库
* @author qq
*
*/
public class DbStrategy extends LogStrategyTemplate{
@Override
protected void doLog(String msg) {
if (msg != null && msg.trim().length()>5) {
throw new RuntimeException("数据入库出现异常");
}
System.out.println("现在把"+msg+"记录到数据库中");
}
}
同理实现记录日志到文件类如下
/***
* 把日志记录到文件中
* @author qq
*
*/
public class FileStrategy extends LogStrategyTemplate{
@Override
protected void doLog(String msg) {
System.out.println("现在把"+msg+"记录到文件中");
}
}
步骤4.算法实现的改变不影响使用算法的上下文,上下文与之前一样,示例代码如下
/***
* 日志记录上下文
* @author qq
*
*/
public class LogContext {
public void log(String msg) {
// 在上下文中,自行实现对具体策略的选择
// 优先选用策略:记录到数据库
LogStrategy strategy = new DbStrategy();
try {
strategy.log(msg);
}catch (Exception e) {
// 出错了,那就记录到文件中
strategy = new FileStrategy();
strategy.log(msg);
}
}
}
步骤5.客户端代码还是跟以前一样,代码示例如下
public class App {
public static void main(String[] args) {
LogContext context = new LogContext();
context.log("策略模式");
context.log("策略模式解决容错机制");
}
}
花样
策略模式的新写法
【案例】
屠龙是一项危险的职业,有经验的将会使它变得简单。经验丰富的屠龙者对不同类型的龙有不同的战斗策略
【传统写法】
步骤1.首先我们定义屠龙策略模式接口和实现类
@FunctionalInterface
public interface DragonSlayingStrategy {
public void execute();
}
public class MeleeStrategy implements DragonSlayingStrategy{
@Override
public void execute() {
System.out.println("With your Excalibur you sever the dragon's head!");
}
}
public class ProjectileStrategy implements DragonSlayingStrategy{
@Override
public void execute() {
System.out.println("You shoot the dragon with the magical crossbow and it falls dead on the ground!");
}
}
public class SpellStrategy implements DragonSlayingStrategy{
@Override
public void execute() {
System.out.println("You cast the spell of disintegration and the dragon vaporizes in a pile of dust!");
}
}
步骤2.屠龙者-策略上下文
public class DragonSlayer {
private DragonSlayingStrategy strategy;
public DragonSlayer(DragonSlayingStrategy strategy) {
this.strategy = strategy;
}
public void changeStrategy(DragonSlayingStrategy strategy) {
this.strategy = strategy;
}
public void goToBattle() {
strategy.execute();
}
}
步骤3.客户端使用模板方法
public class App {
public static void main(String[] args) {
System.out.println("Green dragon spotted ahead!");
DragonSlayer dragonSlayer = new DragonSlayer(new MeleeStrategy());
dragonSlayer.goToBattle();
System.out.println("Red dragon emerges.");
dragonSlayer = new DragonSlayer(new ProjectileStrategy());
dragonSlayer.goToBattle();
System.out.println("Black dragon lands before you.");
dragonSlayer = new DragonSlayer(new SpellStrategy());
dragonSlayer.goToBattle();
}
}
【函数式写法】
public class LambdaStrategy {
public enum Strategy implements DragonSlayingStrategy {
MeleeStrategy(() -> System.out.println("With your Excalibur you severe the dragon's head!")),
ProjectileStrategy(() -> System.out.println("You shoot the dragon with the magical crossbow and it falls dead on the ground!")),
SpellStrategy(() -> System.out.println("You cast the spell of disintegration and the dragon vaporizes in a pile of dust!"));
private final DragonSlayingStrategy dragonSlayingStrategy;
Strategy(DragonSlayingStrategy dragonSlayingStrategy) {
this.dragonSlayingStrategy = dragonSlayingStrategy;
}
@Override
public void execute() {
dragonSlayingStrategy.execute();
}
}
public static void main(String[] args) {
System.out.println("Green dragon spotted ahead!");
DragonSlayer dragonSlayer = new DragonSlayer(LambdaStrategy.Strategy.MeleeStrategy);
dragonSlayer.goToBattle();
System.out.println("Red dragon emerges.");
dragonSlayer = new DragonSlayer(LambdaStrategy.Strategy.ProjectileStrategy);
dragonSlayer.goToBattle();
System.out.println("Black dragon lands before you.");
dragonSlayer = new DragonSlayer(LambdaStrategy.Strategy.SpellStrategy);
dragonSlayer.goToBattle();
}
}
本质
策略模式的本质:分离算法,选择实现
总结
Strategy及其子类为组件提供了一系列可重用的算法,从而可以使
得类型在运行时方便地根据需要在各个算法之间进行切换。
Strategy模式提供了用条件判断语句以外的另一种选择,消除条件
判断语句,就是在解耦合。含有许多条件判断语句的代码通常都需
要Strategy模式。
如果Strategy对象没有实例变量,那么各个上下文可以共享同一个
Strategy对象,从而节省对象开销。
策略模式很好的体现了开闭原则和里氏替换原则