一、定义
策略模式的定义是:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。俗话说,条条大路通罗马。在程序设计中,我们也常常遇到类似的情况,要实现某一个功能有多种方案可以选择。比如一个压缩文件的程序,既可以选择zip算法,也可以选择 gzip 算法。
以公司发年终奖为例:很多公司的年终奖是根据员工的工资基数和年底绩效情况来发放的。例如,绩效为 S 的人年 终奖有 4 倍工资,绩效为 A的人年终奖有3倍工资,而绩效为 B 的人年终奖是 2 倍工资。假设财务部要求我们提供一段代码,来方便他们计算员工的年终奖。
2.1 普通实现(bad)
// 普通实现
function calcBonus({level, salary}) {
if (level === 'S') {
return salary * level * 4;
}
if (level === 'A') {
return salary * level * 3;
}
if (level === 'B') {
return salary * level * 2;
}
}
这个函数的缺点, 1. 如果情况复杂的话可能会有多个if else; 2. 缺乏弹性, 如果需要 增加一个新的level C 并且把S的level调成5 需要深入代码逻辑, 才能修改, 3. 代码无法复用
2.2 使用策略模式改进
const strategies = {
'S': (salary) => salary * 4,
'A': (salary) => salary * 3,
'B': (salary) => salay * 2,
}
const calculateBonus = ({level, salary}) => strategies[level](salary);
const t1 = calculateBonus({ level: 'S', salary: 20000 });
console.log('t1 => ',t1);
通过使用策略模式重构代码,我们消除了原程序中大片的条件分支语句。所有跟计算奖金有 关的逻辑不再放在 Context 中,而是分布在各个策略对象中。Context 并没有计算奖金的能力,而是把这个职责委托给了某个策略对象。每个策略对象负责的算法已被各自封装在对象内部。当我们对这些策略对象发出“计算奖金”的请求时,它们会返回各自不同的计算结果,这正是对象多态性的体现,也是“它们可以相互替换”的目的。替换 Context 中当前保存的策略对象,便能执行不同的算法来得到我们想要的结果。
2.3 策略模式的应用(以表单校验为例)
// 数据结构
[{
strategy: 'minLength:6',
errorMsg: '用户名长度不能小于 10 位',
}]
基本代码实现
import isString from 'lodash/isString';
// 策略对象
const strategies = {
isNonEmpty: (value, errMsg) => {
if (value === '') {
return errMsg;
}
},
minLength: (value, length, errMsg) => {
if (val.length < length) {
return errMsg;
}
},
isMobil: (value, errMsg) => {
var pattern = /^1[3|5|8][0-9]{9}$/ig;
if (!pattern.text(value)) {
return errMsg;
}
},
};
// 校验器对象
function Validator() {
this.cache = []; // 保存校验的函数
}
Validator.prototype.add = function(dom, rules) {
const self = this;
for(let i=0; i<rules.length; i++) {
const rule = rules[i];
const { strategy, errorMsg } = rule;
const strategyArr = isString(strategy) && strategy.spilt(':');
self.cache.push((() => {
const strategy = strategyArr.shift();
strategyArr.unshift(dom.value);
strategyArr.push(errorMsg);
return strategies[strategy].call(dom, ...strategyArr);
}))
}
}
Validator.prototype.valid = function() {
let errorMsg;
this.cache.every((item, index) => {
const result = item();
if (result) {
errorMsg = result;
return false;
}
});
return errorMsg;
}
客户端调用方式
// 客户端调用方式
const userInput = document.getElementById('userInput');
const form = document.getElementById('form');
function validForm() {
const v1 = new Validator();
v1.add( userInput, [
{
strategy: 'isNonEmpty',
errorMsg: '用户名不能为空'
}, {
strategy: 'minLength:6',
errorMsg: '用户名长度不能小于 10 位'
}]
);
const errorMsg = v1.valid();
return errorMsg;
}
form.onsubmit = function() {
const errorMsg = validForm();
if (errorMsg) {
alert(errorMsg);
return false;
}
}
2.4 代码改进
// 校验器对象代码改进
class Validator {
constructor() {
this.cache = []; // 保存校验的函数
}
add = (dom, rules) => {
for(let i=0; i<rules.length; i++) {
const rule = rules[i];
const { strategy, errorMsg } = rule;
const strategyArr = isString(strategy) && strategy.spilt(':');
this.cache.push((() => {
const strategy = strategyArr.shift();
strategyArr.unshift(dom.value);
strategyArr.push(errorMsg);
return strategies[strategy].call(dom, ...strategyArr);
}))
}
}
valid = () => {
let errorMsg;
this.cache.every((item, index) => {
const result = item();
if (result) {
errorMsg = result;
return false;
}
});
return errorMsg;
}
};
二、总结
策略模式是一种常用且有效的设计模式,也具有优点和缺点
优点
- 策略模式利用组合、委托和多态等技术和思想,可以有效地避免多重条件选择语句。
- 策略模式提供了对开放—封闭原则的完美支持,将算法封装在独立的 strategy 中,使得它们易于切换,易于理解,易于扩展。
- 可复用
缺点
- 要使用策略模式,必须了解所有的 strategy,必须了解各个 strategy 之间的不同点, 这样才能选择一个合适的 strategy。
- 会增加策略类或者策略对象