Skip to main content

策略模式(行为型)

策略模式 —— 重构小能手,拆分“胖逻辑”

策略模式是定义一系列的算法,把它们一个个封装起来,并且使它们可相互替换。

一、先从一个场景入手

有一天,产品经理 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,而要考虑更优的映射方案。