从零开始学习Java设计模式 | 行为型模式篇:策略模式

Posted 李阿昀

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从零开始学习Java设计模式 | 行为型模式篇:策略模式相关的知识,希望对你有一定的参考价值。

在本讲,我们来学习一下行为型模式里面的第二个设计模式,即策略模式。

概述

先看下面的图片,我们去旅游选择出行方式能有很多种,可以骑自行车、可以坐汽车、可以坐火车、可以坐飞机。

作为一个程序猿,开发需要选择一款开发工具,当然可以进行代码开发的工具有很多,既可以选择IDEA进行开发,也可以使用Eclipse进行开发,也可以使用其他的一些开发工具。

从第一张图可以看到,不管我们选择哪种交通方式,最终都是要到达目的地。从第二张图可以看到,不管程序猿使用哪个开发工具,他最终的目的就是开发出来一款软件。注意,这两个例子其实描述的就是策略模式。

那么什么是策略模式呢?接下来,我们就来看一看策略模式的概念。

策略模式定义了一系列算法,并将每个算法封装起来(你可以理解成IDEA就是一个算法,Eclipse也是一个算法,这些算法都是用来进行代码开发的,是不是啊!),使它们可以相互替换(也就是说既可以用IDEA进行开发,也可以用Eclipse进行开发),且算法的变化不会影响使用算法的客户(你可以把程序猿理解成就是客户,客户用IDEA进行代码开发或者用Eclipse进行代码开发,最终的目的都是一样的,所以算法的变化并不影响使用算法的客户)。策略模式属于对象行为模式,它通过对算法进行封装,把使用算法的责任和算法的实现分割开来,并委派给不同的对象对这些算法进行管理。当然,策略模式里面有一个角色,该角色就是专门对这些算法进行管理的,也就是说我们可以通过该角色去选择使用哪个算法来实现我们所想要达到的一个目的。

结构

策略模式的主要角色如下:

  • 抽象策略(Strategy)类:这是一个抽象角色,通常由一个接口或抽象类实现。此角色给出所有的具体策略类所需的接口。

    你想啊,IDEA底层的算法和Eclipse底层的算法是不一样的,但是咱们可以向上抽取啊,抽取出来一个抽象方法,这样就起到了一个规范的作用,用的时候我们不必管底层用的是什么算法,因为咱们最终的目的就是进行代码开发。

  • 具体策略(Concrete Strategy)类:实现了抽象策略定义的接口,提供具体的算法实现或行为。

  • 环境(Context)类:持有一个策略类的引用,最终给客户端调用。

    注意了,这里说的环境类就是对算法对象进行管理的角色。

策略模式案例

接下来,我们便通过一个案例来让大家再去理解一下策略模式,这个案例就是促销活动。

分析

话说有一家百货公司在定年度的促销活动,针对不同的节日(春节、中秋节、圣诞节)推出不同的促销活动,由促销员将促销活动展示给客户。

下面我们就来分析一下该案例应该如何去实现。

针对于不同的节日推出不同的促销活动,那么不同的促销活动就是不同的算法,如果现有一个促销活动是买一送一,那么百货公司既可以将其应用在春节,也可以将其应用在中秋节,还可以将其应用在圣诞节;如果还有一个促销活动,即满200减50,那么也是同样的道理,百货公司也可以将其应用在春节、中秋节或者圣诞节。你会发现,这些促销活动的算法是可以相互替换的。

明确了这点之后,下面我们再来看一下下面这张类图。

可以看到,在顶部我们定义了一个策略接口,当然它是属于抽象策略类角色,而且它里面定义了一个show方法,该方法就是用于展示促销活动的内容的。此外,该策略接口下面还有三个子实现类,而每一个子实现类就是具体算法的封装类,也就是具体策略类,当然它们都得要求去重写父接口中的show方法以便进行促销活动内容的一个展示。

注意了,以上类图的左侧还有一个促销员类,它在这儿充当的就是环境类角色。在该类里面,声明了一个策略接口的变量,该变量就是用来管理具体的策略算法的。此外,我们还为该类提供了一个有参构造,以便为策略接口对象进行赋值,当然了,在该类里面,你还可以为声明的策略接口变量提供对应的getter和setter方法,只不过在以上类图中我没有体现出来而已。最后,该类里面还有一个叫salesManShow的方法,它是由促销员给客户去展示促销活动内容的。

以上类图分析完了之后,接下来我们就要编写代码实现以上案例了。

实现

首先,打开咱们的maven工程,并在com.meimeixia.pattern包下新建一个子包,即strategy,也即策略模式的具体代码我们是放在了该包下。

然后,创建策略接口,即百货公司所有促销活动的共同接口。

package com.meimeixia.pattern.strategy;

/**
 * 抽象策略类
 * @author liayun
 * @create 2021-08-02 20:07
 */
public interface Strategy {
    void show();
}

接着,创建具体的策略算法类。这里我们先创建一个StrategyA类,它表示的是买一送一的促销活动。

package com.meimeixia.pattern.strategy;

/**
 * 具体策略类,封装算法
 * @author liayun
 * @create 2021-08-02 20:10
 */
public class StrategyA implements Strategy {
    @Override
    public void show() {
        System.out.println("买一送一");
    }
}

再创建一个StrategyB类,它表示的是满200元减50元的促销活动。

package com.meimeixia.pattern.strategy;

/**
 * 具体策略类,封装算法
 * @author liayun
 * @create 2021-08-02 20:10
 */
public class StrategyB implements Strategy {
    @Override
    public void show() {
        System.out.println("满200元减50元");
    }
}

再再创建一个StrategyC类,它表示的是满1000元加1元换购任意200元以下商品的促销活动。

package com.meimeixia.pattern.strategy;

/**
 * 具体策略类,封装算法
 * @author liayun
 * @create 2021-08-02 20:10
 */
public class StrategyC implements Strategy {
    @Override
    public void show() {
        System.out.println("满1000元加一元换购任意200元以下商品");
    }
}

紧接着,创建促销员类,它对应策略模式里面的环境类角色。环境类是用于连接上下文的,在该案例中,它是用于把促销活动推销给客户的,所以这里可以理解为促销员(或者销售员)。

package com.meimeixia.pattern.strategy;

/**
 * 促销员类(环境类)
 * @author liayun
 * @create 2021-08-02 20:20
 */
public class SalesMan {

    // 聚合策略接口对象
    private Strategy strategy;

    // 通过以下有参构造为上面定义的成员变量赋值(即设置具体策略类对象)
    public SalesMan(Strategy strategy) {
        this.strategy = strategy;
    }

    public Strategy getStrategy() {
        return strategy;
    }

    public void setStrategy(Strategy strategy) {
        this.strategy = strategy;
    }

    // 由促销员展示促销活动给用户
    public void salesManShow() {
        strategy.show();
    }

}

最后,创建一个客户端类进行测试。

package com.meimeixia.pattern.strategy;

/**
 * @author liayun
 * @create 2021-08-02 20:26
 */
public class Client {
    public static void main(String[] args) {
        // 春节来了,使用春节促销活动
        // 创建促销员类对象,注意,在创建的时候必须传递具体的促销活动对象
        SalesMan salesMan = new SalesMan(new StrategyA());
        // 促销员开始展示促销活动
        salesMan.salesManShow();

        System.out.println("=========================");
        // 中秋节到了,使用中秋节的促销活动
        salesMan.setStrategy(new StrategyB());
        // 促销员开始展示促销活动
        salesMan.salesManShow();

        System.out.println("=========================");
        // 圣诞节到了,使用圣诞节的促销活动
        salesMan.setStrategy(new StrategyC());
        // 促销员开始展示促销活动
        salesMan.salesManShow();
    }
}

此时,运行以上客户端类,打印结果如下图所示,可以看到确实都展示出了相应的促销活动。

以上就是策略模式的案例,大家通过这个案例再好好的去理解一下策略模式。

策略模式的优缺点以及使用场景

接下来,我们就来看看策略模式的优缺点以及使用场景。

优缺点

优点

关于策略模式的优点,我总结出来了下面三个。

  1. 策略类之间可以自由切换。

    由于策略类都实现同一个接口(或者继承同一个抽象类),所以使它们之间可以自由切换。记住,策略类本身封装的就是算法,多个策略类,那么封装的就是多个算法,而算法和算法之间是可以相互替换的。

  2. 易于扩展。

    增加一个新的策略只需要添加一个具体的策略类即可,基本不需要改变原有的代码,符合"开闭原则"。

  3. 避免使用多重条件选择语句(例如if else),充分体现面向对象设计思想。

    对于策略模式来说的话,我们是要在多个算法之间进行一个选择的,而现在进行选择的话,就不需要使用if else语句了,而是使用策略模式来进行选择。

缺点

关于策略模式的优点,我总结出来了下面两个。

  1. 客户端必须知道所有的策略类,并自行决定使用哪一个策略类。

  2. 策略模式将造成产生很多策略类,可以通过使用享元模式在一定程度上减少对象的数量。

    使用策略模式虽然会产生很多策略类,但是对于同一个策略类来说,它里面的算法是一样的,那么我们就没有必要去创建多个策略类对象了,所以此时我们就可以使用享元模式来减少对象的数量。当然,这块就是策略模式和享元模式的混合使用了。

使用场景

只要出现如下几个场景,我们就可以去考虑一下能不能使用策略模式了。

  • 一个系统需要动态地在几种算法中选择一种时,可将每个算法封装到策略类中,因为算法之间是可以相互替换的。

  • 一个类定义了多种行为,并且这些行为在这个类的操作中以多个条件语句的形式出现,可将每个条件分支移入它们各自的策略类中以代替这些条件语句。

    也就是说,如果你的程序里面出现了大量的if else这样的语句,那么你就可以选择使用策略模式进行一个改进了。

  • 系统中各算法彼此完全独立,且要求对客户隐藏具体算法的实现细节时。

    也就是说,作为一个用户,当他并不需要去关注算法底层的实现时,就可以选择使用策略模式将他和算法进行一个分离(或者解耦)。

  • 系统要求使用算法的客户不应该知道其操作的数据时,可使用策略模式来隐藏与算法相关的数据结构。

  • 多个类只区别在表现行为不同,可以使用策略模式,在运行时动态选择具体要执行的行为。

策略模式在JDK源码中的应用

接下来,我们就来看一看策略模式在JDK源码里面是如何来应用的。

在JDK源码里面,其实很多地方都用到了策略模式,而在本套系列课程中,我们只看一个接口就行了,这个接口就是Comparator,它就用到了策略模式。

在Arrays类中有一个sort方法,方法定义如下:

public class Arrays {
    public static <T> void sort(T[] a, Comparator<? super T> c) {
        if (c == null) {
            sort(a);
        } else {
            if (LegacyMergeSort.userRequested)
                legacyMergeSort(a, c);
            else
                TimSort.sort(a, 0, a.length, c, null, 0, 0);
        }
    }
}

可以看到,该sort方法是一个静态方法,它是用于对第一个参数所代表的数组里面的元素进行排序的。既然是排序,那么是以什么样的一个规则进行排序的呢?这就得看第二个参数了,即Comparator接口,很显然,我们在传参时,应该传递的是该接口的子实现类对象。这样,该sort方法就会根据我们传递的子实现类对象里面的策略(或者规则)对数组里面的元素进行排序了。

其实,Arrays就是一个环境角色类,它里面的这个sort方法可以传一个新策略让Arrays根据这个策略来进行排序,就比如下面的测试类。

public class demo {
    public static void main(String[] args) {
        Integer[] data = {12, 2, 3, 2, 4, 5, 1};
        // 调用Arrays里面的sort方法对以上数组实现降序排序
        Arrays.sort(data, new Comparator<Integer>() {
            public int compare(Integer o1, Integer o2) {
                return o2 - o1;
            }
        });
        System.out.println(Arrays.toString(data)); // 最后排序的结果:[12, 5, 4, 3, 2, 2, 1]
    }
}

大家不妨去运行一下以上测试类,但是打印的结果肯定是按从大到小的顺序排序之后的数组。

在以上测试类中,我们在调用Arrays类中的sort方法时,第二个参数传递的是Comparator接口的子实现类对象(当然了,是以匿名内部类的形式传递的)。所以,Comparator接口充当的是抽象策略角色,而具体的子实现类充当的是具体策略角色。很显然,环境角色类(Arrays)应该持有抽象策略的引用来调用,通过该引用它就可以调用具体策略中的方法进行一系列的操作了。

所以,现在我们只需要去验证一下Arrays类里面的sort方法到底有没有使用Comparator子实现类中的compare方法就行了,如果使用了的话,那么就表明用的正是策略模式。

继续查看Arrays类里面的sort方法的源码,可以看到它又调用了TimSort类中的sort方法,如果你传递了具体策略(即Comparator接口的子实现类对象),那么程序势必就要走这儿的代码。因此,接下来我们就要去看看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;  // Arrays of size 0 and 1 are always sorted

        // If array is small, do a "mini-TimSort" with no merges
        if (nRemaining < MIN_MERGE) {
            int initRunLen = countRunAndMakeAscending(a, lo, hi, c);
            binarySort(a, lo, hi, lo + initRunLen, c);
            return;
        }

        /**
         * March over the array once, left to right, finding natural runs,
         * extending short natural runs to minRun elements, and merging runs
         * to maintain stack invariant.
         */
        TimSort<T> ts = new TimSort<>(a, c, work, workBase, workLen);
        int minRun = minRunLength(nRemaining);
        do {
            // Identify next run
            int runLen = countRunAndMakeAscending(a, lo, hi, c);

            // If run is short, extend to min(minRun, nRemaining)
            if (runLen < minRun) {
                int force = nRemaining <= minRun ? nRemaining : minRun;
                binarySort(a, lo, lo + force, lo + runLen, c);
                runLen = force;
            }

            // Push run onto pending-run stack, and maybe merge
            ts.pushRun(lo, runLen);
            ts.mergeCollapse();

            // Advance to find next run
            lo += runLen;
            nRemaining -= runLen;
        } while (nRemaining != 0);

        // Merge all remaining runs to complete sort
        assert lo == hi;
        ts.mergeForceCollapse();
        assert ts.stackSize == 1;
    }
}

可以看到,该方法也是一个静态方法,它里面的参数有很多很多,不过大家在这块只需要去关注c参数就行,因为它就是我们传递的Comparator接口的子实现类对象。而且,该方法里面的代码也挺多的,不过大家不用全部搞明白,只须跟随我的步伐就行。

在往下查看以上sort方法时,你会发现在countRunAndMakeAscending方法中用到了c参数,所以我们再跟踪进去countRunAndMakeAscending方法里面看看。

class TimSort<T> {
    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;

        // Find end of run, and reverse range if descending
        if (c.compare(a[runHi++], a[lo]) < 0) { // Descending
            while (runHi < hi && c.compare(a[runHi], a[runHi - 1]) < 0)
                runHi++;
            reverseRange(a, lo, runHi);
        } else {                              // Ascending
            while (runHi < hi && c.compare(a[runHi], a[runHi - 1]) >= 0)
                runHi++;
        }

        return runHi - lo;
    }
}

可以看到,该方法里面的参数也有很多很多,这里大家只需要去关注c参数就行。不知你看到没有,在该方法里面的if else判断语句中就用到了c参数,而且调用的是其里面的compare方法,所以我们就验证了一点,即Arrays类里面的sort方法对数组中的元素进行排序时,正好就用到了Comparator子实现类对象中的compare方法,这便是策略模式。

当然,这个策略模式可能跟我们上面所讲的那种标准的策略模式稍微有一些区别,但是大家在这儿主要理解的就是策略模式思想的运用,形式上虽有变化,但思想上是相通的。

以上是关于从零开始学习Java设计模式 | 行为型模式篇:策略模式的主要内容,如果未能解决你的问题,请参考以下文章

从零开始学习Java设计模式 | 行为型模式篇:状态模式

从零开始学习Java设计模式 | 行为型模式篇:状态模式

从零开始学习Java设计模式 | 行为型模式篇:命令模式

从零开始学习Java设计模式 | 行为型模式篇:命令模式

从零开始学习Java设计模式 | 行为型模式篇:责任链模式

从零开始学习Java设计模式 | 行为型模式篇:责任链模式