Skip to content
On this page

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


前言

策略模式和状态模式属于本书”彩蛋“性质的附加小节。这两种模式理解难度都不大,在面试中也几乎没有什么权重,但是却对大家培养良好的编码习惯和重构意识却大有裨益。针对这两种模式,大家了解、会用即可,不建议大家死磕。

策略模式不太适合一上来就怼概念,容易懵。咱们就先从一个非常贴近业务的需求讲起,大家跟我一起敲完这波代码,自然会知道策略模式是怎么回事儿了。

先来看一个真实场景

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

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

李雷扫了一眼 prd,立刻来了主意。他首先将四种价格做了标签化:

javascript
预售价 - pre
大促价 - onSale
返场价 - back
尝鲜价 - fresh

接下来李雷仔细研读了 prd 的内容,作为资深 if-else 侠,他三下五除二就写出一套功能完备的代码:

javascript
// 询价方法,接受价格标签和原价为入参
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 侠,李雷必须为他的行为付出代价。我们一起来看看这么写代码会带来什么后果:

  • 首先,它违背了“单一功能”原则。一个 function 里面,它竟然处理了四坨逻辑——这个函数的逻辑太胖了!这样会带来什么样的糟糕后果,笔者在前面的小节中已经 BB 过很多次了:比如说万一其中一行代码出了 Bug,那么整个询价逻辑都会崩坏;与此同时出了 Bug 你很难定位到底是哪个代码块坏了事;再比如说单个能力很难被抽离复用等等等等。相信跟着我一路学下来的各位,也已经在重重实战中对胖逻辑的恶劣影响有了切身的体会。总之,见到胖逻辑,我们的第一反应,就是一个字——拆!

  • 不仅如此,它还违背了“开放封闭”原则。假如有一天韩梅梅再次找到李雷,要他加一个满 100 - 50 的“新人价”怎么办?他只能继续 if-else:

    javascript
    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
    }
    }
    

    没错,李雷灰溜溜地跑去改了 askPrice 函数!随后他恬不知耻地徐徐转头,对背后的测试同学说:哥,我改了询价函数,麻烦你帮我把整个询价逻辑回归一下。测试同学莞尔一笑, 心中早已有无数头羊驼在狂奔。他强忍着周末加班的悲痛,做完了这漫长而不必要的回归测试,随后默默点击了同事系统里的举报按钮对李雷说:哥,求你学学设计模式吧!!

重构询价逻辑

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

单一功能改造

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

javascript
// 处理预热价
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)
  }
}

OK,我们现在至少做到了一个函数只做一件事。现在每个函数都有了自己明确的、单一的分工:

prePrice - 处理预热价
onSalePrice - 处理大促价
backPrice - 处理返场价
freshPrice - 处理尝鲜价
askPrice - 分发询价逻辑

如此一来,我们在遇到 Bug 时,就可以做到“头痛医头,脚痛医脚”,而不必在庞大的逻辑海洋里费力去定位到底是哪块不对。

同时,如果我在另一个函数里也想使用某个询价能力,比如说我想询预热价,那我直接把 prePrice 这个函数拿去调用就是了,而不必在 askPrice 肥胖的身躯里苦苦寻觅、然后掏出这块逻辑、最后再复制粘贴到另一个函数去——更何况万一哪天 askPrice 里的预热价逻辑改了,你还得再复制粘贴一次,扎心啊老铁!

到这里,在单一功能原则的指引下,我们已经解决了一半的问题。

我们现在来捋一下,其实这个询价逻辑整体上来看只有两个关键动作:

询价逻辑的分发 ——> 询价逻辑的执行

在改造的第一步,我们已经把“询价逻辑的执行”给摘了出去,并且实现了不同询价逻辑之间的解耦。接下来,我们就要拿“分发”这个动作开刀。

开放封闭改造

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

javascript
// 处理预热价
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)
  }
}

在外层,我们编写一个 newUser 函数用于处理新人价逻辑;在 askPrice 里面,我们新增了一个 if-else 判断。可以看出,这样其实还是在修改 askPrice 的函数体,没有实现“对扩展开放,对修改封闭”的效果。

那么我们应该怎么做?我们仔细想想,楼上用了这么多 if-else,我们的目的到底是什么?是不是就是为了把 询价标签-询价函数 这个映射关系给明确下来?那么在 JS 中,有没有什么既能够既帮我们明确映射关系,同时不破坏代码的灵活性的方法呢?答案就是对象映射

咱们完全可以把询价算法全都收敛到一个对象里去嘛:

javascript
// 定义一个询价处理器对象
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;
  },
};

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

javascript
// 询价函数
function askPrice(tag, originPrice) {
  return priceProcessor[tag](originPrice)
}

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

javascript
priceProcessor.newUser = function (originPrice) {
  if (originPrice >= 100) {
    return originPrice - 50;
  }
  return originPrice;
}

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

从此,李雷就从人人喊打的 if-else 侠,摇身一变成为了测试之友、中国好开发。业务代码里的询价逻辑,也因为李雷坚守设计原则100年不动摇,而变得易读、易维护。

这,就是策略模式!

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

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

回头看看,咱们忙活到现在,是不是就干了这事儿?

但你要直接读这句话,可能确实会懵圈——啥是算法?如何封装?可替换又是咋做到的?

如今你你已经自己动手实现了算法提取、算法封装、分发优化的整个一条龙的操作流,相信面对这条定义,你可以会心一笑——算法,就是我们这个场景中的询价逻辑,它也可以是你任何一个功能函数的逻辑;“封装”就是把某一功能点对应的逻辑给提出来;“可替换”建立在封装的基础上,只是说这个“替换”的判断过程,咱们不能直接怼 if-else,而要考虑更优的映射方案。

(阅读过程中有任何想法或疑问,或者单纯希望和笔者交个朋友啥的,欢迎大家添加我的微信xyalinode与我交流^_^)