策略模式(行为型)
策略模式 —— 重构小能手,拆分“胖逻辑”
策略模式是定义一系列的算法,把它们一个个封装起来,并且使它们可相互替换。
一、先从一个场景入手
有一天,产品经理 kkOma 找 Tim 提了个需求:马上大促要来了,本次大促要做差异化询价,即同一个商品,通过在后台给它设置不同的价格类型,可以让它展示不同的价格。具体逻辑如下:
- 当价格类型为“预售价”时,满 100 - 20,不满 100 打 9 折;
- 当价格类型为“大促价”时,满 100 - 30,不满 100 打 8 折;
- 当价格类型为“返场价”时,满 200 - 50,不叠加;
- 当价格类型为“尝鲜价”时,直接打 5 折。
Tim 扫了一眼需求,将四种价格做了标签化:
预售价 - pre
大促价 - onSale
返场价 - back
尝鲜价 - fresh
接下来 Tim 仔细研读了需求内容,作为资深 if-else 侠,他三下五除二就写出一套功能完备的代码:
// 询价方法,接受价格标签和原价为入参
function askPrice(tag, originPrice) {
// 处理预热价
if (tag === "pre") {
if (originPrice >= 100) {
return originPrice - 20;
}
return originPrice * 0.9;
}
// 处理大促价
if (tag === "onSale") {
if (originPrice >= 100) {
return originPrice - 30;
}
return originPrice * 0.8;
}
// 处理返场价
if (tag === "back") {
if (originPrice >= 200) {
return originPrice - 50;
}
return originPrice;
}
// 处理尝鲜价
if (tag === "fresh") {
return originPrice * 0.5;
}
}
二、if-else 侠,人人喊打
上述代码运行起来虽然没啥问题,但作为人人喊打的 if-else 侠,Tim 必须为他的行为付出代价。
违背了“单一功能”原则
一个 function 中竟然处理了四坨逻辑,万一其中一行代码出了 Bug,那么整个询价逻辑都会崩坏;与此同时出了 Bug 很难定位,另外单个能力很难被抽离复用等等。因此,见到胖逻辑,第一反应就是一个字——拆!
违背了“开放封闭”原则
假如有一天 kkOma 再次找到 Tim,要他加一个满 100 - 50 的“新人价”,他只能继续 if-else:
function askPrice(tag, originPrice) {
// 处理预热价
if (tag === "pre") {
if (originPrice >= 100) {
return originPrice - 20;
}
return originPrice * 0.9;
}
// 处理大促价
if (tag === "onSale") {
if (originPrice >= 100) {
return originPrice - 30;
}
return originPrice * 0.8;
}
// 处理返场价
if (tag === "back") {
if (originPrice >= 200) {
return originPrice - 50;
}
return originPrice;
}
// 处理尝鲜价
if (tag === "fresh") {
return originPrice * 0.5;
}
// 处理新人价
if (tag === "newUser") {
if (originPrice >= 100) {
return originPrice - 50;
}
return originPrice;
}
}
没错,Tim 跑去改了 askPrice
函数!随后他恬不知耻地测试同学说:哥,我改了询价函数,麻烦你帮我把整个询价逻辑回归一下。
测试同学 Bang 一笑,心中早已有无数头羊驼在狂奔。他做完了这漫长而不必要的回归测试,随后对 Tim 说:哥,求你学学设计模式吧!
三、重构询价逻辑
学了一段时间后,Tim 基于前面的设计模式思想,一点一点改造掉这个臃肿的 askPrice
方法:
1、单一功能改造
首先把四种询价逻辑提出来:
// 处理预热价
function prePrice(originPrice) {
if (originPrice >= 100) {
return originPrice - 20;
}
return originPrice * 0.9;
}
// 处理大促价
function onSalePrice(originPrice) {
if (originPrice >= 100) {
return originPrice - 30;
}
return originPrice * 0.8;
}
// 处理返场价
function backPrice(originPrice) {
if (originPrice >= 200) {
return originPrice - 50;
}
return originPrice;
}
// 处理尝鲜价
function freshPrice(originPrice) {
return originPrice * 0.5;
}
function askPrice(tag, originPrice) {
// 处理预热价
if (tag === "pre") {
return prePrice(originPrice);
}
// 处理大促价
if (tag === "onSale") {
return onSalePrice(originPrice);
}
// 处理返场价
if (tag === "back") {
return backPrice(originPrice);
}
// 处理尝鲜价
if (tag === "fresh") {
return freshPrice(originPrice);
}
}
现在至少做到了一个函数只做一件事,每个函数都有了自己明确、单一的分工:
prePrice - 处理预热价
onSalePrice - 处理大促价
backPrice - 处理返场价
freshPrice - 处理尝鲜价
askPrice - 分发询价逻辑
而且,如果 Tim 在另一个函数中也想使用某个询价能力,比如想询预热价,就可以直接把 prePrice
这个函数拿去调用,而不必在 askPrice
肥胖的身躯中苦苦寻觅这块逻辑了。
到这来,这个询价逻辑整体上来看只有两个关键动作:
询价逻辑的分发 → 询价逻辑的执行
在改造的第一步,Tim 已经把“询价逻辑的执行”给提取出来,并且实现了不同询价逻辑之间的解耦。接下来就要拿“分发”这个动作开刀。
2、开放封闭改造
剩下的问题就是上面那个新人价的问题,当 Tim 想给 askPrice
增加新人询价逻辑时,可以这么做:
// 处理预热价
function prePrice(originPrice) {
if (originPrice >= 100) {
return originPrice - 20;
}
return originPrice * 0.9;
}
// 处理大促价
function onSalePrice(originPrice) {
if (originPrice >= 100) {
return originPrice - 30;
}
return originPrice * 0.8;
}
// 处理返场价
function backPrice(originPrice) {
if (originPrice >= 200) {
return originPrice - 50;
}
return originPrice;
}
// 处理尝鲜价
function freshPrice(originPrice) {
return originPrice * 0.5;
}
// 处理新人价
function newUserPrice(originPrice) {
if (originPrice >= 100) {
return originPrice - 50;
}
return originPrice;
}
function askPrice(tag, originPrice) {
// 处理预热价
if (tag === "pre") {
return prePrice(originPrice);
}
// 处理大促价
if (tag === "onSale") {
return onSalePrice(originPrice);
}
// 处理返场价
if (tag === "back") {
return backPrice(originPrice);
}
// 处理尝鲜价
if (tag === "fresh") {
return freshPrice(originPrice);
}
// 处理新人价
if (tag === "newUser") {
return newUserPrice(originPrice);
}
}
在外层,Tim 编写一个 newUser
函数用于处理新人价逻辑;在 askPrice
里新增了一个 if-else 判断。可以看出,这样其实还是在修改 askPrice
的函数体,没有实现“对扩展开放,对修改封闭”的效果。
那么应该怎么做呢?
上面用了这么多 if-else 就是为了把 询价标签-询价函数 这个映射关系给明确下来,而在 JS 中,有没有什么既能明确映射关系,又不破坏代码灵活性的方法呢?答案是对象映射!
可以把询价算法全都收敛到一个对象里:
// 定义一个询价处理器对象
const priceProcessor = {
pre(originPrice) {
if (originPrice >= 100) {
return originPrice - 20;
}
return originPrice * 0.9;
},
onSale(originPrice) {
if (originPrice >= 100) {
return originPrice - 30;
}
return originPrice * 0.8;
},
back(originPrice) {
if (originPrice >= 200) {
return originPrice - 50;
}
return originPrice;
},
fresh(originPrice) {
return originPrice * 0.5;
},
};
当想使用其中某个询价算法时,就可以直接通过标签名去定位:
// 询价函数
function askPrice(tag, originPrice) {
return priceProcessor[tag](originPrice);
}
如此一来,askPrice
函数的 if-else 大军就被消灭了。这时如果要加一个新人价,只需要给 priceProcessor
新增一个映射关系:
priceProcessor.newUser = function (originPrice) {
if (originPrice >= 100) {
return originPrice - 50;
}
return originPrice;
};
这样询价逻辑的分发就成了一个清爽的过程。当 Tim 以这种方式新增一个新人价的询价逻辑时,就可以底气十足地对测试同学说:哥,我改了询价逻辑,但改动范围仅涉及到新人价,是一个单纯的功能增加,你只测这个新功能点就 OK,老逻辑不用管!
从此,Tim 就从人人喊打的 if-else 侠,摇身一变成为了测试之友,项目也变得易读、易维护。
四、策略模式总结
上面的整个重构的过程,就是对策略模式的应用。现在回头看策略模式的定义:
定义一系列的算法,把它们一个个封装起来,并且使它们可相互替换。
上面的重构过程就是就干了这些事,“算法”就是这个场景中的询价逻辑,它可以是任何一个功能函数的逻辑;“封装”就是把某一功能点对应的逻辑给提出来;“可替换”建立在封装的基础上,只是说这个替换的判断过程不能直接怼 if-else,而要考虑更优的映射方案。