跳到主要内容

策略模式

定义

策略模式(strategy pattern )的原始定义是: 定义一系列算法,将每一个算法封装起来,并使它们可以相互替换。策略模式让算法可以独立于使用它的客户端(使用算法的代码)而变化。

在现实生活中,常常遇到实现某种目标存在多种策略可供选择的场景,例如,出行旅游可以乘坐飞机、乘坐火车、自驾等等。

在软件开发中,经常会遇到这种情况,开发一个功能可以通过多个算法实现,我们可以将所有的算法集中在一个类中,在这个类中提供多个方法,每个方法对应一个算法, 或者我们也可以将这些算法都封装在一个统一的方法中,使用 if...else...等条件判断语句进行选择,但是这两种方法都存在硬编码的问题,后期需要增加算法就需要修改源代码,这会导致代码的维护变得困难。

比如网购,可以选择工商银行、农业银行、建设银行等等,但是它们提供的算法是一致的,就是帮你付款。

在软件开发中也会遇到相似的情况,当实现某一个功能存在多种算法或策略,我们可以根据环境或者条件的不同选择不同的算法或者策略来完成该功能。

模式原理

策略模式中,可以定义一些独立的类来封装不同的算法,每一个类都封装一种具体的算法,每一个封装算法的类都为一种策略,为了让这些策略在使用的时候具有一致性,一般会定义一个抽象策略类,作为算法的声明。

  • 抽象策略类: 一个接口
  • 具体策略类: 具体算法类
  • 上下文类:使用算法的角色
Context.java
/**
* 上下文类: 策略模式的本质就是通过 context 类作为控制单元,对不同的策略进行调度分配
*/
public class Context {

/**
* 维持抽象策略的引用
*/
private Strategy strategy;

public Context(Strategy strategy) {
this.strategy = strategy;
}

/**
* 调用策略类中的算法
*/
public void algorithm(){
strategy.algorithm();
}
}

工厂模式是解耦对象的创建和使用,观察者模式是解耦观察者和被观察者。策略模式跟两者类似,也能起到解耦的作用,它解耦的是策略的定义、创建 、使用这三部分。

策略的定义

策略类的定义比较简单,包含一个策略接口和一组实现这个接口的策略类。因为所有的策略类都实现相同的接口,所以客户端代码基于接口而非实现编程,可以灵活地替换不同的策略。

Strategy.java
/**
* 抽象策略类
*/
public interface Strategy {

void algorithm();

}

ConcreteStrategyA.java
/**
* 具体策略类
*/
public class ConcreteStrategyA implements Strategy{

@Override
public void algorithm() {
System.out.println("执行策略A");
}
}
ConcreteStrategyB.java
/**
* 具体策略类
*/
public class ConcreteStrategyB implements Strategy{

@Override
public void algorithm() {
System.out.println("执行策略B");
}
}

策略的创建

因为策略模式包含一组策略,在使用它们的时候,一般会通过类型 (type) 来判断创建那个策略来使用。为了封装创建逻辑,需要对客户端代码屏蔽创建细节。所以可以把根据 type 创建策略的逻辑抽离出来,放到工厂类中。

public class StrategyFactory {
private static final Map<String, Strategy> strategies = new HashMap<>();

static {
strategies.put("A", new ConcreteStrategyA());
strategies.put("B", new ConcreteStrategyB());
}

public static Strategy getStrategy(String type) {
if (type == null || type.isEmpty()) {
throw new IllegalArgumentException("type should not be empty.");
}
return strategies.get(type);
}
}

如果策略类是无状态的,不包含成员变量,只是纯粹的算法实现,这样的策略类是可以被共享使用的。针对这样的情况,可以使用上面这种工厂类的实现方式,实现创建好每个策略对象,直接缓存到工厂类中,用的时候直接返回。

相反,如果是策略类是有状态的,就需要用下面的方式来实现策略工厂类。

public class StrategyFactory {
public static Strategy getStrategy(String type) {
if (type == null || type.isEmpty()) {
throw new IllegalArgumentException("type should not be empty.");
}

if (type.equals("A")) {
return new ConcreteStrategyA();
} else if (type.equals("B")) {
return new ConcreteStrategyB();
}

return null;
}
}

策略的使用

策略模式包含一组可选策略,客户端代码一般如何确定使用那个策略呢,最常见的是运行时动态确定使用那种策略,这也是策略模式最经典的应用场景。

运行时动态 指的是,事先并不知道会使用那个策略,而是在程序运行期间,根据配置、用户输入、计算结果这些不确定的因素,动态决定使用那种策略。

// 策略接口:EvictionStrategy
// 策略类:LruEvictionStrategy、FifoEvictionStrategy、LfuEvictionStrategy...
// 策略工厂:EvictionStrategyFactory

public class UserCache {
private Map<String, User> cacheData = new HashMap<>();
private EvictionStrategy eviction;

public UserCache(EvictionStrategy eviction) {
this.eviction = eviction;
}

//...
}

// 运行时动态确定,根据配置文件的配置决定使用哪种策略
public class Application {
public static void main(String[] args) throws Exception {
EvictionStrategy evictionStrategy = null;
Properties props = new Properties();
props.load(new FileInputStream("./config.properties"));
String type = props.getProperty("eviction_type");
evictionStrategy = EvictionStrategyFactory.getEvictionStrategy(type);
UserCache userCache = new UserCache(evictionStrategy);
//...
}
}

// 非运行时动态确定,在代码中指定使用哪种策略
public class Application {
public static void main(String[] args) {
//...
EvictionStrategy evictionStrategy = new LruEvictionStrategy();
UserCache userCache = new UserCache(evictionStrategy);
//...
}
}

从上面的代码可以看出, 非运行时动态确定,也就是第二个 Application 中的使用方式,并不能发挥策略模式的优势。在这种应用 场景下,策略模式其实退化成了 面向对象的多态特性 或 基于接口而非实现编程。

利用策略模式避免分支判断

能够解决分支判断逻辑的模式不仅仅有策略模式,后面的状态模式也可以。对于使用那种状态,具体还要看应用场景来定。策略模式适用于根据不同类型的动态,决定使用那种策略这样一种业务场景 。

不使用策略模式的代码如下,会将策略的定义、创建、使用直接耦合在一起。

public class OrderService {
public double discount(Order order) {
double discount = 0.0;
OrderType type = order.getType();
if (type.equals(OrderType.NORMAL)) { // 普通订单
//...省略折扣计算算法代码
} else if (type.equals(OrderType.GROUPON)) { // 团购订单
//...省略折扣计算算法代码
} else if (type.equals(OrderType.PROMOTION)) { // 促销订单
//...省略折扣计算算法代码
}
return discount;
}
}

使用策略模式对上面的代码进行重构,将不同类型的订单的打折策略设计成策略类,并由工厂类来负责创建策略对象。具体的代码如下所示。

// 策略的定义
public interface DiscountStrategy {
double calDiscount(Order order);
}
// 省略NormalDiscountStrategy、GrouponDiscountStrategy、PromotionDiscountStrategy类代码...

// 策略的创建
public class DiscountStrategyFactory {
private static final Map<OrderType, DiscountStrategy> strategies = new HashMap<>();

static {
strategies.put(OrderType.NORMAL, new NormalDiscountStrategy());
strategies.put(OrderType.GROUPON, new GrouponDiscountStrategy());
strategies.put(OrderType.PROMOTION, new PromotionDiscountStrategy());
}

public static DiscountStrategy getDiscountStrategy(OrderType type) {
return strategies.get(type);
}
}

// 策略的使用
public class OrderService {
public double discount(Order order) {
OrderType type = order.getType();
DiscountStrategy discountStrategy = DiscountStrategyFactory.getDiscountStrategy(type);
return discountStrategy.calDiscount(order);
}
}

重构之后的代码就没有 if-else 分支判断语句了。实际上,这得益于策略工厂类。 在工厂类中,用 Map 来缓存策略,根据 type 直接从 Map 中获取对应的策略,从而避免 if-else 分支判断。

后面的状态模式避免分支判断逻辑的时候,使用的是同样的套路。本质上都是借助 “查表法”,根据 type 查表(strategies 就是表)替代根据 type 分支判断。

但是,如果业务场景需要每次都创建不同的策略对象,就需要用另一种工厂类的实现方式了。

public class DiscountStrategyFactory {
public static DiscountStrategy getDiscountStrategy(OrderType type) {
if (type == null) {
throw new IllegalArgumentException("Type should not be null.");
}
if (type.equals(OrderType.NORMAL)) {
return new NormalDiscountStrategy();
} else if (type.equals(OrderType.GROUPON)) {
return new GrouponDiscountStrategy();
} else if (type.equals(OrderType.PROMOTION)) {
return new PromotionDiscountStrategy();
}
return null;
}
}

这种实现方式相当于把原来的 if-else 分支逻辑,从 OrderService 类中转移到了工厂类中,实际上并没有将它真正移除(可以存储 class 对象,查表获取 class 对象,然后使用反射创建对象)。

应用实例

面试问题:如何用设计模式消除代码中的 if-else

物流行业中,通常会涉及到 EDI报文(XML格式文件)传输和回执接受,每发送一份 EDI 报文,后续都会收到与之关联的回执(标识该数据在第三方系统中的流转状态)。

这里列举几种回执类型:MT1101、MT2101、MT4101、MT8104,系统收到不同的回执报文后,会执行对应的业务逻辑处理。

不使用设计模式

总结

策略模式定义一族算法类,将每个算法分别封装起来,让它们可以互相替换。策略模式可以使算法的变化独立于使用它们的客户端(这里的客户端代指使用算法的代码)。

策略模式用来解耦策略的定义、创建、使用。实际上,一个完整的策略模式都是由这三部分组成的。

  • 策略类的定义比较简单,包含一个策略接口和一组实现这个接口的策略类。
  • 策略的创建由工厂类来完成,封装策略创建的细节。
  • 策略模式包含一组策略可选,客户端代码如何选择使用那个策略,有两种方法:编译时静态确定和运行时动态确定(策略模式最经典的应用场景)。

还可以通过策略模式移除 if-else 分支判断,这得益于工厂类,更本质上来说,是借助查表法, 根据 type 查表替代根据 type 分支判断。

如果 if-else 分支判断不复杂、代码不多,这并没有任何问题,毕竟 if-else 分支判断是几乎所有编程语言都提供的语法,存在即合理。遵循 KISS 原则,怎么简单怎么来,就是最好的设计。使用策略模式,反倒是一种过度设计。

策略模式避免 if-else 分支判断逻辑,这只是它其中的一个优点。主要的作用还是解耦策略的定义、创建、使用,控制代码的复杂度,让每个部分都不至于太过复杂、代码量过多。

除此之外,对于复杂代码来说,策略模式还能让其满足开闭原则,添加新策略的时候,最小化、集中化代码改动,减少引入 bug 的风险。

优点

  • 策略类之间 可以自由切换:由于策略类都实现同一个接口,所以它们之间可以自由切换。
  • 易于扩展:增加一个新的策略只需要添加一个具体的策略类即可,基本不需要改变原有的代码,符合开闭原则。
  • 避免使用多重条件选择语句(if else ),充分体现面向对象的设计思想。

缺点

  • 客户端必须知道所有策略类,并自行决定使用哪一个策略类。
  • 策略模式将造成很多策略类,可以通过使用享元模式在一定程度上减少对象的数量。

使用场景

  • 一个系统需要动态地在几种算法中选择一种时,并将每个算法封装到策略类中。
  • 一个类定义了多种行为,并且这些行为在这个类的操作中以多个条件语句的形式出现,可将每个条件分支移入它们各自的策略类中以替代这些条件语句。
  • 系统要求使用算法的客户不应该知道其操作的数据时,可使用策略模式来隐藏与算法相关的数据结构。

** 设计原则和思想其实比设计模式更加普适和重要,掌握了设计原则和思想,自然而然地就可以使用到设计模式,还有可能自己创建出一种新的设计模式**。