> For the complete documentation index, see [llms.txt](https://onenesss-organization.gitbook.io/jj-xiaoce/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://onenesss-organization.gitbook.io/jj-xiaoce/javascript-she-ji-mo-shi-he-xin-yuan-li-yu-ying-yong-shi-jian/13.-xing-wei-xing-ce-le-mo-shi-zhong-gou-xiao-neng-shou-chai-fen-pang-luo-ji.md).

# 13. 行为型：策略模式——重构小能手，拆分“胖逻辑”

### 前言

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

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

### 先来看一个真实场景

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

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

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

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

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

```js
// 询价方法，接受价格标签和原价为入参
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：

  ```js
  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。

**单一功能改造**

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

```js
// 处理预热价
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 增加新人询价逻辑，我该咋整？我只能这么来：

```js
// 处理预热价
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 中，有没有什么既能够既帮我们明确映射关系，同时不破坏代码的灵活性的方法呢？答案就是**对象映射**！

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

```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;
  },
};
```

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

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

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

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

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

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

### 这，就是策略模式！

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

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

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

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

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

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