策略模式

239 阅读7分钟

1. 策略模式的定义

策略模式,英文全称是 Strategy Design Pattern。在 GoF 的《设计模式》一书中,它是这样定义的:

Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it.

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

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

2 一个Demo

一个完整的策略模式应该包含的这三个部分。

1. 策略的定义

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

public interface Strategy {
  void algorithmInterface();
}
public class ConcreteStrategyA implements Strategy {
  @Override
  public void  algorithmInterface() {
    //具体的算法...
  }
}
public class ConcreteStrategyB implements Strategy {
  @Override
  public void  algorithmInterface() {
    //具体的算法...
  }
}

2. 策略的创建

因为策略模式会包含一组策略,在使用它们的时候,一般会通过类型(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);
  }
}

一般来讲,如果策略类是无状态的,不包含成员变量,只是纯粹的算法实现,这样的策略对象是可以被共享使用的,不需要在每次调用 getStrategy() 的时候,都创建一个新的策略对象。针对这种情况,我们可以使用上面这种工厂类的实现方式,事先创建好每个策略对象,缓存到工厂类中,用的时候直接返回。

相反,如果策略类是有状态的,根据业务场景的需要,我们希望每次从工厂方法中,获得的都是新创建的策略对象,而不是缓存好可共享的策略对象,那我们就需要按照如下方式来实现策略工厂类。

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;
  }
}

3. 策略的使用

刚刚讲了策略的定义和创建,现在,我们再来看一下,策略的使用。

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

这里的“运行时动态”指的是,我们事先并不知道会使用哪个策略,而是在程序运行期间,根据配置、用户输入、计算结果等这些不确定因素,动态决定使用哪种策略。接下来,我们通过一个例子来解释一下。

// 策略接口: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 中的使用方式,并不能发挥策略模式的优势。在这种应用场景下,策略模式实际上退化成了“面向对象的多态特性”或“基于接口而非实现编程原则”。

3 如何利用策略模式避免分支判断?

实际上,能够移除分支判断逻辑的模式不仅仅有策略模式,状态模式也可以。对于使用哪种模式,具体还要看应用场景来定。 策略模式适用于根据不同类型待动态,决定使用哪种策略这样一种应用场景。

我们先通过一个例子来看下,if-else 或 switch-case 分支判断逻辑是如何产生的。具体的代码如下所示。在这个例子中,我们没有使用策略模式,而是将策略的定义、创建、使用直接耦合在一起。

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;
  }
}

3.1 无状态策略实现方式

如何来移除掉分支判断逻辑呢?那策略模式就派上用场了。我们使用策略模式对上面的代码重构,将不同类型订单的打折策略设计成策略类,并由工厂类来负责创建策略对象。具体的代码如下所示:

// 策略的定义
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 分支判断。

3.2 有状态策略实现方式

但是,如果业务场景需要每次都创建不同的策略对象,我们就要用另外一种工厂类的实现方式了。具体的代码如下所示:

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 类中转移到了工厂类中,实际上并没有真正将它移除。

其实这里的 if-else 逻辑分支不多、也不复杂,这样写完全没问题。

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

// 策略的定义
interface DiscountStrategy {
    double calDiscount(Order order);
}


class NormalDiscountStrategy implements DiscountStrategy {
    @Override
    public double calDiscount(Order order) {
        return 0;
    }
}

class GrouponDiscountStrategy implements DiscountStrategy {
    @Override
    public double calDiscount(Order order) {
        return 0;
    }
}

class PromotionDiscountStrategy implements DiscountStrategy {
    @Override
    public double calDiscount(Order order) {
        return 0;
    }
}


// 策略的创建 放在工厂方法中
class DiscountStrategyFactory {

    private static final Map<OrderType, Class> strategies = new HashMap<>();

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

    public static DiscountStrategy getDiscountStrategy(OrderType type) throws InstantiationException, IllegalAccessException {
        if (type == null) {
            throw new IllegalArgumentException("Type should not be null.");
        }

        Class aClass = strategies.get(type);
        if (Objects.isNull(aClass)) {
            return null;
        }
        return (DiscountStrategy) aClass.newInstance();
    }
}


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


class Order {
    OrderType orderType;

    public OrderType getType() {
        return orderType;
    }
}

enum OrderType {
    GROUPON, PROMOTION, NORMAL

}

实际上,也是基于查表法来解决的,其中的“strategies”就是“表”

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

一提到策略模式,有人就觉得,它的作用是避免 if-else 分支判断逻辑。实际上,这种认识是很片面的。策略模式主要的作用还是解耦策略的定义、创建和使用,控制代码的复杂度,让每个部分都不至于过于复杂、代码量过多。除此之外,对于复杂代码来说,策略模式还能让其满足开闭原则,添加新策略的时候,最小化、集中化代码改动,减少引入 bug 的风险。

实际上,设计原则和思想比设计模式更加普适和重要。掌握了代码的设计原则和思想,我们能更清楚的了解,为什么要用某种设计模式,就能更恰到好处地应用设计模式。

4 现实中生活demo

货币转换

图片

public class CurrencyConversion {

    public BigDecimal CNYCurrencyConversion(BigDecimal num, String moneyType) {

        BigDecimal result = new BigDecimal(0);
        // 人民币转美元
        if ("CNYToUSD".equalsIgnoreCase(moneyType)) {

            result = num.multiply(new BigDecimal(0.1421)).setScale(3, BigDecimal.ROUND_HALF_UP);

            // 人民币转日元
        } else if ("CNYToJPY".equalsIgnoreCase(moneyType)) {
            result = num.multiply(new BigDecimal(15.4342));
            // 人民币转欧元
        } else if ("CNYToEUR".equalsIgnoreCase(moneyType)) {
            result = num.multiply(new BigDecimal(0.1289));
        }
        return result;
    }
}
public class CurrencyConversionMain {
    public static void main(String[] args) {


        String CNYToUSD = "CNYToUSD";
        CurrencyConversion b = new CurrencyConversion();
        BigDecimal bigDecimal = b.CNYCurrencyConversion(BigDecimal.ONE, CNYToUSD);
        System.out.println(bigDecimal.toString());
    }
}
问题
  1. 代码很多且复杂,不利于维护和扩展

  2. 违反了"开闭原则",增加新的出行方式必须修改源码

  3. 复用性差,无法单独重用其中的某个或某些算法

4.2 使用策略模式进行重构

4.2.1 策略定义

import java.math.BigDecimal;

public interface Conversion {

    BigDecimal CNYCurrencyConversion(BigDecimal num);
}

4.2.2 具体的策略实现:

public class CNYToEURConversionImpl implements Conversion {
    @Override
    public BigDecimal CNYCurrencyConversion(BigDecimal num) {
        BigDecimal result = ConversionUtil.getResult(num, new BigDecimal(0.1289));
        return result;
    }
}
public class CNYToJPYConversionImpl implements Conversion {
    @Override
    public BigDecimal CNYCurrencyConversion(BigDecimal num) {
        BigDecimal result = ConversionUtil.getResult(num, new BigDecimal(15.4342));
        return result;
    }
}
public class CNYToUSDConversionImpl implements Conversion {
    @Override
    public BigDecimal CNYCurrencyConversion(BigDecimal num) {
        BigDecimal result = ConversionUtil.getResult(num, new BigDecimal(0.1421));
        return result;
    }
}
public class ConversionUtil {

    public static BigDecimal getResult(BigDecimal num1, BigDecimal num2){
        BigDecimal result = num1.multiply(num2).setScale(3, BigDecimal.ROUND_HALF_UP);
        return result;
    }
}

策略上下文:负责策略的创建

public class ConversionContext {
    private Conversion conversion;

    // 使用构造器注入具体的策略类
    public ConversionContext(Conversion conversion) {
        this.conversion = conversion;
    }

    public BigDecimal contextMethod(BigDecimal num) {
        // 调用策略实现的方法
       return conversion.CNYCurrencyConversion(num);
    }
}

外部客户端:

public class Client {
    public static void main(String[] args) {
        //1.创建人民转换美元策略
        CNYToUSDConversionImpl  CNYToUSDConversion = new CNYToUSDConversionImpl();
        // 2.创建转换上下文对象,并设置具体的报价策略
        ConversionContext conversionContext = new ConversionContext(CNYToUSDConversion);
        //3.调用转换上下文的方法
        BigDecimal result = conversionContext.contextMethod(new BigDecimal(100));
        System.out.println(result);
    }
}

5 策略模式在线程池中的应用:

在多线程编程中,我们经常使用线程池来管理线程,以减缓线程频繁的创建和销毁带来的资源的浪费,在创建线程池的时候,经常使用一个工厂类来创建线程池Executors,实际上Executors的内部使用的是类ThreadPoolExecutor.它有一个最终的构造函数如下:

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }
  • corePoolSize:线程池中的核心线程数量,即使这些线程没有任务干,也不会将其销毁。
  • maximumPoolSize:线程池中的最多能够创建的线程数量。
  • keepAliveTime:当线程池中的线程数量大于corePoolSize时,多余的线程等待新任务的最长时间。
  • unit:keepAliveTime的时间单位。
  • workQueue:在线程池中的线程还没有还得及执行任务之前,保存任务的队列(当线程池中的线程都有任务在执行的时候,仍然有任务不断的提交过来,这些任务保存在workQueue队列中)。
  • threadFactory:创建线程池中线程的工厂。
  • handler:当线程池中没有多余的线程来执行任务,并且保存任务的多列也满了(指的是有界队列),对仍在提交给线程池的任务的处理策略。

我们在日常使用中的API,会默认TreadFactory和handler

private static final RejectedExecutionHandler defaultHandler =
    new AbortPolicy();

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), defaultHandler);
}


public static class AbortPolicy implements RejectedExecutionHandler {
    //...
}


可以看到默认使用的处理策略为AbortPolicy。AbortPolicy实现了RejectedExecutionHandler接口.

RejectedExecutionHandler 是一个策略接口,用在当线程池中没有多余的线程来执行任务,并且保存任务的多列也满了(指的是有界队列),对仍在提交给线程池的任务的处理策略。

public interface RejectedExecutionHandler {
 
    /**
     *当ThreadPoolExecutor的execut方法调用时,并且ThreadPoolExecutor不能接受一个任务Task时,
     * 该方法就有可能被调用。
   * 不能接受一个任务Task的原因:有可能是没有多余的线程来处理,有可能是workqueue队列中没有多余的
     * 位置来存放该任务,该方法有可能抛出一个未受检的异常RejectedExecutionException
     * @param r the runnable task requested to be executed
     * @param executor the executor attempting to execute this task
     * @throws RejectedExecutionException if there is no remedy
     */
    void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}

该策略接口有四个实现类:

AbortPolicy:该策略是直接将提交的任务抛弃掉,并抛出RejectedExecutionException异常。

/**
     * A handler for rejected tasks that throws a
     * <tt>RejectedExecutionException</tt>.
     */
    public static class AbortPolicy implements RejectedExecutionHandler {
        /**
         * Creates an <tt>AbortPolicy</tt>.
         */
        public AbortPolicy() { }
 
        /**
         * Always throws RejectedExecutionException.
         * @param r the runnable task requested to be executed
         * @param e the executor attempting to execute this task
         * @throws RejectedExecutionException always.
         */
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            throw new RejectedExecutionException();
        }
    }
    

DiscardPolicy:该策略也是将任务抛弃掉(对于提交的任务不管不问,什么也不做),不过并不抛出异常。

/**
     * A handler for rejected tasks that silently discards the
     * rejected task.
     */
    public static class DiscardPolicy implements RejectedExecutionHandler {
        /**
         * Creates a <tt>DiscardPolicy</tt>.
         */
        public DiscardPolicy() { }
 
        /**
         * Does nothing, which has the effect of discarding task r.
         * @param r the runnable task requested to be executed
         * @param e the executor attempting to execute this task
         */
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        }
    }
    

DiscardOldestPolicy:该策略是当执行器未关闭时,从任务队列workQueue中取出第一个任务,并抛弃这第一个任务,进而有空间存储刚刚提交的任务。使用该策略要特别小心,因为它会直接抛弃之前的任务。

/**
     * A handler for rejected tasks that discards the oldest unhandled
     * request and then retries <tt>execute</tt>, unless the executor
     * is shut down, in which case the task is discarded.
     */
    public static class DiscardOldestPolicy implements RejectedExecutionHandler {
        /**
         * Creates a <tt>DiscardOldestPolicy</tt> for the given executor.
         */
        public DiscardOldestPolicy() { }
 
        /**
         * Obtains and ignores the next task that the executor
         * would otherwise execute, if one is immediately available,
         * and then retries execution of task r, unless the executor
         * is shut down, in which case task r is instead discarded.
         * @param r the runnable task requested to be executed
         * @param e the executor attempting to execute this task
         */
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (!e.isShutdown()) {
                e.getQueue().poll();
                e.execute(r);
            }
        }
    }

CallerRunsPolicy:该策略并没有抛弃任何的任务,由于线程池中已经没有了多余的线程来分配该任务,该策略是在当前线程(调用者线程)中直接执行该任务。

/**
     * A handler for rejected tasks that runs the rejected task
     * directly in the calling thread of the {@code execute} method,
     * unless the executor has been shut down, in which case the task
     * is discarded.
     */
    public static class CallerRunsPolicy implements RejectedExecutionHandler {
        /**
         * Creates a {@code CallerRunsPolicy}.
         */
        public CallerRunsPolicy() { }
 
        /**
         * Executes task r in the caller's thread, unless the executor
         * has been shut down, in which case the task is discarded.
         *
         * @param r the runnable task requested to be executed
         * @param e the executor attempting to execute this task
         */
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (!e.isShutdown()) {
                r.run();
            }
        }
    }
    

类ThreadPoolExecutor中持有一个RejectedExecutionHandler接口的引用,以便在构造函数中可以由外部客户端自己制定具体的策略并注入。下面看一下其类图:

image.png

6 策略模式的总结

6.1 策略模式的本质

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

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

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

除此之外,我们还可以通过策略模式来移除 if-else 分支判断。实际上,这得益于策略工厂类,更本质上点讲,是借助“查表法”,根据 type 查表替代根据 type 分支判断。

策略模式的作用:就是把具体的算法实现(业务逻辑的实现)从业务逻辑中剥离出来,成为一系列独立类,使得它们可以相互替换。

6.2 策略模式的优缺点

策略模式的优点:

  1. 策略模式的功能就是通过抽象、封装来定义一系列的算法,使得这些算法可以相互替换,所以为这些算法定义一个公共的接口,以约束这些算法的功能实现。如果这些算法具有公共的功能,可以将接口变为抽象类,将公共功能放到抽象父类里面。
  2. 策略模式的一系列算法是可以相互替换的、是平等的,写在一起就是if-else组织结构,如果算法实现里又有条件语句,就构成了多重条件语句,可以用策略模式,避免这样的多重条件语句。
  3. 扩展性更好:在策略模式中扩展策略实现非常的容易,只要新增一个策略实现类,然后在使用策略实现的地方,使用这个新的策略实现就好了。

策略模式的缺点:

  1. 客户端必须了解所有的策略,清楚它们的不同:

如果由客户端来决定使用何种算法,那客户端必须知道所有的策略,清楚各个策略的功能和不同,这样才能做出正确的选择,但是这暴露了策略的具体实现。

  1. 增加了对象的数量:

由于策略模式将每个具体的算法都单独封装为一个策略类,如果可选的策略有很多的话,那对象的数量也会很多。

  1. 只适合偏平的算法结构:

由于策略模式的各个策略实现是平等的关系(可相互替换),实际上就构成了一个扁平的算法结构。即一个策略接口下面有多个平等的策略实现(多个策略实现是兄弟关系),并且运行时只能有一个算法被使用。这就限制了算法的使用层级,且不能被嵌套。