策略模式的定义是: 定义一系列的算法, 把它们一个个封装起来, 并且使它们可以相互替换.
在现实中, 如果我们想去某个地方旅游, 可以根据实际情况有多种路线
如果没有时间但是不在乎钱, 可以选择飞机
如果没有钱, 可以选择大巴活着火车
如果再穷一点, 可以选择骑自行车
使用策略模式计算奖金
现在以年终奖的计算为例
公司年终奖根据员工的工资基数和年底绩效来发放
绩效 S, 四倍年终奖
绩效 A, 三倍年终奖
绩效 B, 二倍年终奖
最初的实现
- 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', 2000) // 4000
- calculateBonus('S', 2000) // 8000
这段代码简单, 但是存在显而易见的缺点
函数比较庞大, 包含很多 if-else 语句, 这些语句需要覆盖所有的逻辑分支
缺乏弹性, 如果想新增绩效 C, 就得深入函数内部实现, 违反开放 - 封闭原则
算法的复用性差
策略模式的实现
- 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', 2000)) // 8000
- console.log(calculateBonus('B', 2000)) // 4000
通过使用策略模式重构代码, 消除来原程序中分支语句. 所有计算奖金有关的逻辑分布在策略对象中, 每个策略对象的算法已被各自封装在对象内部, 当我们对这些策略对象发出 "计算奖金" 的请求时, 它们会返回各自的计算结果, 这不仅是多态性的体现, 也是 "自由交换" 的目的.
使用策略模式实现缓动动画
- <!DOCTYPE html>
- <HTML>
- <head>
- <meta charset="utf-8" />
- <meta name="viewport" content="width=device-width, initial-scale=1">
- <title>
- </title>
- </head>
- <body>
- <div id="div" style="background: #141527;display: inline-block;position: relative;">
- 我说 div
- </div>
- <script type="text/javascript">
- /**
- * 缓动算法
- * @t 已消耗的时间
- * @b 小球原始位置
- * @c 小球目标位置
- * @d 动画持续的总时间
- */
- 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;
- },
- easeOut: function(t, b, c, d) {
- return - c * (t /= d) * (t - 2) + b;
- },
- easeInOut: function(t, b, c, d) {
- if ((t /= d / 2) < 1) return c / 2 * t * t + b;
- return - c / 2 * ((--t) * (t - 2) - 1) + b;
- }
- }
- /**
- * 定义 Animate 类
- */
- var Animate = function(dom) {
- this.dom = dom; // 运动的元素
- this.startTime = 0; // 动画开始时间
- this.startPos = 0; // 元素初始位置
- this.endPos = 0; // 元素结束位置
- this.propertyName = ''; // 实现动画的元素属性
- this.easing = null; // 缓动算法
- this.duration = 0; // 动画持续的时间
- };
- /**
- * 启动动画
- */
- 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 that = this;
- var timed = setInterval(function() {
- // 执行每帧操作
- if (that.step() === false) { // 动画已结束
- clearInterval(timed);
- }
- },
- 19);
- };
- // 判断当前动画状态, 调用 update
- Animate.prototype.step = function() {
- var t = new Date(); // 执行动画的当前时间
- if (t.getTime() >= this.startTime.getTime() + 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);
- };
- // 计算位置更新属性
- Animate.prototype.update = function(pos) {
- this.dom.style[this.propertyName] = pos + 'px';
- };
- var div = document.getElementById('div') var animate = new Animate(div) animate.start('top', 500, 1000, 'easeInOut')
- </script>
- </body>
- </HTML>
用策略模式实现表单验证
从定义上看, 策略模式就是用来封装算法的. 但是如果仅仅用来封装算法, 未免有点大材小用. 在实际业务中, 策略模式也可以用来封装一系列的 "业务规则". 只要业务规则指向的目标一致, 并且可以被替换使用, 我们就可以用策略模式来封装它们.
普通版本的表单验证
- <!DOCTYPE HTML>
- <HTML>
- <head>
- <meta charset="utf-8" />
- <meta name="viewport" content="width=device-width, initial-scale=1">
- <title></title>
- </head>
- <body>
- <form action="http://xxx.com/register" id="registerForm" method="post">
请输入用户名 < input type="text" name="username"/><br />
请输入密码 < input type="text" name="password"/><br />
请输入手机号 < input type="text" name="phonenumber"/><br />
- <input type="submit" value="提交" style="padding: 10px 20px;">
- </form>
- <script type="text/javascript">
- var registerForm = document.getElementById('registerForm')
- registerForm.onsubmit = function() {
- if (registerForm.username.value === '') {
- alert('用户名不能为空')
- return false
- }
- if (registerForm.password.value.length <6) {
- alert('密码长度不能小于 6 位')
- return false
- }
- if (!/(^1[3|5|8][0-9]{9}$)/.test(registerForm.phonenumber.value) ){
- alert('手机号码格式不正确')
- return false
- }
- }
- </script>
- </body>
- </HTML>
这是一种很常见的编码方式, 可以看到缺点和计算奖金一摸一样
用策略模式重构表单验证
很明显第一步我们需要将验证逻辑封装成策略对象
- var strategies = {
- isNonEmpty: function(value, errorMsg) {
- if (value === '') {
- return errorMsg
- }
- },
- minLength: function(value, length, errorMsg) {
- if (value.length < length) {
- return errorMsg
- }
- },
- isMobile: function(value, errorMsg) {
- if (!/(^1[3|5|8][0-9]{9}$)/.test(value)) {
- return errorMsg
- }
- }
- }
接下来实现 Validator 类, 负责接受用户的请求并委托给 strategies
- var Validator = function() {
- // 保存校验规则
- this.cache = []
- }
- // 添加校验
- Validator.prototype.add = function(dom, rules) {
- var self = this
- // 遍历校验规则
- for(var i = 0, rule; rule = rules[i++];) {
- (function(rule){
- // 把 strategy 和参数分开
- var strategyAry = rule.strategy.split(':')
- var errorMsg = rule.errorMsg
- // 把校验的步骤用空函数包装起来, 并且放入 cache
- self.cache.push(function(){
- // 挑选出校验规则
- var strategy = strategyAry.shift()
- // 把 input 的 value 添加进参数列表
- strategyAry.unshift(dom.value)
- // 把 errorMsg 添加进参数列表
- strategyAry.push(errorMsg)
- return strategies[strategy].apply(dom, strategyAry)
- })
- })(rule)
- }
- }
- // 启动校验
- Validator.prototype.start = function() {
- for (var i = 0, validatorFunc; validatorFunc = this.cache[i++];) {
- // 开始校验, 并取得校验后的结果
- var errorMsg = validatorFunc()
- if (errorMsg) {
- return errorMsg
- }
- }
- }
接下来就是调用了
- var registerForm = document.getElementById('registerForm')
- var validataFunc = function() {
- var validator = new Validator()
- validator.add(registerForm.username, [
- {
- strategy: 'isNonEmpty',
- errorMsg: '用户名不能为空'
- },
- {
- strategy: 'minLength:10',
- errorMsg: '用户名长度不能小于 10 位'
- }
- ]
- )
- validator.add(registerForm.password, [
- {
- strategy: 'minLength:6',
- errorMsg: '密码长度不能小于 6 位'
- }
- ]
- )
- validator.add(registerForm.phonenumber, [
- {
- strategy: 'isMobile',
- errorMsg: '手机号码格式不正确'
- }
- ]
- )
- var errorMsg = validator.start()
- return errorMsg
- }
- var sub = document.querySelector('input[type="submit"]')
- sub.onclick = function() {
- var errorMsg = validataFunc()
- if (errorMsg) {
- console.error(errorMsg)
- return false
- }
- }
使用策略模式重构代码之后, 我们不仅通过 "配置" 的方式就可以完成一个表单的校验, 这些规则也可以复用在程序的任何地方, 还能以插件的形式, 方便地移植到其他项目中. 并且新增或者修改规则也是毫不费力的.
策略模式的优缺点
优点
策略模式利用组合, 委托和多态等技术思想, 可以有效避免多重条件选择语句.
策略模式提供了对开放 - 封闭原则的完美支持. 将算法封装在独立的 strategy 中, 使得它们易于切换, 易于理解, 易于扩展.
策略模式中的算法也可以复用在系统中的其他地方.
在策略模式中利用组合和委托让 Content 拥有执行算法的能力, 这也是继承的一种更轻便的替代方案.
缺点
使用策略对象会增加很多策略类或者策略对象, 但实际上比把这些逻辑放在 Content 更好.
策略模式会向用户暴露所有实现细节, 这其实是违反最少知识原则.
来源: https://juejin.im/post/5be69894e51d4549fb347b27