行为型-策略(Strategy)

4/4/2022 设计模式

摘要

JDK:1.8.0_202

# 一:场景问题

# 1.1 报价管理

向客户报价,对不同的客户要报不同的价格,比如:

    1. 对普通客户或者是新客户报的是全价
    2. 对老客户报的价格,根据客户年限,给予一定的折扣
    3. 对大客户报的价格,根据大客户的累计消费金额,给予一定的折扣

还要考虑客户购买的数量和金额,比如:虽然是新用户,但是一次购买的数量非常大,或者是总金额非常高,也会有一定的折扣。

还有,报价人员的职务高低,也决定了他是否有权限对价格进行一定的浮动折扣。

甚至在不同的阶段,对客户的报价也不同,一般情况是刚开始比较高,越接近成交阶段,报价越趋于合理。

总之,向客户报价是非常复杂的,因此在一些CRM(客户关系管理)的系统中,会有一个单独的报价管理模块,来处理复杂的报价功能。

为了演示的简洁性,假定现在需要实现一个简化的报价管理,实现如下的功能:

(1)对普通客户或者是新客户报全价

(2)对老客户报的价格,统一折扣5%

(3)对大客户报的价格,统一折扣10%

该怎么实现呢?

# 1.2 不用模式的解决方式

/**
 * 价格管理,主要完成计算向客户所报价格的功能
 */
public class Price {

    /**
     * 报价,对不同类型的,计算不同的价格
     *
     * @param goodsPrice   商品销售原价
     * @param customerType 客户类型
     * @return 计算出来的,应该给客户报的价格
     */
    public BigDecimal quote(BigDecimal goodsPrice, String customerType) {
        switch (customerType) {
            case "普通客户":
                return this.calcPriceForNormal(goodsPrice);
            case "老客户":
                return this.calcPriceForOld(goodsPrice);
            case "大客户":
                return this.calcPriceForLarge(goodsPrice);
        }
        //其余人员都是报原价
        return goodsPrice;
    }

    /**
     * 为新客户或者是普通客户计算应报的价格
     *
     * @param goodsPrice 商品销售原价
     * @return 计算出来的,应该给客户报的价格
     */
    private BigDecimal calcPriceForNormal(BigDecimal goodsPrice) {
        System.out.println("对于新客户或者是普通客户,没有折扣 ");
        return goodsPrice;
    }

    /**
     * 为老客户计算应报的价格
     *
     * @param goodsPrice 商品销售原价
     * @return 计算出来的,应该给客户报的价格
     */
    private BigDecimal calcPriceForOld(BigDecimal goodsPrice) {
        System.out.println("对于老客户,统一折扣 5%");
        return goodsPrice.multiply(BigDecimal.ONE.subtract(new BigDecimal("0.05")));
    }

    /**
     * 为大客户计算应报的价格
     *
     * @param goodsPrice 商品销售原价
     * @return 计算出来的,应该给客户报的价格
     */
    private BigDecimal calcPriceForLarge(BigDecimal goodsPrice) {
        System.out.println("对于大客户,统一折扣 10%");
        return goodsPrice.multiply(BigDecimal.ONE.subtract(new BigDecimal("0.1")));
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

# 1.3 有何问题

如果存在多个计算方式,这会让这个价格类非常庞大,难以维护。而且,维护和拓展都需要修改已有的代码,这样很不好,违反了 开-闭原则

其次:经常会有这样的需要,在不同的时候,要使用不同的计算方式

比如:在公司周年庆的时候,所有的客户额外增加3%的折扣;在换季促销的时候,普通客户是额外增加折扣2%,老客户是额外增加折扣3%,大客户是额外增加折扣5%。这意味着计算报价的方式会经常被修改,或者被切换。

通常情况下应该是被切换,因为过了促销时间,又还回到正常的价格体系上来了。而现在的价格类中计算报价的方法,是固定调用各种计算方式,这使得切换调用不同的计算方式很麻烦,每次都需要修改 case 里面的调用代码。

那么到底应该如何实现,才能够让价格类中的计算报价的算法,能很容易的实现可维护、可扩展,又能动态的切换变化呢?

# 二:解决方案

用来解决上述问题的一个合理的解决方案就是策略模式。

策略模式:定义一系列的算法,把它们一个个封装起来,并且使它们可相互替换。本模式使得算法可独立于使用它的客户而变化。

# 2.1 解决思路

仔细分析上面的问题,先来把它抽象一下,各种计算报价的计算方式就好比是具体的算法,而使用这些计算方式来计算报价的程序,就相当于是使用算法的客户。

再分析上面的实现方式,为什么会造成那些问题,根本原因,就在于算法和使用算法的客户是耦合的,甚至是密不可分的,在上面实现中,具体的算法和使用算法的客户是同一个类里面的不同方法。

现在要解决那些问题,按照策略模式的方式,应该先把所有的计算方式独立出来,每个计算方式做成一个单独的算法类,从而形成一系列的算法,并且为这一系列算法定义一个公共的接口,这些算法实现是同一接口的不同实现,地位是平等的,可以相互替换。这样一来,要扩展新的算法就变成了增加一个新的算法实现类,要维护某个算法,也只是修改某个具体的算法实现即可,不会对其它代码造成影响。也就是说这样就解决了可维护、可扩展的问题。

为了实现让算法能独立于使用它的客户,策略模式引入了一个上下文的对象,这个对象负责持有算法,但是不负责决定具体选用哪个算法,把选择算法的功能交给了客户,由客户选择好具体的算法后,设置到上下文对象里面,让上下文对象持有客户选择的算法,当客户通知上下文对象执行功能的时候,上下文对象会去转调具体的算法。这样一来,具体的算法和直接使用算法的客户是分离的。

具体的算法和使用它的客户分离过后,使得算法可独立于使用它的客户而变化,并且能够动态的切换需要使用的算法,只要客户端动态的选择使用不同的算法,然后设置到上下文对象中去,实际调用的时候,就可以调用到不同的算法。

# 2.2 模式结构和说明

classDiagram Strategy <|.. ConcreteStrategyA Strategy <|.. ConcreteStrategyB Strategy <|.. ConcreteStrategyC Context o--> Strategy class Strategy{ <<interface>> +algorithmInterface()* void } class ConcreteStrategyA{ +algorithmInterface() void } class ConcreteStrategyB{ +algorithmInterface() void } class ConcreteStrategyC{ +algorithmInterface() void } class Context{ -Strategy strategy +Context(Strategy) +contextInterface() void }
  • Strategy:策略接口,用来约束一系列具体的策略算法。Context使用这个接口来调用具体的策略实现定义的算法。
  • ConcreteStrategy:具体的策略实现,也就是具体的算法实现。
  • Context:上下文,负责和具体的策略类交互,通常上下文会持有一个真正的策略实现,上下文还可以让具体的策略类来获取上下文的数据,甚至让具体的策略类来回调上下文的方法。

# 2.3 示例代码

定义算法的接口:

/**
 * 策略,定义算法的接口
 */
public interface Strategy {

    /**
     * 某个算法的接口,可以有传入参数,也可以有返回值
     */
    void algorithmInterface();

}
1
2
3
4
5
6
7
8
9
10
11

具体的算法实现了:(ConcreteStrategyA.java、ConcreteStrategyB.java 和 ConcreteStrategyC.java)

/**
 * 实现具体的算法
 */
public class ConcreteStrategyA implements Strategy {

    @Override
    public void algorithmInterface() {
        // 具体的算法实现
    }
}
1
2
3
4
5
6
7
8
9
10
/**
 * 实现具体的算法
 */
public class ConcreteStrategyB implements Strategy {

    @Override
    public void algorithmInterface() {
        // 具体的算法实现
    }
}
1
2
3
4
5
6
7
8
9
10
/**
 * 实现具体的算法
 */
public class ConcreteStrategyC implements Strategy {

    @Override
    public void algorithmInterface() {
        // 具体的算法实现
    }
}
1
2
3
4
5
6
7
8
9
10

# 2.4 重写案例

要使用策略模式来重写前面报价的示例,大致有如下改变:

首先需要定义出算法的接口。

然后把各种报价的计算方式单独出来,形成算法类。

对于Price这个类,把它当做上下文,在计算报价的时候,不再需要判断,直接使用持有的具体算法进行运算即可。选择使用哪一个算法的功能挪出去,放到外部使用的客户端去。

程序结构如果所示:

classDiagram Strategy <|.. NormalCustomerStrategy Strategy <|.. OldCustomerStrategy Strategy <|.. LargeCustomerStrategy Price o--> Strategy class Strategy{ <<interface>> +cakPrice(BigDecimal)* BigDecimal } class NormalCustomerStrategy{ +cakPrice(BigDecimal) BigDecimal } class OldCustomerStrategy{ +cakPrice(BigDecimal) BigDecimal } class LargeCustomerStrategy{ +cakPrice(BigDecimal) BigDecimal } class Price{ -Strategy strategy +Price(Strategy) +quote(BigDecimal) BigDecimal }

策略接口:

/**
 * 策略,定义计算报价算法的接口
 */
public interface Strategy {

    /**
     * 计算应报的价格
     *
     * @param goodsPrice 商品销售原价
     * @return 计算出来的,应该给客户报的价格
     */
    BigDecimal calcPrice(BigDecimal goodsPrice);

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

具体的算法实现:(NormalCustomerStrategy.java、OldCustomerStrategy.java 和 LargeCustomerStrategy.java)

/**
 * 具体算法实现,为新客户或者是普通客户计算应报的价格
 */
public class NormalCustomerStrategy implements Strategy {
    @Override
    public BigDecimal calcPrice(BigDecimal goodsPrice) {
        System.out.println("对于新客户或者是普通客户,没有折扣");
        return goodsPrice;
    }
}
1
2
3
4
5
6
7
8
9
/**
 * 具体算法实现,为老客户计算应报的价格
 */
public class OldCustomerStrategy implements Strategy {
    @Override
    public BigDecimal calcPrice(BigDecimal goodsPrice) {
        System.out.println("对于老客户,统一折扣5%");
        return goodsPrice.multiply(BigDecimal.ONE.subtract(new BigDecimal("0.05")));
    }
}
1
2
3
4
5
6
7
8
9
10
/**
 * 具体算法实现,为大客户计算应报的价格
 */
public class LargeCustomerStrategy implements Strategy {
    @Override
    public BigDecimal calcPrice(BigDecimal goodsPrice) {
        System.out.println("对于大客户,统一折扣10%");
        return goodsPrice.multiply(BigDecimal.ONE.subtract(new BigDecimal("0.1")));
    }
}
1
2
3
4
5
6
7
8
9
10

上下文的实现,原来的价格类,变化比较大,主要有:

原来那些私有的,用来做不同计算的方法,已经去掉了,独立出去做成了算法类

原来报价方法里面,对具体计算方式的判断,去掉了,让客户端来完成选择具体算法的功能

新添加持有一个具体的算法实现,通过构造方法传入

原来报价方法的实现,变化成了转调具体算法来实现

/**
 * 价格管理,主要完成计算向客户所报价格的功能
 */
public class Price {

    /**
     * 持有一个具体的策略对象
     */
    private Strategy strategy = null;

    /**
     * 构造方法,传入一个具体的策略对象
     *
     * @param strategy 具体的策略对象
     */
    public Price(Strategy strategy) {
        this.strategy = strategy;
    }

    /**
     * 报价,计算对客户的报价
     *
     * @param goodsPrice 商品销售原价
     * @return 计算出来的,应该给客户报的价格
     */
    public BigDecimal quote(BigDecimal goodsPrice) {
        return strategy.calcPrice(goodsPrice);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

客户端:

public class Client {
    public static void main(String[] args) {
        //1:选择并创建需要使用的策略对象
        Strategy strategy = new LargeCustomerStrategy();
        //2:创建上下文
        Price ctx = new Price(strategy);
        //3:计算报价
        BigDecimal quote = ctx.quote(new BigDecimal(1000));
        System.out.println("向客户报价:" + quote.doubleValue());
    }
}
1
2
3
4
5
6
7
8
9
10
11

# 三:模式讲解

# 3.1 认识策略模式

1. 策略模式的功能

策略模式的功能是把具体的算法实现,从具体的业务处理里面独立出来,把它们实现成为单独的算法类,从而形成一系列的算法,并让这些算法可以相互替换。

策略模式的重心不是如何来实现算法,而是如何组织、调用这些算法,从而让程序结构更灵活、具有更好的维护性和扩展性。

2. 策略模式和 if-else/switch 语句

看了前面的示例,会发现,每个策略算法具体实现的功能,就是原来在 if-else/switch 结构中的具体实现。

其实多个 if-else/switch 语句表达的就是一个平等的功能结构,你要么执行if,要不你就执行else,或者是elseif,这个时候,if块里面的实现和else块里面的实现从运行地位上来讲就是平等的。

而策略模式就是把各个平等的具体实现封装到单独的策略实现类了,然后通过上下文来与具体的策略类进行交互。

因此多个if-else/switch语句可以考虑使用策略模式。

3. 算法的平等性

策略模式一个很大的特点就是各个策略算法的平等性。对于一系列具体的策略算法,大家的地位是完全一样的,正是因为这个平等性,才能实现算法之间可以相互替换。

所有的策略算法在实现上也是相互独立的,相互之间是没有依赖的。

所以可以这样描述这一系列策略算法:策略算法是相同行为的不同实现

4. 谁来选择具体的策略算法

在策略模式中,可以在两个地方来进行具体策略的选择。

一个是在客户端,在使用上下文的时候,由客户端来选择具体的策略算法,然后把这个策略算法设置给上下文。前面的示例就是这种情况。

还有一个是客户端不管,由上下文来选择具体的策略算法,这个在后面讲容错恢复的时候给大家演示一下。

5. Strategy的实现方式

在前面的示例中,Strategy都是使用的接口来定义的,这也是常见的实现方式。但是如果多个算法具有公共功能的话,可以把Strategy实现成为抽象类,然后把多个算法的公共功能实现到Strategy里面

6. 运行时策略的唯一性

运行期间,策略模式在每一个时刻只能使用一个具体的策略实现对象,虽然可以动态的在不同的策略实现中切换,但是同时只能使用一个。

# 3.2 优缺点

1. 定义一系列算法

策略模式的功能就是定义一系列算法,实现让这些算法可以相互替换。所以会为这一系列算法定义公共的接口,以约束一系列算法要实现的功能。如果这一系列算法具有公共功能,可以把策略接口实现成为抽象类,把这些公共功能实现到父类里面,对于这个问题,前面讲了三种处理方法,这里就不罗嗦了。

2. 避免多重条件语句

根据前面的示例会发现,策略模式的一系列策略算法是平等的,可以互换的,写在一起就是通过 if-else/switch 结构来组织,如果此时具体的算法实现里面又有条件语句,就构成了多重条件语句,使用策略模式能避免这样的多重条件语句。

3. 更好的扩展性

在策略模式中扩展新的策略实现非常容易,只要增加新的策略实现类,然后在选择使用策略的地方选择使用这个新的策略实现就好了。

4. 客户必须了解每种策略的不同

策略模式也有缺点,比如让客户端来选择具体使用哪一个策略,这就可能会让客户需要了解所有的策略,还要了解各种策略的功能和不同,这样才能做出正确的选择,而且这样也暴露了策略的具体实现。

5. 增加了对象数目

由于策略模式把每个具体的策略实现都单独封装成为类,如果备选的策略很多的话,那么对象的数目就会很可观。

6. 只适合扁平的算法结构

策略模式的一系列算法地位是平等的,是可以相互替换的,事实上构成了一个扁平的算法结构,也就是在一个策略接口下,有多个平等的策略算法,就相当于兄弟算法。而且在运行时刻只有一个算法被使用,这就限制了算法使用的层级,使用的时候不能嵌套使用。

对于出现需要嵌套使用多个算法的情况,比如折上折、折后返卷等业务的实现,需要组合或者是 嵌套使用多个算法的情况,可以考虑使用装饰模式、或是变形的职责链、或是AOP等方式来实现

# 3.3 思考策略模式

1. 策略模式的本质

策略模式的本质:分离算法,选择实现

仔细思考策略模式的结构和实现的功能,会发现,如果没有上下文,策略模式就回到了最基本的接口和实现了,只要是面向接口编程的,那么就能够享受到接口的封装隔离带来的好处。也就是通过一个统一的策略接口来封装和隔离具体的策略算法,面向接口编程的话,自然不需要关心具体的策略实现,也可以通过使用不同的实现类来实例化接口,从而实现切换具体的策略。

看起来好像没有上下文什么事情,但是如果没有上下文,那么就需要客户端来直接与具体的策略交互,尤其是当需要提供一些公共功能,或者是相关状态存储的时候,会大大增加客户端使用的难度。因此,引入上下文还是很必要的,有了上下文,这些工作就由上下文来完成了,客户端只需要与上下文交互就可以了,这样会让整个设计模式更独立、更有整体性,也让客户端更简单。

但纵观整个策略模式实现的功能和设计,它的本质还是“分离算法,选择实现”,因为分离并封装了算法,才能够很容易的修改和添加算法;也能很容易的动态切换使用不同的算法,也就是动态选择一个算法来实现需要的功能了。

2. 对设计原则的体现

从设计原则上来看,策略模式很好的体现了开-闭原则。策略模式通过把一系列可变的算法进行封装,并定义出合理的使用结构,使得在系统出现新算法的时候,能很容易的把新的算法加入到已有的系统中,而已有的实现不需要做任何修改。这在前面的示例中已经体现出来了,好好体会一下。

从设计原则上来看,策略模式还很好的体现了里氏替换原则。策略模式是一个扁平结构,一系列的实现算法其实是兄弟关系,都是实现同一个接口或者继承的同一个父类。这样只要使用策略的客户保持面向抽象类型编程,就能够使用不同的策略的具体实现对象来配置它,从而实现一系列算法可以相互替换。

3. 何时选用策略模式

建议在如下情况中,选用策略模式:

出现有许多相关的类,仅仅是行为有差别的情况,可以使用策略模式来使用多个行为中的一个来配置一个类的方法,实现算法动态切换。

出现同一个算法,有很多不同的实现的情况,可以使用策略模式来把这些“不同的实现”实现成为一个算法的类层次。

需要封装算法中,与算法相关的数据的情况,可以使用策略模式来避免暴露这些跟算法相关的数据结构。

出现抽象一个定义了很多行为的类,并且是通过多个 if-else/switch 语句来选择这些行为的情况,可以使用策略模式来代替这些条件语句。

# 3.4 相关模式

1. 策略模式和状态模式

这两个模式从模式结构上看是一样的,但是实现的功能是不一样的。

状态模式是根据状态的变化来选择相应的行为,不同的状态对应不同的类,每个状态对应的类实现了该状态对应的功能,在实现功能的同时,还会维护状态数据的变化。这些实现状态对应的功能的类之间是不能相互替换的。

策略模式是根据需要或者是客户端的要求来选择相应的实现类,各个实现类是平等的,是可以相互替换的。

另外策略模式可以让客户端来选择需要使用的策略算法,而状态模式一般是由上下文,或者是在状态实现类里面来维护具体的状态数据,通常不由客户端来指定状态。

2. 策略模式和模板方法模式

这两个模式可组合使用,如同前面示例的那样。

模板方法重在封装算法骨架,而策略模式重在分离并封装算法实现。

3. 策略模式和享元模式

这两个模式可组合使用。

策略模式分离并封装出一系列的策略算法对象,这些对象的功能通常都比较单一,很多时候就是为了实现某个算法的功能而存在,因此,针对这一系列的、多个细粒度的对象,可以应用享元模式来节省资源,但前提是这些算法对象要被频繁的使用,如果偶尔用一次,就没有必要做成享元了。

# 四:JDK

# 五:参考文献

最后更新: 4/4/2022, 6:36:26 PM