策略模式

什么是策略模式

定义

策略模式定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用算法的客户。

策略模式属于对象行为模式,它通过对算法进行封装,把使用算法的责任和算法的实现分割开来,并委派给不同的对象对这些算法进行管理。

简而言之,就是策略模式准备了一组算法,并将每个算法进行封装,使它们之间可用相互替换。

策略模式除了用来封装算法,也可以用来封装一系列的”业务规则”,只要这些业务规则指向的目标一致,并且可以被替换使用,我们就可以用策略模式来封装它们。

相关概念

一个基于策略模式的程序至少由两部分组成。第一个部分是一组策略类,第二个部分是环境类Context。

  • 策略类:策略类封装了具体的算法,并负责具体的计算过程。
  • 环境类Context:环境类Context接受客户的请求,随后把请求委托给某一个策略类。

基于传统面向对象语言的方式使用策略模式

尝试开一家猫咖

我们举一个猫咖的例子,假定我们开了一家猫咖,需要举办周年庆典,其中一项就是为我们猫咖的会员,在周年庆期间充值预存款,根据不同的VIP级别加赠不同比例的赠款余额。

例如,普通会员(regular)赠送价值预存款10%的赠款余额,金卡会员(gold)赠送价值预存款20%的赠款余额,白金卡会员(platinum)赠送价值预存款30%的赠款余额。

通常的逻辑,我们会想到使用if-else来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*
* 根据不同的VIP等级,返还不同比例的赠款余额
* @param vipLevel 会员等级
* @param deposit 会员预存款数值
*/
let calculateBonus = (vipLevel,deposit) => {
if (vipLevel === 'regular') {
return deposit * 0.1
}

if (vipLevel === 'gold') {
return deposit * 0.2
}

if (vipLevel === 'platinum') {
return deposit * 0.3
}
};

// 顾客1:金卡会员,预存500
calculateBonus('gold', 500) // 输出:100
// 顾客2:白金卡会员,预存2000
calculateBonus('platinum', 2000) // 输出:600

这样的代码会有以下三个问题:

  • calculateBonus函数比较庞大,包含需要if-else语句,难以维护。
  • calculateBonus函数缺乏灵活的弹性,如果需要增加钻石卡会员(diamond)的赠款策略,还需要通读函数内部实现,违反了开放-封闭原则
  • 赠款算法无复用性,在程序的其他地方需要重用,只能复制粘贴。

为了解决这三个问题中的第三个赠款算法无复用性的问题,我们可以尝试使用复合函数进行解决。

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
// 普通会员的赠款算法
let regular = (deposit) => {
return deposit * 0.1;
}
// 金卡会员的赠款算法
let gold = (deposit) => {
return deposit * 0.2;
}
// 白金卡会员的赠款算法
let platinum = (deposit) => {
return deposit * 0.3;
}

/*
* 根据不同的VIP等级,返还不同比例的赠款余额
* @param vipLevel 会员等级
* @param deposit 会员预存款数值
*/
let calculateBonus = (vipLevel,deposit) => {
if (vipLevel === 'regular') {
return regular(deposit);
}

if (vipLevel === 'gold') {
return gold(deposit);
}

if (vipLevel === 'platinum') {
return platinum(deposit);
}
};

// 顾客1:金卡会员,预存500
calculateBonus('gold', 500) // 输出:100
// 顾客2:白金卡会员,预存2000
calculateBonus('platinum', 2000) // 输出:600

这样也仅仅解决了赠款算法在程序其他地方需要服用的问题,仍然还存在着其它两个无法解决的问题。

因此,我们可以使用策略模式来重构代码。

使用策略模式重构猫咖周年庆预存活动

ES5实现

完整代码

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
/**
* 策略类
*/

// 普通会员策略类
var RegularCard = function () { };

RegularCard.prototype.calculate = function (deposit) {
return deposit * 0.1;
}

// 金卡会员策略类
var GoldCard = function () { };

GoldCard.prototype.calculate = function (deposit) {
return deposit * 0.2;
}

// 白金卡会员策略类
var PlatinumCard = function () { };

PlatinumCard.prototype.calculate = function (deposit) {
return deposit * 0.3;
}


/**
* 奖金类(对应环境类Context)
*/
var Bonus = function(){
this.deposit = null; // 预存款
this.strategy = null; // 会员等级对应的策略对象
};
// 设置顾客的预存款
Bonus.prototype.setSalary = function( deposit ){
this.deposit = deposit; // 设置顾客的预存款
};
// 设置顾客的会员等级对应的策略对象
Bonus.prototype.setStrategy = function( strategy ){
this.strategy = strategy; // 设置顾客的会员等级对应的策略对象
};

Bonus.prototype.getBonus = function(){ // 取得赠款金额
return this.strategy.calculate( this.deposit ); // 把计算赠款的操作委托给对应的策略对象
};

测试用例

1
2
3
4
5
6
7
8
9
var bonus = new Bonus();

bonus.setSalary( 2000 ); // 设置顾客的预存款2000
bonus.setStrategy( new GoldCard() ); // 设置策略对象-金卡会员

console.log( bonus.getBonus() ); // 输出:400

bonus.setStrategy( new PlatinumCard() ); // 设置策略对象-白金卡会员
console.log( bonus.getBonus() ); // 输出:600

ES6实现

完整代码

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
/**
* 策略类
*/

// 普通会员策略类
class RegularCard {
calculate(deposit) {
return deposit * 0.1;
}
}

// 金卡会员策略类
class GoldCard {
calculate(deposit) {
return deposit * 0.2;
}
}

// 白金卡会员策略类
class PlatinumCard {
calculate(deposit) {
return deposit * 0.3;
}
}


/**
* 奖金类(对应环境类Context)
*/
class Bonus {
constructor() {
this.deposit = null; // 预存款
this.strategy = null; // 会员等级对应的策略对象
}
// 设置顾客的预存款
setSalary( deposit ) {
this.deposit = deposit; // 设置顾客的预存款
}
// 设置顾客的会员等级对应的策略对象
setStrategy( strategy ) {
this.strategy = strategy; // 设置顾客的会员等级对应的策略对象
};
// 取得赠款金额
getBonus() {
return this.strategy.calculate( this.deposit ); // 把计算赠款的操作委托给对应的策略对象
};
}

测试用例

1
2
3
4
5
6
7
8
9
let bonus = new Bonus();

bonus.setSalary( 2000 ); // 设置顾客的预存款2000
bonus.setStrategy( new GoldCard() ); // 设置策略对象-金卡会员

console.log( bonus.getBonus() ); // 输出:400

bonus.setStrategy( new PlatinumCard() ); // 设置策略对象-白金卡会员
console.log( bonus.getBonus() ); // 输出:600

基于JavaScript语言使用策略模式

Peter Norvig在他的演讲中曾说过:“在函数作为一等对象的语言中,策略模式是隐形的。策略类strategy就是值为函数的变量。”

上述策略模式的实现是模拟传统面向对象语言的实现,由于在JavaScript中,函数也可以作为对象的value值成员,所以更方便的做法是使用对象字面量实现策略模式的策略类,同理,环境类(在这里是奖金类),也可以单独使用calculateBonus 函数来接受用户的请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 策略类
// 所有跟计算奖金有关的逻辑不再放在环境类Context中,而是分布在各个策略对象中。
let strategies = {
// 每个策略对象负责的算法被各自封装在对象内部
"RegularCard": function( deposit ){
return deposit * 0.1;
},
"GoldCard": function( deposit ){
return deposit * 0.2;
},
"PlatinumCard": function( deposit ){
return deposit * 0.3;
}
};
// 奖金类(对应环境类`calculateBonusContext)
// 环境类Context并没有计算奖金的能力,而是把这个职责委托给了某个策略对象
let calculateBonus = (vipLevel,deposit) => {
return strategies[vipLevel](deposit);
}

// 替换Context中当前保存的策略对象,便能执行不同的算法来得到我们想要的结果
console.log('GoldCard', 2000) // 输出:400
console.log('RegularCard', 1000) // 输出:100

优点与缺点

优点

  • 多重条件语句(if-else)不易维护,策略模式利用组合、委托和多态等技术和思想,可以有效地避免多重条件选择语句。
  • 在策略模式中利用组合和委托来让环境类拥有执行算法的能力,这也是继承的一种更轻便的替代方案。
  • 策略模式可以提供相同行为的不同实现,更容易满足需求的多变。
  • 策略模式提供了对开放—封闭原则的完美支持,将算法封装在独立的策略类strategy中,使得它们易于切换,易于理解,易于扩展。
  • 策略模式把算法的使用放到环境类中,而算法的实现移到具体策略类中,实现了二者的分离。

缺点

  • 使用策略模式会在程序中增加许多策略类或者策略对象,但实际上这比把它们负责的逻辑堆砌在环境类中要好。
  • 使用者需要理解所有策略算法strategy的区别,以便使用合适的策略算法。(比如,我们要选择一种合适的旅游出行路线,必须先了解选择飞机、火车、自行车等方案的细节。此时策略类strategy要向使用者暴露它的所有实现,这是违反最少知识原则的)

策略模式
https://sothx.com/2021/06/24/Strategy/
作者
Sothx
发布于
2021年6月24日
许可协议