JavaScript模式——策略模式:告别if-else地狱,2个案例让你“策略”在握
1. 策略模式是什么鬼?
简单说,策略模式就是帮你把一堆“算法”或者“规则”分别打包,让它们可以随时互换。就像你去旅行,可以选择飞机、火车、自驾,目的地一样,但方式随便换。这种模式的核心思想就是把“做什么”和“怎么做”分开。
比如,你要计算年终奖,绩效S和绩效A的算法不一样,但它们都是“计算奖金”这件事。策略模式就让你把这些算法一个个封装好,想用哪个就用哪个。
2. 策略模式长啥样?
策略模式通常有两部分:
- 策略对象:负责具体干活,比如不同的奖金算法、不同的动画缓动公式。
- 环境对象:负责接收命令,然后把活派给某个策略。
听起来抽象?没关系,下面两个例子保你一看就懂。
3. 案例一:年终奖怎么算?
3.1 新手写法:if-else堆成山
刚入行的程序员可能会这么写:
var calculateBonus = function(performanceLevel, salary) { if (performanceLevel === 'S') { return salary * 4; } if (performanceLevel === 'A') { return salary * 3; } if (performanceLevel === 'B') { return salary * 2; } }; calculateBonus('B', 20000); // 40000 calculateBonus('S', 6000); // 24000这段代码简单粗暴,但问题也很明显:
- 函数越来越胖,加个绩效C就得往里塞代码。
- 算法写死在函数里,别的项目想用?只能复制粘贴。
- 哪天绩效S改成5倍工资?得钻进这个函数里改,一不小心还可能影响别的逻辑。
3.2 稍微进步:函数拆分
把每个算法单独拎出来,稍微好看一点:
var performanceS = function(salary) { return salary * 4; }; var performanceA = function(salary) { return salary * 3; }; var performanceB = function(salary) { return salary * 2; }; var calculateBonus = function(performanceLevel, salary) { if (performanceLevel === 'S') return performanceS(salary); if (performanceLevel === 'A') return performanceA(salary); if (performanceLevel === 'B') return performanceB(salary); };可那个if-else还是赖在那儿不走。
3.3 面向对象版:策略模式登场
我们把每种绩效包装成一个“策略类”:
var performanceS = function() {}; performanceS.prototype.calculate = function(salary) { return salary * 4; }; var performanceA = function() {}; performanceA.prototype.calculate = function(salary) { return salary * 3; }; var performanceB = function() {}; performanceB.prototype.calculate = function(salary) { return salary * 2; }; // 奖金类(环境角色) var Bonus = function() { this.salary = null; this.strategy = null; }; Bonus.prototype.setSalary = function(salary) { this.salary = salary; }; Bonus.prototype.setStrategy = function(strategy) { this.strategy = strategy; }; Bonus.prototype.getBonus = function() { return this.strategy.calculate(this.salary); }; // 用起来 var bonus = new Bonus(); bonus.setSalary(10000); bonus.setStrategy(new performanceS()); console.log(bonus.getBonus()); // 40000 bonus.setStrategy(new performanceA()); console.log(bonus.getBonus()); // 30000现在,想加绩效C,只需要新建一个performanceC类,根本不用碰原来的代码——完美符合开放-封闭原则。
3.4 JavaScript精简版:函数当策略
JavaScript可是函数一等公民啊,何必非得用类?直接把策略写成函数,塞进一个对象里,多清爽:
var strategies = { S: function(salary) { return salary * 4; }, A: function(salary) { return salary * 3; }, B: function(salary) { return salary * 2; } }; var calculateBonus = function(level, salary) { return strategies[level](salary); }; console.log(calculateBonus('S', 20000)); // 80000 console.log(calculateBonus('A', 10000)); // 30000你看,没了那些啰嗦的类,代码干净得像刚洗过的盘子。
4. 多态:策略模式的隐形翅膀
策略模式之所以这么灵活,靠的是多态。不管是什么策略,只要它们有相同的“接口”(比如都有一个calculate方法),环境对象就能一视同仁地调用。在JavaScript里,甚至不需要显式的接口,函数参数对了就行。
比如下面的动画例子,所有缓动函数都接收(t, b, c, d)四个参数,环境类只管调用,至于具体怎么算,那是策略自己的事。这就是多态的魅力——对象们虽然长得不一样,但说起话来都一个调调。
5. 案例二:让小球动起来——缓动动画
5.1 动画原理
动画其实就是快速播放一系列画面,每秒几十张,眼睛就被骗了。在网页里,我们用定时器一点点改变元素的位置,每一帧算好新的坐标,元素就“动”起来了。
5.2 缓动算法:各种运动配方
var tween = { linear: function(t, b, c, d) { return c * t / d + b; }, easeIn: function(t, b, c, d) { return c * (t /= d) * t + b; }, strongEaseIn: function(t, b, c, d) { return c * (t /= d) * t * t * t * t + b; }, strongEaseOut: function(t, b, c, d) { return c * ((t = t / d - 1) * t * t * t * t + 1) + b; }, sineaseIn: function(t, b, c, d) { return c * (t /= d) * t * t + b; }, sineaseOut: function(t, b, c, d) { return c * ((t = t / d - 1) * t * t + 1) + b; } };5.3 动画类:用策略模式控制节奏
我们写一个Animate类,把缓动算法当作策略注入,想用什么运动,就传什么算法名:
var Animate = function(dom) { this.dom = dom; // 要运动的元素 this.startTime = 0; this.startPos = 0; this.endPos = 0; this.propertyName = null; // 比如 'left', 'top' this.easing = null; this.duration = null; }; Animate.prototype.start = function(propertyName, endPos, duration, easing) { this.startTime = +new Date(); this.startPos = this.dom.getBoundingClientRect()[propertyName]; this.propertyName = propertyName; this.endPos = endPos; this.duration = duration; this.easing = tween[easing]; var self = this; var timeId = setInterval(function() { if (self.step() === false) { clearInterval(timeId); } }, 19); }; Animate.prototype.step = function() { var t = +new Date(); if (t >= this.startTime + this.duration) { this.update(this.endPos); return false; } var pos = this.easing( t - this.startTime, this.startPos, this.endPos - this.startPos, this.duration ); this.update(pos); return true; }; Animate.prototype.update = function(pos) { this.dom.style[this.propertyName] = pos + 'px'; };使用的时候,想怎么动就怎么动:
var div = document.getElementById('div'); var animate = new Animate(div); animate.start('left', 500, 1000, 'strongEaseOut'); // 缓慢加速然后冲出?只需换一个算法名字,小球的运动轨迹就完全变了——策略模式让你轻松切换“运动配方”。
6. 策略模式的优点和缺点
优点:
- 干掉一堆if-else,代码瞬间清爽。
- 算法独立,扩展方便,想加新规则?写个新策略就行。
- 算法复用,哪里需要哪里搬。
- 组合和委托代替继承,更灵活。
缺点:
- 多了不少策略类或对象,文件数量增加。
- 使用者得知道有哪些策略可选,不然不知道怎么配。
7. JavaScript的一等函数:策略模式隐身了
在JavaScript里,函数本身就是对象,可以到处传。所以策略模式常常简化到你都认不出来:
var S = function(salary) { return salary * 4; }; var A = function(salary) { return salary * 3; }; var B = function(salary) { return salary * 2; }; var calculateBonus = function(func, salary) { return func(salary); }; calculateBonus(S, 10000); // 40000你看,策略模式已经“隐形”了,但思想还在——函数即策略。
8. 总结
策略模式不是什么高深莫测的东西,它就是把“算法”或“规则”抽出来单独管理,让主程序只关心“做什么”,不操心“怎么做”。通过两个例子(年终奖、缓动动画),你应该已经感受到它的魅力了。
在JavaScript里,因为函数是一等公民,策略模式往往变得非常轻盈,甚至感觉不到它的存在。但不管形式如何,其核心思想——分离变化,封装行为——依然是我们写出好代码的指路明灯。