浅析设计模式2 —— 策略模式
鎏越2022-11-09

策略模式是一种应用广泛的行为型模式,本文将着眼于策略模式进行学习分享。

概述

我们在进行软件开发时要想实现可维护、可扩展,就需要尽量复用代码,并且降低代码的耦合度,而设计模式就是一种可以提高代码可复用性、可维护性、可扩展性以及可读性的解决方案。

大家熟知的23种设计模式,可以分为创建型模式、结构型模式和行为型模式三大类。其中,行为型模式可用于描述程序中多个类和多个对象如何协作完成复杂的任务,涉及不同对象间的职责分配、算法的抽象化。策略模式是一种应用广泛的行为型模式,本文将着眼于策略模式进行学习分享,如有表述不当的地方恭请大佬们指教哦~

基本概念

策略模式的核心思想是对算法进行封装,委派给不同对象来管理。这样,我们就可以定义一系列算法,将每个算法封装到具有公共接口的一系列具体策略类中,从而使它们可以灵活替换,并让算法可以在不影响到客户端的情况下发生变化。同时,策略模式仅仅封装算法(包括添加、删除),但其并不决定在何时使用何种算法,算法的选择由客户端来决定。

比如,我们旅游时可以选择的出行策略有很多种:自行车、汽车、火车、飞机,每种出行策略都有各自的使用方法,只要能到目的地,我们可以随意更换各种策略。再比如我们去逛商场,商场会有很多促销活动:满减、返利等,这些促销方式本质上都是一些算法,而算法本身也是一种策略,随时都可能互相替换的,针对同一件商品,今天满500减50、明天满300返100购物券,这些策略之间同样可以互换。

那么,我们应该如何使用策略模式呢?下面将从结构和使用步骤两个层面,对策略模式进行概念性介绍。

结构

策略模式包含三种类,分别是抽象策略类、具体策略类、环境类,它们各自负责完成特定任务,并且相互之间存在紧密的联系。

角色

关系

作用

抽象策略 Strategy

所有具体策略类的父类

定义一个公共接口,定义若干个算法标识和抽象方法

具体策略 Concrete Strategy

抽象策略的接口实现类

实现抽象策略定义的抽象方法,描述具体的算法实现

环境 Context

维护一个抽象策略类的引用实例

委托策略变量,调用具体策略所实现的抽象策略接口中的方法

使用

有了上述的基本概念,我们将策略模式的使用步骤概括为:

  1. step1:创建抽象策略类,为具体策略定义好一个公共接口;
  2. step2:创建具体策略类,其通过接口来实现抽象策略类,同时封装了具体的算法;
  3. step3:创建环境类,持有一个抽象策略类的引用,提供给客户端调用。

使用示例

淘宝用户都知道,除了双11购物狂欢节,平台每年都会打造很多其他的促销活动。试想一下,如果每种大促活动都使用一种促销模式,未免太过枯燥,于用户、商家、平台而言都不友好。因此,为了提升用户购买体验、突出商家营销特点,需要面向不同大促活动使用不同的策略进行促销。这里以促销策略为例,简单分析策略模式如何使用:

代码实现

//step1:定义抽象策略角色(Strategy):所有促销活动的共同接口
public interface Strategy {  
    void show();
}

//step2:定义具体策略角色(Concrete Strategy):不同类型的具体促销策略
//618大促活动 A
public class ConcreteStrategyA implements Strategy{
    @Override
    public void show() {
        System.out.println("618大促");
    }
}

//99大促活动 B
public class ConcreteStrategyB implements Strategy{
    @Override
    public void show() {
        System.out.println("99大促");
    }
}

//双11大促活动 C
public class ConcreteStrategyC implements Strategy{
    @Override
    public void show() {
        System.out.println("双11大促");
    }
}

//step3:定义环境角色(Context):把促销活动推送给用户,这里可理解为淘宝平台
public class Context_TaoPlatform{
    //持有抽象策略的引用
    private Strategy myStrategy;
    //生成构造方法,让平台根据传入的策略参数选择策略
    public TaoPlatform(Strategy strategyType) {
        this.myStrategy = strategyType;
    }
    //向用户展示促销活动
    public void taoPlatformShow(String time) {
        System.out.println(time + "的促销策略是:");
        myStrategy.show();
    }
}

//step4:客户端调用
public class StrategyPattern{
  public static void main(String[] args){
        Context_TaoPlatform context;
    
        String time1 = "9月";
        Strategy strategyB = new ConcreteStrategyB();
        context = new Context_TaoPlatform(strategyB);
        context.taoPlatformShow(time1);
    
        String time2 = "11月";
        Strategy strategyC = new ConcreteStrategyC();
        context = new Context_TaoPlatform(strategyC);
        context.taoPlatformShow(time2);
    
        String time3 = "6月";
        Strategy strategyA = new ConcreteStrategyA();
        context = new Context_TaoPlatform(strategyA);
        context.taoPlatformShow(time3);
  }   
}

结果输出

9月的促销策略是:
99大促
11月的促销策略是:
双11大促
6月的促销策略是:
618大促

UML图

与简单工厂模式的区别

从上面的代码示例及类图可以看出来,策略模式和上一篇文章中介绍的简单工厂模式很像,两者主要区别在于 Context 类和工厂类。为了方便对比,我们把这两个类的代码单独拎出来看看:

public class Context_TaoPlatform{
    //持有抽象策略的引用
    private Strategy myStrategy;
    //生成构造方法,让平台根据传入的策略参数选择促销活动
    public TaoPlatform(Strategy strategyType) {
        this.myStrategy = strategyType;
    }
    //向用户展示促销活动
    public void taoPlatformShow(String time) {
        System.out.println(time + "的促销策略是:");
        myStrategy.show();
    }
}
public class Factory{
    public static Shirt exhibit(String ShirtName){
        switch(ShirtName){
            case "女款衬衫":
                return new WomenShirt();
            case "男款衬衫":
                return new MenShirt();
            default:
                return null;
        }
    }
}

首先看一下接收参数:工厂类 Factory 中的 exhibit() 方法接收字符串,返回一个 Shirt 对象;环境类 Context_TaoPlatform 初始化时需要接收一个 Strategy 对象。也就是说:工厂类中是根据接收的条件创建一个相应的对象,而 Context 类接收的是一个对象,可以调用方法去执行此对象的方法。

举个例子:笔有很多种,假设有一个工厂专门负责生产不同用途的笔。

  1. 工厂模式:根据用户给出的目的来生产不同用途的笔,如:要写毛笔字就生产毛笔、要写钢笔字就生产钢笔。即根据用户给出的某种属性,生产能做出相应行为的一种对象返回给用户,重点在于创建何种对象。
  2. 策略模式:用工厂生产的笔去出做对应的行为,如:用毛笔写毛笔字、用钢笔写钢笔字。即根据用户给出的某种对象,执行相应的方法,重点在于选择何种行为。

JDK源码赏析

这里以 Comparator 比较器为例,通过分析其源码实现来深入理策略模式。

在 JDK 中,我们调用数组工具类 Arrays 的一个排序方法 sort() 时,可以使用默认的排序规则(升序),也可以自定义一种排序的规则,即自定义实现升序或降序的排序。源码如下:

public class Arrays{
    public static <T> void sort(T[] a, Comparator<? super T> c) {
        if (c == null) {
            //若没有传入Comparator接口的实现类对象,调用默认的升序排序方法
            sort(a);
        } else {
            if (LegacyMergeSort.userRequested)
                //jdk5及之前的传统归并排序,新版本中LegacyMergeSort.userRequested默认false
                legacyMergeSort(a, c);
            else
                //改进后的归并排序
                TimSort.sort(a, 0, a.length, c, null, 0, 0);
        }
    }
}

此时我们需要传入两个参数:一个是待排序的数组,另一个则是 Comparator 接口的实现类对象。其中,Comparator 接口是一种函数式接口,该接口中定义了一个抽象方法 int compare(T o1, T o2),用于定义具体的排序规则。这里,Comparator 接口就是策略模式中的抽象策略接口,它定义了一个排序算法,而具体策略(具体的排序算法)将由用户来定义,那么Arrays 就是一个环境类,sort() 方法可以传入一个策略 c ,让 Arrays 根据这个策略进行排序任务。

public class demo {
    public static void main(String[] args) {

        Integer[] data = {2, 0, 22, 14, 1, 3, 4};
        // 实现降序排序
        Arrays.sort(data, new Comparator<Integer>() {
             // 排序策略 降序
            public int compare(Integer o1, Integer o2) {
                return o2 - o1;
            }
        });
        System.out.println(Arrays.toString(data)); 
    }
}

在上面这个 demo 中,我们在调用 Arrays.sort() 方法时,第二个参数传递的是 Comparator 接口的子实现类对象。由此可见,Comparator 充当的是抽象策略角色,而具体的子实现类充当的是具体策略角色,环境角色类 Arrays 应该持有抽象策略的引用来调用。那么,Arrays.sort() 方法究竟有没有使用 Comparator 子实现类中的 compare() 方法?下面再看看 TimSort.sort() 方法,源码如下:

class TimSort<T> {
    static <T> void sort(T[] a, int lo, int hi, Comparator<? super T> c,
                         T[] work, int workBase, int workLen) {
        assert c != null && a != null && lo >= 0 && lo <= hi && hi <= a.length;

        int nRemaining  = hi - lo;
        if (nRemaining < 2)
            return;  

        if (nRemaining < MIN_MERGE) {
            int initRunLen = countRunAndMakeAscending(a, lo, hi, c);
            binarySort(a, lo, hi, lo + initRunLen, c);
            return;
        }
        ...
    }   
        
    private static <T> int countRunAndMakeAscending(T[] a, int lo, int hi,Comparator<? super T> c) {
        assert lo < hi;
        int runHi = lo + 1;
        if (runHi == hi)
            return 1;

        if (c.compare(a[runHi++], a[lo]) < 0) { 
            while (runHi < hi && c.compare(a[runHi], a[runHi - 1]) < 0)
                runHi++;
            reverseRange(a, lo, runHi);
        } else {                              
            while (runHi < hi && c.compare(a[runHi], a[runHi - 1]) >= 0)
                runHi++;
        }
        return runHi - lo;
    }
}

上面的代码最后会执行到 countRunAndMakeAscending() 方法中,在执行判断语句时调用了 compare() 方法。那么如果只用了 compare() 方法,在调用 Arrays.sort() 方法时只要传具体 compare() 重写方法的类对象。

优缺点及适用场景

优点

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

缺点

  1. 客户端必须知道所有的具体策略类,并理解不同具体策略的区别、自行决定使用哪一个策略类。
  2. 策略模式将产生很多具体策略类,在一定程度上增加了系统中类的个数(可通过使用享元模式在一定程度上减少对象数量)。

适用场景

  1. 一个系统需要动态地在几种算法中选择一种时,可将每个算法封装到具体策略类中。
  2. 一个类定义了多种行为,并且这些行为在这个类的操作中以多个条件语句的形式出现,可将每个条件分支移入它们各自的策略类中以代替这些条件语句,就能避免使用难以维护的多重条件选择语句,并体现面向对象涉及的概念。
  3. 系统中各算法彼此完全独立,且要求对客户隐藏具体算法的实现细节,提高算法的保密性与安全性。
  4. 多个类只区别在表现行为不同,可以使用策略模式,在运行时动态选择具体要执行的行为。

总结

策略模式是一个比较容易理解和使用的设计模式,它仅封装算法,方便新算法插入系统中、老算法从系统中退休。本文在分析策略模式的缺点时提到,策略模式并不决定在何时使用何种算法,算法选择由客户端来决定,虽然这在一定程度上提高了系统的灵活性,但客户端需要理解所有具体策略类之间的区别,以便选择合适的算法,增加了客户端的使用难度。

上一篇文章提到,策略模式和工厂模式有一定相似之处,在于它们的模式结构,因此有时候会让人混淆不清。实际上,这两者之间存在较多差异:工厂模式是创建型模式,作用是创建对象,它关注对象如何创建,主要解决的是资源的统一分发,将对象的创建完全独立出来,让对象的创建和具体的使用客户无关;策略模式是行为型模式,作用是让一个对象在许多行为中选择一种行为,它关注行为如何封装,通过定义策略族来实现策略的灵活切换与扩展,并让策略的变化独立于使用策略的客户。

另外,很多场景下策略模式和工厂模式可以结合使用,共同发挥优势起到相辅相成的作用。比如,策略模式的缺点之一是用户必须清楚所有的具体策略算法,这样具体策略难免暴露出去,并且要由上层模块初始化,这与迪米特法则相悖(最少知识原则),而上层模块和底层模之间的解耦,可以让工厂模式来完成。两者结合之后,对于上层模块而言不需要知道每种具体策略,只要通过 Context 就可以实现策略模式。(至于如何结合策略模式和工厂模式,大家可以上网搜索哦,已经有很多大佬给出了具体的案例和代码示例,这里就不再赘述了)