有一天,产品经理韩梅梅找到李雷,给李雷提了这么个需求:
马上大促要来了,我们本次大促要做差异化询价。啥是差异化询价?就是说同一个商品,我通过在后台给它设置不同的价格类型,可以让它展示不同的价格。具体的逻辑如下:

  1. 当价格类型为“预售价”时,满 100 - 20,不满 100 9
  2. 当价格类型为“大促价”时,满 100 - 30,不满 100 8
  3. 当价格类型为“返场价”时,满 200 - 50,不叠加
  4. 当价格类型为“尝鲜价”时,直接打 5

李雷扫了一眼 prd,立刻来了主意。他首先将四种价格做了标签化:
预售价 - pre
大促价 - onSale
返场价 - back
尝鲜价 - fresh

  1. // 询价方法,接受价格标签和原价为入参
  2. function askPrice(tag, originPrice) {
  3. // 处理预热价
  4. if(tag === 'pre') {
  5. if(originPrice >= 100) {
  6. return originPrice - 20
  7. }
  8. return originPrice * 0.9
  9. }
  10. // 处理大促价
  11. if(tag === 'onSale') {
  12. if(originPrice >= 100) {
  13. return originPrice - 30
  14. }
  15. return originPrice * 0.8
  16. }
  17. // 处理返场价
  18. if(tag === 'back') {
  19. if(originPrice >= 200) {
  20. return originPrice - 50
  21. }
  22. return originPrice
  23. }
  24. // 处理尝鲜价
  25. if(tag === 'fresh') {
  26. return originPrice * 0.5
  27. }
  28. }

我们一起来看看这么写代码会带来什么后果:

  • 首先,它违背了“单一功能”原则。一个 function 里面,它竟然处理了四坨逻辑——这个函数的逻辑太胖了!比如说万一其中一行代码出了 Bug,那么整个询价逻辑都会崩坏;与此同时出了 Bug 你很难定位到底是哪个代码块坏了事;再比如说单个能力很难被抽离复用等等等等。相信跟着我一路学下来的各位,也已经在重重实战中对胖逻辑的恶劣影响有了切身的体会。总之,见到胖逻辑,我们的第一反应,就是一个字——拆!
  • 不仅如此,它还违背了“开放封闭”原则。假如要加一个满 100 - 50 的“新人价”怎么办?只能继续 if-else:
  1. function askPrice(tag, originPrice) {
  2. // 处理预热价
  3. if(tag === 'pre') {
  4. if(originPrice >= 100) {
  5. return originPrice - 20
  6. }
  7. return originPrice * 0.9
  8. }
  9. // 处理大促价
  10. if(tag === 'onSale') {
  11. if(originPrice >= 100) {
  12. return originPrice - 30
  13. }
  14. return originPrice * 0.8
  15. }
  16. // 处理返场价
  17. if(tag === 'back') {
  18. if(originPrice >= 200) {
  19. return originPrice - 50
  20. }
  21. return originPrice
  22. }
  23. // 处理尝鲜价
  24. if(tag === 'fresh') {
  25. return originPrice * 0.5
  26. }
  27. // 处理新人价
  28. if(tag === 'newUser') {
  29. if(originPrice >= 100) {
  30. return originPrice - 50
  31. }
  32. return originPrice
  33. }
  34. }

重构询价逻辑

现在我们基于我们已经学过的设计模式思想,一点一点改造掉这个臃肿的 askPrice。

单一功能改造

首先,我们赶紧把四种询价逻辑提出来,让它们各自为政:

  1. // 处理预热价
  2. function prePrice(originPrice) {
  3. if(originPrice >= 100) {
  4. return originPrice - 20
  5. }
  6. return originPrice * 0.9
  7. }
  8. // 处理大促价
  9. function onSalePrice(originPrice) {
  10. if(originPrice >= 100) {
  11. return originPrice - 30
  12. }
  13. return originPrice * 0.8
  14. }
  15. // 处理返场价
  16. function backPrice(originPrice) {
  17. if(originPrice >= 200) {
  18. return originPrice - 50
  19. }
  20. return originPrice
  21. }
  22. // 处理尝鲜价
  23. function freshPrice(originPrice) {
  24. return originPrice * 0.5
  25. }
  26. function askPrice(tag, originPrice) {
  27. // 处理预热价
  28. if(tag === 'pre') {
  29. return prePrice(originPrice)
  30. }
  31. // 处理大促价
  32. if(tag === 'onSale') {
  33. return onSalePrice(originPrice)
  34. }
  35. // 处理返场价
  36. if(tag === 'back') {
  37. return backPrice(originPrice)
  38. }
  39. // 处理尝鲜价
  40. if(tag === 'fresh') {
  41. return freshPrice(originPrice)
  42. }
  43. }

OK,我们现在至少做到了一个函数只做一件事。现在每个函数都有了自己明确的、单一的分工:
prePrice - 处理预热价
onSalePrice - 处理大促价
backPrice - 处理返场价
freshPrice - 处理尝鲜价
askPrice - 分发询价逻辑
如此一来,我们在遇到 Bug 时,就可以做到“头痛医头,脚痛医脚”,而不必在庞大的逻辑海洋里费力去定位到底是哪块不对。
同时,如果我在另一个函数里也想使用某个询价能力,比如说我想询预热价,那我直接把 prePrice 这个函数拿去调用就是了,而不必在 askPrice 肥胖的身躯里苦苦寻觅、然后掏出这块逻辑、最后再复制粘贴到另一个函数去——更何况万一哪天 askPrice 里的预热价逻辑改了,你还得再复制粘贴一次
到这里,在单一功能原则的指引下,我们已经解决了一半的问题。
我们现在来捋一下,其实这个询价逻辑整体上来看只有两个关键动作:
询价逻辑的分发 ——> 询价逻辑的执行
在改造的第一步,我们已经把“询价逻辑的执行”给摘了出去,并且实现了不同询价逻辑之间的解耦。接下来,我们就要拿“分发”这个动作开刀。


开放封闭改造

剩下一半的问题是啥呢?就是咱们上面说的那个新人价的问题——这会儿我要想给 askPrice 增加新人询价逻辑,我该咋整?我只能这么来:

  1. // 处理预热价
  2. function prePrice(originPrice) {
  3. if(originPrice >= 100) {
  4. return originPrice - 20
  5. }
  6. return originPrice * 0.9
  7. }
  8. // 处理大促价
  9. function onSalePrice(originPrice) {
  10. if(originPrice >= 100) {
  11. return originPrice - 30
  12. }
  13. return originPrice * 0.8
  14. }
  15. // 处理返场价
  16. function backPrice(originPrice) {
  17. if(originPrice >= 200) {
  18. return originPrice - 50
  19. }
  20. return originPrice
  21. }
  22. // 处理尝鲜价
  23. function freshPrice(originPrice) {
  24. return originPrice * 0.5
  25. }
  26. // 处理新人价
  27. function newUserPrice(originPrice) {
  28. if(originPrice >= 100) {
  29. return originPrice - 50
  30. }
  31. return originPrice
  32. }
  33. function askPrice(tag, originPrice) {
  34. // 处理预热价
  35. if(tag === 'pre') {
  36. return prePrice(originPrice)
  37. }
  38. // 处理大促价
  39. if(tag === 'onSale') {
  40. return onSalePrice(originPrice)
  41. }
  42. // 处理返场价
  43. if(tag === 'back') {
  44. return backPrice(originPrice)
  45. }
  46. // 处理尝鲜价
  47. if(tag === 'fresh') {
  48. return freshPrice(originPrice)
  49. }
  50. // 处理新人价
  51. if(tag === 'newUser') {
  52. return newUserPrice(originPrice)
  53. }
  54. }

在外层,我们编写一个 newUser 函数用于处理新人价逻辑;在 askPrice 里面,我们新增了一个 if-else 判断。可以看出,这样其实还是在修改 askPrice 的函数体,没有实现“对扩展开放,对修改封闭”的效果。
那么我们应该怎么做?这么多 if-else,目的到底是什么?是不是就是为了把 询价标签-询价函数 这个映射关系给明确下来?那么在 JS 中,有没有什么既能够既帮我们明确映射关系,同时不破坏代码的灵活性的方法呢?答案就是对象映射

  1. // 定义一个询价处理器对象
  2. const priceProcessor = {
  3. pre(originPrice) {
  4. if (originPrice >= 100) {
  5. return originPrice - 20;
  6. }
  7. return originPrice * 0.9;
  8. },
  9. onSale(originPrice) {
  10. if (originPrice >= 100) {
  11. return originPrice - 30;
  12. }
  13. return originPrice * 0.8;
  14. },
  15. back(originPrice) {
  16. if (originPrice >= 200) {
  17. return originPrice - 50;
  18. }
  19. return originPrice;
  20. },
  21. fresh(originPrice) {
  22. return originPrice * 0.5;
  23. },
  24. };

当我们想使用其中某个询价算法的时候:通过标签名去定位就好了:

  1. // 询价函数
  2. function askPrice(tag, originPrice) {
  3. return priceProcessor[tag](originPrice)
  4. }

如此一来,askPrice 函数里的 if-else 大军彻底被咱们消灭了。这时候如果你需要一个新人价,只需要给 priceProcessor 新增一个映射关系:

  1. priceProcessor.newUser = function (originPrice) {
  2. if (originPrice >= 100) {
  3. return originPrice - 50;
  4. }
  5. return originPrice;
  6. }

这样一来,询价逻辑的分发也变成了一个清清爽爽的过程。当李雷以这种方式新增一个新人价的询价逻辑的时候,就可以底气十足地对测试同学说:老哥,我改了询价逻辑,但是改动范围仅仅涉及到新人价,是一个单纯的功能增加。所以你只测这个新功能点就 OK,老逻辑不用管!

说起来你可能不相信,咱们上面的整个重构的过程,就是对策略模式的应用。
现在大家来品品策略模式的定义:

  1. 定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换。