最近被问到了什么是策略模式,很惭愧没有答出来。自己在设计模式方面确实有些欠缺,因此接下来打算撰写一系列有关设计模式的文章来帮助自己学习和加深理解,以达到在工作中,能够根据业务场景灵活选择合适的设计模式的目的。那么,这一系列的文章就从策略模式开始吧。

什么是策略模式

在软件开发中,实现某一个功能有多条途径,每一条途径对应一种算法,此时我们可以使用一种设计模式来实现灵活地选择解决途径,也能够方便地增加新的解决途径。策略模式就是为了适应算法灵活性而产生的设计模式。

场景

考虑这样一个场景,最近有关音乐版权的事情闹得沸沸扬扬,阿里旗下的虾米音乐和腾讯旗下的QQ音乐达成了版权共享,猪场的网易云就显得孤立无援了。这里就用这个事件举一个例子。
假设:

  1. 虾米音乐有较为完整的经典老歌的版权
  2. QQ音乐有较为完整的流行音乐的版权
  3. 网易云有较为完整的民谣的版权

我们打算做一个音乐聚合导航网站,用户选择不同类型的音乐类型时,我们自动为其选择不同的音乐提供商。在不使用设计模式的情况下,我们可能写出这样的代码

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
class MusicProvider {
private String providerName;

private String musicType;

public void setProviderName(String providerName) {
this.providerName = providerName;
}

public void setMusicType(String musicType) {
this.musicType = musicType;
}

public String getProviderName() {
return this.chooseProvider();
}

public String chooseProvider() {
if (this.musicType.equalsIgnoreCase("classic")) {
return "Xiami Music";
} else if (this.musicType.equalsIgnoreCase("pop") {
return "QQ Music";
} else if (this.musicType.equalsIgnoreCase("folk")) {
return "Netease Music";
} else {
return "No suitable music provider available";
}
}
}

// 客户端测试代码
class Client {
public static void main(String args[]) {
MusicProvider musicProvider = new MusicProvider();
musicProvider.setMusicType("classic");
System.out.println(musicProvider.getProviderName());

musicProvider.setMusicType("pop");
System.out.println(musicProvider.getProviderName());

musicProvider.setMusicType("folk");
System.out.println(musicProvider.getProviderName());
}
}

/**
* 输出结果
* Xiami Music
* QQ Music
* Netease Music
**/

不难发现,上面的代码有这样几个问题:

  1. chooseProvider方法有大量的if else,不利于维护
  2. 如果增加了音乐的类型或是音乐提供商,需要修改MusicProvider类,违反开闭原则,代码可扩展性很差
  3. 代码难以复用

这些都是因为MusicProvider类承担的职责过多、过重,自然不利于代码的扩展和复用。我们要做的就是将其拆分,将定义和使用分离,策略模式就是用来解决这个问题的。

使用策略模式

在策略模式中,我们可以定义一些独立的类来封装不同的算法,每一个类封装一种具体的算法,在这里,每一个封装算法的类我们都可以称之为一种策略(Strategy),为了保证这些策略在使用时具有一致性,一般会提供一个抽象的策略类来做规则的定义,而每种算法则对应于一个具体策略类。
策略模式的主要目的是将算法的定义与使用分开,也就是将算法的行为和环境分开,将算法的定义放在专门的策略类中,每一个策略类封装了一种实现算法,使用算法的环境类针对抽象策略类进行编程,符合“依赖倒转原则”。在出现新的算法时,只需要增加一个新的实现了抽象策略类的具体策略类即可。
策略模式定义如下: 策略模式(Strategy Pattern):定义一系列算法类,将每一个算法封装起来,并让它们可以相互替换,策略模式让算法独立于使用它的客户而变化,也称为政策模式(Policy)。策略模式是一种对象行为型模式。

策略模式结构并不复杂,但我们需要理解其中环境类Context的作用,其结构如下图所示:

在策略模式结构图中包含如下几个角色:

  1. Context(环境类):环境类是使用算法的角色,它在解决某个问题(即实现某个方法)时可以采用多种策略。在环境类中维持一个对抽象策略类的引用实例,用于定义所采用的策略。
  2. Strategy(抽象策略类):它为所支持的算法声明了抽象方法,是所有策略类的父类,它可以是抽象类或具体类,也可以是接口。环境类通过抽象策略类中声明的方法在运行时调用具体策略类中实现的算法。
  3. ConcreteStrategy(具体策略类):它实现了在抽象策略类中声明的算法,在运行时,具体策略类将覆盖在环境类中定义的抽象策略类对象,使用一种具体的算法实现某个业务处理。

策略模式的典型代码如下

1
2
3
abstract class AbstractStrategy {  
public abstract void algorithm(); //声明抽象算法
}

然后再将封装每一种具体算法的类作为该抽象策略类的子类,如下代码所示:

1
2
3
4
5
6
class ConcreteStrategyA extends AbstractStrategy {  
//算法的具体实现
public void algorithm() {
//算法A
}
}

其他具体策略类与之类似,对于Context类而言,在它与抽象策略类之间建立一个关联关系,其典型代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
class Context {  
private AbstractStrategy strategy; //维持一个对抽象策略类的引用

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

//调用策略类中的算法
public void algorithm() {
strategy.algorithm();
}
}

在Context类中定义一个AbstractStrategy类型的对象strategy,通过注入的方式在客户端传入一个具体策略对象,客户端代码片段如下所示:

1
2
3
4
5
Context context = new Context();  
AbstractStrategy strategy;
strategy = new ConcreteStrategyA(); //可在运行时指定类型
context.setStrategy(strategy);
context.algorithm();

下面就可以用策略模式来重构一开始的代码

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
59
60
61
62
63
64
65
66
67
// 音乐提供商类:环境类
class MusicProvider {

private ProviderChooser providerChooser; // 维持一个音乐提供商选择类的引用

// 注入一个音乐供应商选择类对象
public void setProviderChooser(ProviderChooser providerChooser) {
this.providerChooser = providerChooser;
}

public String geProviderName() {
// 调用音乐提供商选择类的折选择方法
return providerChooser.choose();
}
}

//音乐提供商选择类:抽象策略类
interface ProviderChooser {
public String choose();
}

// 经典歌曲选择类:具体策略类
class ClassicMusicChooser implements ProviderChooser {
public String choose() {
return "Xiami Music";
}
}

// 流行音乐选择类:具体策略类
class PopMusicChooser implements ProviderChooser {
public String choose() {
return "QQ Music";
}
}

// 民谣音乐选择类:具体策略类
class FolkMusicChooser implements ProviderChooser {
public String choose() {
return "Netease Music";
}
}

// 客户端测试代码
class Client {
public static void main(String args[]) {
MusicProvider musicProvider = new MusicProvider();
ProviderChooser chooser;
chooser = new ClassicMusicChooser();
musicProvider.setProviderChooser(chooser);
System.out.println(MusicProvider.getProviderName());

chooser = new PopMusicChooser();
musicProvider.setProviderChooser(chooser);
System.out.println(MusicProvider.getProviderName());

chooser = new FolkMusicChooser();
musicProvider.setProviderChooser(chooser);
System.out.println(MusicProvider.getProviderName());
}
}

/**
* 输出结果
* Xiami Music
* QQ Music
* Netease Music
**/

由上面的代码可以看出,如果要增加新的音乐提供商和音乐类型,只需要增加一个新的类实现ProviderChooser接口即可,符合开闭原则。(写完之后发现这个例子不是很恰当)

总结

策略模式的优点:

  1. 符合开闭原则,不需要修改原有代码
  2. 使用继承将公共的代码移到抽象策略类中,避免重复代码
  3. 将算法单独封装在策略类中,方便复用
  4. 避免编写大量的选择语句,易于维护

策略模式的缺点:

  1. 客户端需要知道所有的策略类
  2. 可能产生大量的策略类

参考资料
策略模式-Strategy Pattern