策略模式的定义是:定义一系列的算法(这些算法目标一致),把它们一个个封装起来,并且使它们可以相互替换。
比如要实现从上海到广州,既可以坐火车,又可以坐高铁,还可以坐飞机。这取决与个人想法。在这里,不同的到达方式就是不同的策略,个人想法就是条件。
1.计算奖金
以计算奖金为例,绩效为S的年终奖是4倍工资,绩效为A的年终奖是3倍工资,绩效为B的年终奖是2倍工资。那么这里奖金取决于两个条件,绩效和薪水。最初编码实现如下:
const calculateBonus = (performanceLevel, salary) => {
if(performanceLevel === 'S') {
return salary * 4;
}
if(performanceLevel === 'A') {
return salary * 3;
}
if(performanceLevel === 'B') {
return salary * 2;
}
}
复制代码
这段代码十分简单,但存在显而易见的缺点。
- if语句过多,需要涵盖所有的条件。
- 弹性差,如果新增绩效C,那么需要什么calculateBonus的内部实现去修改代码,不符合开放封闭原则。
- 复用性差,计算奖金的算法不能直接复用,除非复制粘贴。
2.使用策略模式
策略模式是指定义一系列的算法,并将它们封装起来,这很符合开闭原则。策略模式的目的就是将算法的使用和算法的实现分离出来。
一个基于策略模式的程序至少由两部分组成。第一个部分是策略类,它封装了具体的算法,并负责计算的具体过程。第二个部分是环境类Context,Context接受客户的请求,随后将请求委托给某一个策略类。要做到这点,Context中需要维持对某个策略对象的引用。
现在使用策略模式来重构以上代码,第一个版本是基于class,第二个版本是基于函数。
2.1基于class
class PerformanceS {
calculate(salary) {
return salary * 4;
}
}
class PerformanceA {
calculate(salary) {
return salary * 3;
}
}
class PerformanceB {
calculate(salary) {
return salary * 2;
}
}
class Bonus {
constructor(strategy, salary) {
this.strategy = strategy;
this.salary = salary;
}
getBonus() {
if(!this.strategy) {
return -1;
}
return this.strategy.calculate(this.salary);
}
}
const bonus = new Bonus(new PerformanceA(), 2000);
console.log(bonus.getBonus()) // 6000
复制代码
它没有上述的三个缺点。在这里,有三个策略类,分别是PerformanceS、Performance A、PerformanceB。这里的context就是Bonus类,它接受客户的请求(bonus.getBonus),将请求委托给策略类。它保存着策略类的引用。
2.2基于函数
上述中,每一个策略都是class,实际上,class也是一个函数。这里,可以直接用函数实现。
const strategies = {
'S': salary => salary * 4,
'A': salary => salary * 3,
'B': salary => salary * 2,
}
const getBonus = (performanceLevel, salary) => strategies[performanceLevel](salary)
console.log(getBonus('A', 2000)) // 6000
复制代码
3.多态在策略模式中的体现
通过使用策略模式重构代码,消除了程序中大片的条件语句。所有和奖金相关的逻辑都不在Context中,而是分布在各个策略对象中。Context并没有计算奖金的能力,当它接收到客户的请求时,它将请求委托给某个策略对象计算,计算方法被封装在策略对象内部。当我们发起“获得奖金”的请求时,Context将请求转发给策略类,策略类根据客户参数返回不同的内容,这正是对象多态性的体现,这也体现了策略模式的定义--“它们可以相互替换”。
4.计算小球的缓动动画
我们的目标是编写一个动画类和缓动算法,让小球以各种各样的缓动效果在页面中进行移动。
很明显,缓动算法是一个策略对象,它有几种不同的策略。这些策略函数都接受四个参数:动画开始的位置s、动画结束的位置e、动画已消耗的时间t、动画总时间d。
const tween = {
linear: (s, e, t, d) => { return e*t/d + s },
easeIn: () => { /* some code */ },
easeOut: () => { /* some code */ },
easeInOut: () => { /* some code */ },
}
复制代码
页面上有一个div元素。
<div id='div' style='position: absolute; left: 0;'></div>
复制代码
现在要让这个div动起来,需要编写一个动画类。
const tween = {
linear: () => { /* some code */ },
easeIn: () => { /* some code */ },
easeOut: () => { /* some code */ },
easeInOut: () => { /* some code */ },
}
class Animation {
constructor(dom) {
this.dom = dom;
this.startTime = 0;
this.startPos = 0;
this.endPos = 0;
this.propertyName = null;
this.easing = null;
this.duration = null;
}
// 开始动画
start(propertyName, endPos, duration, easing) {
this.startTime = Date.now();
// 初始化参数,省略其他
const self = this;
// 循环执行动画,如果动画已结束,那么清除定时器
let timer = setInterval(() => {
if(self.step() === false) {
clearInterval(timer);
}
}, 1000/60);
}
// 计算下一次循环到的时候小球位置
step() {
const now = Date.now();
if(now > this.startTime + this.duration) {
return false;
} else {
// 获得小球在本次循环结束时的位置并更新位置
// const pos = this.easing();
// this.update(pos);
}
}
update(pos) {
this.dom.style[propertyName] = pos + 'px';
}
}
复制代码
具体实现略去。这里的Animation类就是环境类Context,当接收到客户的请求(更新小球位置 self.step()),它将请求转发给策略内(this.easing()),策略类进行计算并返回结果。
5.更广义的“算法”
策略模式指的是定义一系列的算法,并且把他们封装起来。上述所说的计算奖金和缓动动画的例子都封装了一些策略方法。
从定义上看,策略模式就是用来封装算法的。但如果仅仅将策略模式用来封装算法,有些大材小用。在实际开发中,策略模式也可以用来封装一些的“业务规则”。只要这些业务规则目标一致,并且可以替换,那么就可以用策略模式来封装它们。以使用策略模式来完成表单校验为例。
6.表单校验
<form action='xxx' id='form' method='post'>
<input type='text' name='username'>
<input type='password' name='passsword'>
<button>提交</button>
</form>
复制代码
验证规则如下:
const form = document.querySelector('form')
form.onsubmit = () => {
if(form.username.value === '') {
alert('用户名不能为空')
return false;
}
if(form.password.value.length < 6) {
alert('密码不能少于6位')
return false;
}
}
复制代码
这是一种很常见的思路,和最开始计算奖金一样。缺点也是一样。
6.1使用策略模式重构表单校验
第一步需要把这些校验逻辑封装成策略对象。
const strategies = {
isNonEmpty: (value, errMsg) => {
if(value === '') {
return errMsg
}
},
minLength: (value, errMsg) => {
if(value.length < minLength) {
return errMsg
}
}
}
复制代码
第二步对表单进行校验。
class Validator {
constructor() {
this.rules = [];
}
add(dom, rule, errMsg) {
const arr = rule.split(':');
this.rules.push(() => {
const strategy = arr.shift();
arr.unshift(dom.value);
arr.push(errMsg);
return strategies[strategy].apply(dom, arr);
})
}
start() {
for(let i = 0, validatorFunc; validatorFunc = this.rules[i++];) {
let msg = validatorFunc();
if(msg) {
return msg;
}
}
}
}
const form = document.querySelector('form')
form.onsubmit = (e) => {
e.preventDefault();
const validator = new Validator();
validator.add(form.username, 'isNonEmpty', '用户名不能为空');
validator.add(form.password, 'minLength:6', '密码长度不能小于6位');
const errMsg = validator.start();
if(errMsg) {
alert(errMsg);
return false;
}
}
复制代码
上述例子中,校验逻辑是策略对象,其中包含策略的实现函数。Validator类是Context,用于将客户的请求(表单验证)转发到策略对象进行验证。与计算奖金的Bonus不同的是,这里并没有将验证参数通过构造函数传入,而是通过validator.add传入相关验证参数,通过validator.start()进行验证。
策略模式优缺点
- 策略模式利用组合、委托和多态等技术和思想,可以有效避免多重选择语句。
- 策略模式通过扩展策略类,对开放封闭原则完全支持,使得它们易于切换和扩展。
- 策略模式的算法可以用在其他地方,避免复制。
- 策略模式利用组合和委托让Context拥有执行算法的能力,这也是继承的一种更轻便的替代方案。
前三点正是开头实现的计算奖金函数的缺点。 策略模式有一点缺点,不过并不严重。
- 会增加策略类或者策略对象,增加了复杂度。但是与Context解耦了,这样更便于扩展。
- 使用策略模式,必须了解所有的策略以便选择合适的策略,这是strategies要向客户暴露它的所有实现,不符合最少知识原则。