策略模式的概念
策略模式(Strategy)强调的是行为的灵活切换,比如一个类的多个方法有着类似的行为接口,可以将它们抽离出来作为一系列策略类,在运行时灵活对接,变更其算法策略,以适应不同的场景。
例如我们在乔欣、许魏洲主演的电视剧《我不能恋爱的女朋友》中,许魏洲饰演剧中的迟信。电视节目的金牌制作人,永远有PLAN B的职业病先生,在工作过程中由于情况突变而导致预案无法继续实施A计划时,马上更换为B计划,以另一种行为方式达成目标。所以说提前策划非常重要,而随机应变的能力更是不可或缺,系统需要时刻确保灵活性、机动性才能立于不败之地。
一个真实场景——产品促销-策略模式
在双十一大促来临之际,产品经理W小姐提出一个需求:差异化销售。
同一个商品,在不同时期下销售价格不同,大致氛围4种:
-
当商品处于预售期(尝鲜价)时,商品价格为9折
-
当商品处于正季销售时,商品价格为原价
-
当商品处于大促期时,商品价格为每满200-50,不满则享受8折
-
当商品处于反季期时,商品价格为每满100-50,不满则享受6折
预售期 preSale 正季销售 normalSale 大促期 bigSale 反季期 offSale 新人价newSale
C先生看到该需求的第一反应是这很简单呀,初次的代码实现,编写一个名为askPrice的函数来计算不同时期优惠完的实际价格。askPrice函数需要接受两个参数:代表不同时期的价格标签和原价价格。代码如下:
const askPrice = (tag, originPrice) => {
if(tag === 'pre') {
// 预售期价
return originPrice * 0.9
} else if(tag === 'normal') {
// 正季销售价
return originPrice
} else if(tag === 'big') {
// 大促期价
if(originPrice >= 200) {
let reduceTime = originPrice / 200;
return originPrice - 50 * reduceTime
}
return originPrice * 0.8
} else if(tag === 'off') {
// 反季期价
if(originPrice >= 100) {
let reduceTime = originPrice / 100;
return originPrice - 50 * reduceTime
}
return originPrice * 0.6
}
}
console.log('预售期价', askPrice('pre', 200)) // 预售期价 180
console.log('正季销售价', askPrice('normal', 200))// 正季销售价 200
console.log('大促期价', askPrice('big', 200))// 大促期价 150
console.log('反季期价', askPrice('off', 200))// 反季期价 100
以上这段代码十分简单,但是存在着显而易见的缺点:
-
askPrice函数比较庞大,包含了很多if-else语句,这些语句需要覆盖所有的逻辑分支。其中一行代码出现bug,那么将影响整个函数的逻辑,很难定位有问题的代码,函数复用性较差,单个功能很难被抽离等等,这是违反了单一功能的原则。
-
askPrice函数缺乏易扩展性,如果增加了一种新品发布newSale,或者由于竞争压力需要调整大促的优惠力度,那我们必须深入askPrice函数的内部实现,这是违反开放-封闭原则的。
与此同时,产品经理W小姐提出了一个新需求,决定增加一个两个需求,(1)新人福利价,第一次购买产品享受5折优惠,(2)在双十一当天满足满减的同时享受9折优惠。以上所有的福利均不累加,只能享受其中一种。随着需求的不断增加,为了提高工作效率及代码质量,减少回归测试,C先生决定重构这段代码。
第一步,实现单一功能的原则,使用组合函数重构代码
const preSale = function(originPrice) {
return originPrice * 0.9
}
const normalSale = function(originPrice) {
return originPrice
}
const bigSale = function(originPrice) {
if(originPrice >= 200) {
let reduceTime = originPrice / 200;
return (originPrice - 50 * reduceTime) * 0.9
}
return originPrice * 0.8
}
const offSale = function(originPrice) {
if(originPrice >= 100) {
let reduceTime = originPrice / 100;
return originPrice - 50 * reduceTime
}
return originPrice * 0.6
}
const newSale = function(originPrice) {
return originPrice * 0.5
}
const askPrice = function(tag, originPrice) {
if(tag == 'pre') {
return preSale(originPrice)
} else if(tag == 'normal') {
return normalSale(originPrice)
} else if(tag == 'big') {
return bigSale(originPrice)
} else if(tag == 'off') {
return offSale(originPrice)
} else if(tag == 'new') {
return newSale(originPrice)
}
}
console.log('预售期价', askPrice('pre', 200)) // 预售期价 180
console.log('正季销售价', askPrice('normal', 200))// 正季销售价 200
console.log('大促期价', askPrice('big', 200))// 大促期价 135
console.log('反季期价', askPrice('off', 200))// 反季期价 100
console.log('新人价', askPrice('new', 200)) // 新人价 100
这段代码C先生做到了一个函数只做一件事,每个函数都有自己明确的分工,符合单一功能的原则。该程序在一定程度上得到了改善,但最终还是没有逃脱掉if-else的魔掌,没有解决根本问题。
第二步,实现开放-封闭的原则,使用策略模式进行重构
const preSale = function(originPrice) {
return originPrice * 0.9
}
const normalSale = function(originPrice) {
return originPrice
}
const bigSale = function(originPrice) {
if(originPrice >= 200) {
let reduceTime = originPrice / 200;
return (originPrice - 50 * reduceTime) * 0.9
}
return originPrice * 0.8
}
const offSale = function(originPrice) {
if(originPrice >= 100) {
let reduceTime = originPrice / 100;
return originPrice - 50 * reduceTime
}
return originPrice * 0.6
}
const newSale = function(originPrice) {
return originPrice * 0.5
}
const strategies = {
'pre': preSale,
'normal':normalSale,
'big': bigSale,
'off': offSale,
'new': newSale
}
const askPrice = function(tag, originPrice) {
return strategies[tag](originPrice)
}
console.log('预售期价', askPrice('pre', 200)) // 预售期价 180
console.log('正季销售价', askPrice('normal', 200))// 正季销售价 200
console.log('大促期价', askPrice('big', 200))// 大促期价 135
console.log('反季期价', askPrice('off', 200))// 反季期价 100
console.log('新人价', askPrice('new', 200)) // 新人价 100
上面的代码实现了“对扩展开放,对修改封闭”的效果
当产品经理W小姐再提出新需求的时候,C先生可以根据新功能进行小幅度的改造,通过增加一个新函数或者改造某一个函数来满足需求。涉及改动范围较小,代码易读、易维护,且便于测试,只需要测试改动部分,不需要做回归测试。
总结
变化是世界的常态,唯一不变的就是变化本身。策略模式的定义是:定义一系列的算法,把它们一个个封装起来, 并且使它们可以相互替换。
策略模式整体理解难度不大,在面试中几乎没有什么权重,但是却对培养良好的编码习惯和重构意识大有裨益,且利于提高代码质量等。
通过小故事的方式介绍知识点儿来源于《大话设计模式》,通过故事的方式学习知识点儿。
参考书籍:《JavaScript设计模式与开发实践》
《大话设计模式》
《秒懂设计模式》