文章

设计模式 - 策略模式

策略模式的定义

策略模式(Strategy Pattern):定义一系列算法,将每一个算法封装起来,并让它们可以相互替换。策略模式让算法独立于使用它的客户而变化,也称为政策模式(Policy)。

一个例子

在写策略模式之前,先看一个日常生活中常见的小例子: 我们在电商平台买东西的时候,平台一般会根据不同的用户制定不同的报价策略,比如新用户立减 10 元,VIP 客户免配送费,SVIP 客户 8 折优惠,现在我们要做一个结算模块,针对不同客户计算不同价格。
对于这个需求,我们很有可能写出这样的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class PriceManager {

    public int calculatePrice(int totalPrice, int shippingFee, String userType) {
        int finalPrice = totalPrice;
        switch (userType) {
            case "new":
                finalPrice = totalPrice - 10;
                break;
            case "vip":
                finalPrice = totalPrice - shippingFee;
                break;
            case "svip":
                finalPrice = (int)(totalPrice * 0.8);
                break;
            default:
                break;
        }
        return finalPrice;
    }
}

简单来看,上面的代码可以满足需求,但是其实是存在问题的,它把不同的价格算法都写在一个方法里面,随着业务量的增多,这个方法将会变得越来越庞大,越来越难维护。针对这个情况,可以对这个方法进行改进,把不同的算法独立成一个方法。

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
public class PriceManager {

    public int calculatePrice(int totalPrice, int shippingFee, String userType) {
        int finalPrice = totalPrice;
        switch (userType) {
            case "new":
                finalPrice = calculatePrice4New(totalPrice, shippingFee);
                break;
            case "vip":
                finalPrice = calculatePrice4Vip(totalPrice, shippingFee);
                break;
            case "svip":
                finalPrice = calculatePrice4SVip(totalPrice, shippingFee);
                break;
            default:
                finalPrice = calculatePrice4Default(totalPrice, shippingFee);
                break;
        }
        return finalPrice;
    }

    private int calculatePrice4New(int totalPrice, int shippingFee) {
        return totalPrice - 10;
    }

    private int calculatePrice4Vip(int totalPrice, int shippingFee) {
        return totalPrice - shippingFee;
    }

    private int calculatePrice4SVip(int totalPrice, int shippingFee) {
        return (int) (totalPrice * 0.8);
    }

    private int calculatePrice4Default(int totalPrice, int shippingFee) {
        return totalPrice;
    }
}

经过上面的修改,代码相对好了一点,如果某一个具体的算法规则改变了,我们只需要修改对应的方法即可。但是,还存在另外的问题:目前的用户类型只有四种,如果要新增用户类型同时新增价格算法,那么就必须修改计算价格的方法,在方法中增加分支,长此以往,方法中的分支将会变得非常多,而且需要经常修改。

这时候策略模式就派上用场了,使用策略模式可以使价格计算既可扩展、可维护,又可以方便的响应变化。

策略模式的一般结构

  1. 策略接口 IStrategy: 用来抽象一系列策略算法
  2. 具体的策略实现 StrategyImpl: 根据具体情况实现 IStrategy 中定义的算法
  3. 策略上下文 StrategyContext: 负责具体的策略交换,决定具体使用哪一个策略,通常 StrategyContext 会通过构造方法持有一个真正的策略实现对象
  4. 外部客户端通过 StrategyContext 指定具体的 IStrategy 从而实现不同策略的切换

代码示例:

1
2
3
4
5
6
7
/**
 * 策略接口
 */
public interface IStrategy {

    public void doSth();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
 * 策略实现A
 */
public class StrategyImplA implements IStrategy {
    @Override
    public void doSth() {
        System.out.println("StrategyImplA doSth");
    }
}
/**
 * 策略实现B
 */
public class StrategyImplB implements IStrategy {
    @Override
    public void doSth() {
        System.out.println("StrategyImplB doSth");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
 * 策略上下文
 */
public class StrategyContext {

    private IStrategy iStrategy;

    public StrategyContext(IStrategy iStrategy) {
        this.iStrategy = iStrategy;
    }

    public void doSth() {
        this.iStrategy.doSth();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
 * 外部客户端调用
 */
public class StrategyClient {

    public static void main(String[] args) {
        StrategyContext context;
        System.out.println("使用 StrategyImplA");
        context = new StrategyContext(new StrategyImplA());
        context.doSth();

        System.out.println("使用 StrategyImplB");
        context = new StrategyContext(new StrategyImplB());
        context.doSth();
    }
}

通过上面对策略模式的一般结构实现的了解,我们可以依葫芦画瓢,改造价格计算模块。

例子改造

首先分析一下价格计算模块与策略模式例子的一一对应关系

1
2
3
IStrategy:抽象价格计算方法   
StrategyImpl:不同类型用户实现不同的价格算法   
StrategyContext:价格计算上下文   

写成代码大概是这样:

1
2
3
4
5
6
7
/**
 * 价格计算策略接口
 */
public interface IPriceStrategy {

    public void calculatePrice(int totalPrice, int shippingFee);
}
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
/**
 * 新用户价格计算策略实现
 */
public class NewPriceStrategy implements IPriceStrategy {
    @Override
    public void calculatePrice(int totalPrice, int shippingFee) {
        System.out.println("新用户立减 10 元");
        return totalPrice - 10;
    }
}

/**
 * VIP 用户价格计算策略实现
 */
public class VipPriceStrategy implements IPriceStrategy {
    @Override
    public void calculatePrice(int totalPrice, int shippingFee) {
        System.out.println("VIP 用户免配送费");
        return totalPrice - shippingFee;
    }
}

/**
 * SVIP 用户价格计算策略实现
 */
public class VipPriceStrategy implements IPriceStrategy {
    @Override
    public void calculatePrice(int totalPrice, int shippingFee) {
        System.out.println("SVIP 用户 8 折");
        return (int)(totalPrice * 0.8);
    }
}

/**
 * 普通用户价格计算策略实现
 */
public class DefaultPriceStrategy implements IPriceStrategy {
    @Override
    public void calculatePrice(int totalPrice, int shippingFee) {
        System.out.println("普通用户无优惠");
        return totalPrice;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
 * 价格计算上下文
 */
public class PriceStrategyContext {

    private IPriceStrategy iStrategy;

    public PriceStrategyContext(IPriceStrategy iStrategy) {
        this.iStrategy = iStrategy;
    }

    public void calculatePrice(int totalPrice, int shippingFee) {
        this.iStrategy.calculatePrice(totalPrice, shippingFee);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
 * 外部客户端调用
 */
public class PriceStrategyClient {

    public static void main(String[] args) {
        int totalPrice = 100, shippingFee = 10;
        int finalPrice;
        PriceStrategyContext context;
        System.out.println("新用户结算:");
        context = new PriceStrategyContext(new NewPriceStrategy());
        finalPrice = context.calculatePrice(totalPrice, shippingFee);
        System.out.println("新用户结算价格:" + finalPrice);

        System.out.println("VIP 用户结算:");
        context = new PriceStrategyContext(new VipPriceStrategy());
        finalPrice = context.calculatePrice(totalPrice, shippingFee);
        System.out.println("VIP 用户结算价格:" + finalPrice);
    }
}

上面的代码使用策略模式同样实现价格计算模块,接着可以利用策略模式解决前面的提到的问题了。

假如新增了一个用户类型 SSVIP , 价格算法是可以享受免配送费之后 8 折优惠。这时候需要做的就是新增一个 SSVIP 的价格计算策略,然后外部客户端调用的时候采用 SSVIP 策略即可,代码如下:

1
2
3
4
5
6
7
8
9
10
/**
 * SSVIP 用户价格计算策略实现
 */
public class SSVipPriceStrategy implements IPriceStrategy {
    @Override
    public void calculatePrice(int totalPrice, int shippingFee) {
        System.out.println("SSVIP 用户免配送费后 8 折");
        return (int)((totalPrice - shippingFee) * 0.8);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
 * 外部客户端调用
 */
public class PriceStrategyClient {

    public static void main(String[] args) {
        int totalPrice = 100, shippingFee = 10;
        int finalPrice;
        PriceStrategyContext context;
        System.out.println("SSVIP 用户结算:");
        context = new PriceStrategyContext(new SSVipPriceStrategy());
        finalPrice = context.calculatePrice(totalPrice, shippingFee);
        System.out.println("SSVIP 用户结算价格:" + finalPrice);
    }
}

优缺点

优点:

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

缺点:

  • 客户端或者调用方必须了解所有的策略,清楚各个策略之间的不同,这样客户端才能选择正确的策略。
  • 增加了对象的数量,由于每个策略都是一个实现类,如果可选的策略很多的话,那对象的数量也会增多。

应用场景

  • 需要在多种算法中动态的选择一种的情况下可以使用
  • 不需要客户知道复杂的、与算法相关的数据结构时可以使用
  • 使用第三方库时,想要快速方便切换同类型的其他库,例如 Android 开发中的图片加载框架,可以使用策略模式封装一套 Glide 的策略,一套 Picasso 的策略
本文由作者按照 CC BY 4.0 进行授权