策略模式 ⭐
定义
定义一系列的算法(策略),把它们一个个封装起来,并且使它们可以相互替换。
策略模式(Strategy Pattern) 使得算法可以独立于使用它的客户端(上下文)而变化。
就像你打开高德地图,从公司回家的出行方式。你可以选择“驾车”、“打车”、“公交”、“骑行”或“步行”。
不管你选哪种方式,你的目的都是回家(客户端调用一致)。高德地图不需要把你和某种特定的交通工具绑死,每种交通工具(策略)的计算时间和路线逻辑都是独立的,你可以随时一键切换。
痛点:当一个功能有多种实现分支时,新手通常会写出无数个 if-else。一旦增加新分支或修改老分支,整个庞大的函数都要被修改,极易牵一发而动全身。
解决:将每一个分支的逻辑剥离出来,封装成独立的“策略对象/函数”,根据不同的条件直接映射调用。
与设计原则的关系
策略模式几乎是面向对象原则的完美教科书:
- 开闭原则 (Open-Closed Principle):
- 最核心体现。当你需要新增一种算法(比如出行方式新增了“坐直升机”)时,你只需要新增一个直升机的策略类/函数即可,完全不需要去修改原来的地图环境代码和已有的策略。
- 单一职责原则 (Single Responsibility Principle):
- 每种算法/业务规则都被提取到了独立的策略模块中。A算法的修改绝对不会影响到B算法。
- 多用组合,少用继承 (Favor Composition Over Inheritance):
- 系统(Context)不通过继承子类来改变自己的行为,而是通过组合不同的策略对象来切换行为,这让代码极其灵活。
实现
我们通过一个前端经典的案例—— “计算员工年终奖” 来看看代码是如何演进的。
1. 反面教材
function calculateBonus(level, salary) {
if (level === 'S') {
return salary * 4;
} else if (level === 'A') {
return salary * 3;
} else if (level === 'B') {
return salary * 2;
} else if (level === 'C') {
return salary * 1;
}
}
// 缺点:如果明年加了 'SS' 级别,或者修改了 'A' 级的系数,就要钻进这个长函数里改代码。
2. 现代 JavaScript 的策略模式
在 JS 中,函数是一等公民,且对象可以当做字典(Map)使用。所以前端在使用策略模式时,往往不需要写繁琐的类,用一个配置对象就可以完美实现:
// 1. 封装策略字典
const bonusStrategies = {
'S': (salary) => salary * 4,
'A': (salary) => salary * 3,
'B': (salary) => salary * 2,
'C': (salary) => salary * 1,
};
// 2. 环境调用(极其清爽,完全消灭了 if-else)
function calculateBonus(level, salary) {
// 加上容错机制,如果找不到策略就走默认逻辑
return bonusStrategies[level] ? bonusStrategies[level](salary) : 0;
}
// 使用
console.log(calculateBonus('S', 10000)); // 40000
// 扩展:明年老板说加个 SS 级!
// 完全不需要改动 calculateBonus 函数,只要往字典里加一项即可:
bonusStrategies['SS'] = (salary) => salary * 6;
前端应用
策略模式在前端业务逻辑和组件设计中无处不在,以下是几个高频场景:
1. 表单校验库 (Form Validation)
著名的校验库(如 async-validator,Element UI 和 Ant Design 底层都在用)天然就是策略模式。
// 策略集合:不同的校验规则
const validRules = {
isEmail: (value) => /.*@.*/.test(value) || '邮箱格式不正确',
minLength: (value, length) => value.length >= length || `长度不能小于${length}`,
isMobile: (value) => /^1[3-9]\d{9}$/.test(value) || '手机号格式不正确'
};
// 使用时,只需要组装策略
const rules =[
{ type: 'isEmail', value: 'test#gmail.com' },
{ type: 'minLength', value: '123', length: 6 }
];
2. 电商复杂价格计算 (促销活动)
双十一时,购物车可能有各种复杂的满减规则:满100减20、打8折、VIP折上折。
把每一个活动规则写成一个策略函数,后端下发不同的活动代码(如 discountType: 'vip'),前端通过字典匹配对应的策略函数去计算展示的金额。
3. 动态组件渲染 (Vue / React 组件分发)
当我们需要根据后端返回的 type 渲染不同的 UI 模块时,不要写一堆 v-if / v-else-if。
利用组件映射对象配合动态组件,这就是 UI 维度的策略模式。
<!-- Vue 场景演示:利用 <component :is="..."> 动态分发 -->
<template>
<div class="feed-list">
<!-- 如果 item.type 是 'video',就会使用 VideoCard 组件 -->
<component
v-for="item in feedList"
:is="cardStrategy[item.type]"
:data="item"
/>
</div>
</template>
<script setup>
import TextCard from './TextCard.vue';
import VideoCard from './VideoCard.vue';
import ImageCard from './ImageCard.vue';
// 这就是 UI 的策略字典
const cardStrategy = {
'text': TextCard,
'video': VideoCard,
'image': ImageCard
};
</script>
4. Axios 的 Adapter (适配器/策略)
在封装 HTTP 请求时,Axios 在浏览器端使用的是 XMLHttpRequest (或 fetch),在 Node.js 环境使用的是 http 模块。Axios 内部就是通过一种策略,根据当前环境动态选择加载哪一种请求适配器(Adapter),而对外暴露的 API(axios.get/post)始终保持一致。
总结
在前端开发中,“一切可以用对象映射(Map/Dictionary)解决的多重判断,本质上都是策略模式思想的体现”。
当你下次在写代码时发现准备敲下第三个 else if,或者看到别人留下的数十行 switch 时,请马上想到策略模式,把它重构成一个映射字典对象,你的代码质量将会有质的飞跃。