什么是策略模式
定义
策略模式定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用算法的客户。
策略模式属于对象行为模式,它通过对算法进行封装,把使用算法的责任和算法的实现分割开来,并委派给不同的对象对这些算法进行管理。
简而言之,就是策略模式准备了一组算法,并将每个算法进行封装,使它们之间可用相互替换。
策略模式除了用来封装算法,也可以用来封装一系列的"业务规则",只要这些业务规则指向的目标一致,并且可以被替换使用,我们就可以用策略模式来封装它们。
相关概念
一个基于策略模式的程序至少由两部分组成。第一个部分是一组策略类,第二个部分是环境类Context。
基于传统面向对象语言的方式使用策略模式
尝试开一家猫咖
我们举一个猫咖的例子,假定我们开了一家猫咖,需要举办周年庆典,其中一项就是为我们猫咖的会员,在周年庆期间充值预存款,根据不同的VIP级别加赠不同比例的赠款余额。
例如,普通会员(regular)赠送价值预存款10%的赠款余额,金卡会员(gold)赠送价值预存款20%的赠款余额,白金卡会员(platinum)赠送价值预存款30%的赠款余额。
通常的逻辑,我们会想到使用if-else来实现。
/*
* 根据不同的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)的赠款策略,还需要通读函数内部实现,违反了开放-封闭原则。
赠款算法无复用性,在程序的其他地方需要重用,只能复制粘贴。
为了解决这三个问题中的第三个赠款算法无复用性的问题,我们可以尝试使用复合函数进行解决。
// 普通会员的赠款算法
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实现
完整代码
/**
* 策略类
*/
// 普通会员策略类
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 ); // 把计算赠款的操作委托给对应的策略对象
};
测试用例
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实现
完整代码
/**
* 策略类
*/
// 普通会员策略类
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 ); // 把计算赠款的操作委托给对应的策略对象
};
}
测试用例
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
函数来接受用户的请求。
// 策略类
// 所有跟计算奖金有关的逻辑不再放在环境类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
中,使得它们易于切换,易于理解,易于扩展。
策略模式把算法的使用放到环境类中,而算法的实现移到具体策略类中,实现了二者的分离。
缺点