小谈策略模式

327 阅读7分钟

什么是策略模式?

五一黄金周,你打算和你的朋友出发去海南感受一下海岛风情,此时的你有以下几个选择

  • 乘坐飞机,下面是你在某出行平台对北京到海口的航班票务信息

image.png

  • 乘坐火车,下面是你在12306上查询到的信息

image.png


我们看到每种出行方式需要花费的时间和钱是不一样的

  • 如果手头富裕并且向尽快去海南游玩,你可以选择飞机的方式
  • 如果手头不富裕,那么只有选择火车这种交通方式虽然花一些时间,但是胜在价格便宜。

最后你和你的朋友选择了飞机出行的方式,完美的度过了假期。


大学时我们上交作业,如果作业内有很多素材,老师会让我们将文件压缩后上交,压缩文件时,我们可以选择使用zip或者是rar格式来压缩,我们完全可以灵活多样的相互替换。

image.png



在实际的例子当中,我们看到这些方式都是灵活多样的,我们在达到我们想要的目的时,可以根据自己的情况随意替换解决方案。这也就引申出来我们策略模式的定义:

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


如何实现策略模式

让我们来实现一个开发者大会的票价计算

作为开发同学,相信应该都不陌生开发者大会。这些开发者大会为了举办的需要会卖票给开发者,此时对于不同时候购买的开发者同学价格也不一样,通常是越早购买约便宜。现在作为主办方的你,设计了下面的票价方案:


  • 早鸟票,大会开始前两个月以上购买的开发者享受原票价6折优惠。
  • 预售票,大会开始前两个月内,一个月以上购买的开发者享受票价8折优惠。
  • 正价票,大会前开始一个月内,购买的开发者全价票款购买。


image.png

用if-else来实现票价


假设是一个新手看到了这个需求,一般脑海中的第一反应就是用if-else来实现这个功能,方便快捷,代码如下:


const conferencePrice = (type, price) => {
  // 早鸟票
  if (type === 'earlyBirds') {
    return price * 0.6
  }

  // 预售票
  if (type === 'preSale') {
    return price * 0.8
  }

  // 正价票
  if (type === 'normal') {
    return price
  }
}

// 输出:600
console.log(conferencePrice('earlyBirds', 1000))
// 输出:1000
console.log(conferencePrice('normal', 1000))

拒绝if-else使用

现在你的销售人员告诉你,需要再加一种票务类型:网络优惠票,大会前开始一个月内,通过网络渠道购买的开发者享受票价9折优惠。


按照上面的写法,你需要再加上一种类型来处理这个票价类型,代码如下:

const conferencePrice = (type, price) => {
  // 早鸟票
  if (type === 'earlyBirds') {
    return price * 0.6
  }

  // 预售票
  if (type === 'preSale') {
    return price * 0.8
  }
  
  // 网络票
  if (type === 'online') {
    return price * 0.9
  }

  // 正价票
  if (type === 'normal') {
    return price
  }
}

// 输出:600
console.log(conferencePrice('earlyBirds', 1000))
// 输出:1000
console.log(conferencePrice('normal', 1000))


由于是在流程内添加的,因此测试同学需要将整个流程都测试一遍。此时测试同学的内心是崩溃的!!想拿一块板砖拍死你

image.png


使用策略模式重构

上面的if-else写法有哪些问题?

上面的if-else代码实际上违反了以下两个原则:

  • 单一职责原则
    • 我们可以看到conferencePrice方法内,有三坨逻辑,导致代码臃肿,一旦一个逻辑出现问题会导致整个方法直接崩溃,风险可想而知!而且如果想复用逻辑会非常困难。
  • 开放-封闭原则
    • 在销售同学提出要新增加一种售票种类时,上述的代码只能添加if-else语句,这样导致测试的同学需要将之前的流程都测试一遍才能保证功能的正常!


image.png


实际上,我们的内心是拒绝这种代码的,接下来让我们开始使用策略模式的重构之旅

抽离所有的价格逻辑


我们先一步步来,将这四种询价逻辑抽离出来,成为独立的方法:


// 早鸟票
const earlyBirdsPrice = (price) => {
  return price * 0.6
}

// 预售票
const preSalePrice = (price) => {
  return price * 0.8
}

// 网络票
const onlinePrice = (price) => {
  return price * 0.9
}

// 正价票
const normalPrice = (price) => {
  return price
}


现在我们每个方法的分工明确,各司其职,完全符合我们的单一职责原则:


earlyBirdsPrice() // 早鸟票
preSalePrice() // 预售票
onlinePrice() // 网络票
normalPrice() // 正价票


由于没有前置的if-else逻辑,因此当出现问题时,只要找到对应的功能函数即可,无需查看是否是之前的代码影响了改逻辑。

使用映射计算价格

现在我们已经把计算价格的逻辑拆分了出来,但是如果我们还是用if-else来调用各个方法的话,实际上还是违反了开放封闭原则,一旦增加了需求,我还是要更改if-else的分支逻辑。因此我们需要使用映射来将不同的策略一一对应。首先我们先将上面的价格方案放到一个对象中去


// 价格策略
const priceStrategies = {
  // 早鸟票
  earlyBirds (price) {
    return price * 0.6
  },

  // 预售票
  preSale (price) {
    return price * 0.8
  },

  // 网络票
  online (price) {
    return price * 0.9
  },

  // 正价票
  normal (price) {
    return price
  }
}


加下来我们添加一个方法来映射不同的价格策略


// 查询票价
const conferencePrice = (type, price) => {
  return priceStrategies[type](price)
}


此时如果新增加一种票价类型,比如团购票,只需要简单的增加一个映射关系即可,完全不会影响到现有的逻辑:


priceStrategies.group = (price) => {
  return price * 0.7
}


此时测试的同学,也只需要测试这一个分支逻辑即可,完全不会影响别的逻辑。

策略模式小结

下面我们梳理一下整个的代码结构:


- 具体策略
    - earlyBirdsPrice() // 早鸟票
    - preSalePrice() // 预售票
    - onlinePrice() // 网络票
    - normalPrice() // 正价票
- 策略
    - priceStrategies对象
- 上下文
    - conferencePrice() // 由该方法来实现查询价格的需求


完整代码如下:


// 价格策略
const priceStrategies = {
  // 早鸟票
  earlyBirds (price) {
    return price * 0.6
  },

  // 预售票
  preSale (price) {
    return price * 0.8
  },

  // 网络票
  online (price) {
    return price * 0.9
  },

  // 正价票
  normal (price) {
    return price
  }
}

// 查询票价
const conferencePrice = (type, price) => {
  return priceStrategies[type](price)
}

// 输出:600
console.log(conferencePrice('earlyBirds', 1000))
// 输出:1000
console.log(conferencePrice('normal', 1000))


请对比完整代码和梳理的代码结构仔细想一想,策略模式的实现基于哪几个必要的要素。平时写代码遇到类似的情况下代码应该如何书写。

策略模式结构

通过上面代码的学习,相信大家对于策略模式的结构有了基本的了解,下面这幅图带大家重温一下相关结构。

策略模式.png

策略模式的优缺点

优点

  • 满足开放-封闭原则
  • 上面的例子中很好的提现了策略模式对开放-封闭原则的完美支持,将算法封装在具体策略中,无需修改上下文就可以引入新的策略,让我们易于切换、扩展和理解。
  • 消除了一些if-else条件语句
  • 策略模式中的一些算法也可以复用在系统的其它地方,避免一些复制粘贴代码的行为。
  • 策略模式使用组合和委托来让上下文有执行具体某个算法的能力。

缺点

  • 我们需要了解每种策略之间的不同,以此来选择一个合适的策略。
  • 策略模式会增加系统内对象的数目
  • 策略模式会在程序中添加许多策略对象,不过实际上笔者认为这样比逻辑堆砌要好很多。