JavaScript设计模式(策略模式)
策略模式的定义是:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。将不变的部分和变化的部分隔开是每个设计模式的主题,策略模式也不例外,策略模式的目的就是将算法的使用与算法的实现分离开来。
一、最初的策略模式:
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
或者使用组合函数封装改善下代码
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 ); } } calculateBonus( 'A' , 10000 ); // 输出:30000
但是,这种改善非常有限,calculateBonus 函数有可能越来越庞大,而且在系统变化的时候缺乏弹性。
二、JavaScript 版本的策略模式:
在 JavaScript 语言中,函数也是对象,所以更简单和直接的做法是把 strategy 直接定义为函数
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
三、使用策略模式重构表单:
1、初始的表单验证的写法:
以上这种写法有一些缺点:
(1)registerForm.onsubmit 函数比较庞大,包含了很多 if-else 语句,这些语句需要覆盖所有的校验规则。
(2)registerForm.onsubmit 函数缺乏弹性,如果增加了一种新的校验规则,或者想把密码的长 度校验从 6 改成 8,我们都必须深入 registerForm.onsubmit 函数的内部实现,这是违反开放—封闭原则的。
(3)算法的复用性差,如果在程序中增加了另外一个表单,这个表单也需要进行一些类似的 校验,那我们很可能将这些校验逻辑复制得漫天遍野。
2、使用策略模式重构之后的代码:
(1)第一步我们要把这些校验逻辑都封装成策略对象
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; // 手机号码格式 } } };
(2)实现 Validator 类。Validator 类在这里作为 Context,负责接收用户的请求并委托给 strategy 对象。
var Validator = function(){ this.cache = []; // 保存校验规则 }; Validator.prototype.add = function(dom, rule, errorMsg ) { var ary = rule.split( ':' ); // 把strategy和参数分开 this.cache.push(function(){ // 把校验的步骤用空函数包装起来,并且放入 cache var strategy = ary.shift(); // 用户挑选的 strategy ary.unshift( dom.value ); // 把 input 的 value 添加进参数列表 ary.push( errorMsg ); // 把 errorMsg 添加进参数列表 return strategies[strategy].apply( dom, ary ); }); }; Validator.prototype.start = function(){ for ( var i = 0, validatorFunc; validatorFunc = this.cache[ i++ ]; ){ var msg = validatorFunc(); // 开始校验,并取得校验后的返回信息 if ( msg ){ if ( msg ){ return msg; // 如果有确切的返回值,说明校验没有通过 } } };
(3)用户使用代码:
var validataFunc = function(){ var validator = new Validator(); // 创建一个 validator 对象 /***************添加一些校验规则****************/ validator.add( registerForm.userName, 'isNonEmpty', '用户名不能为空' ); validator.add( registerForm.password, 'minLength:6', '密码长度不能少于 6 位' ); validator.add( registerForm.phoneNumber, 'isMobile', '手机号码格式不正确' ); var errorMsg = validator.start(); // 获得校验结果 return errorMsg; // 返回校验结果 } var registerForm = document.getElementById( 'registerForm' ); registerForm.onsubmit = function(){ var errorMsg = validataFunc(); // 如果errorMsg有确切的返回值,说明未通过校验 if ( errorMsg ){ alert ( errorMsg ); return false; // 阻止表单提交 } } };
使用策略模式重构代码之后,我们仅仅通过“配置”的方式就可以完成一个表单的校验, 这些校验规则也可以复用在程序的任何地方,还能作为插件的形式,方便地被移植到其他项目中。
在修改某个校验规则的时候,只需要编写或者改写少量的代码。比如我们想将用户名输入框 的校验规则改成用户名不能少于 4 个字符。可以看到,这时候的修改是毫不费力的。代码如下:
validator.add( registerForm.userName, 'isNonEmpty', '用户名不能为空' ); // 改成:
validator.add( registerForm.userName, 'minLength:10', '用户名长度不能小于 10 位' );
四、给某个文本输入框添加多种校验规则
最后,如果去掉 strategies,我们还能认出这是一个策略模式的实现吗? 代码如下:
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