[JS设计模式]:策略模式及应用-计算奖金、表单验证的实现(5)

  • A+
所属分类:Web前端
摘要

策略模式的意义是定义一系列的算法,把它们一个个封装起来,并且使它们可相互替换。此模式让算法的变化不会影响到使用算法的客户。


介绍

策略模式的意义是定义一系列的算法,把它们一个个封装起来,并且使它们可相互替换。此模式让算法的变化不会影响到使用算法的客户。

实现

举一个例子,比如我们做数据合法性校验,一般是通过swich来实现,或者通过if语句来实现,如果校验规则多了的话,那么代码的扩展性和维护性就很差了,而且进行单元测试就越来越复杂,代码如下:

var validator = {     validate: function(value,type) {         switch(type) {             case 'isNonEmpty':                 return true             case 'isNumber':                 return true;             case 'isAlphaNum':                 return true;             default:                 return true;         }     } }  alert(validator.validate('123','isNonEmpty'))

怎么避免上面代码的弊端呢,我们可以使用策略模式把相同的工作代码封装成不同的验证类,我们只需要通过传递不同的名称来调用不同的验证类方法(也即是不同的算法),实现代码如下:

var validator = {     types: { // 存放验证规则         isNonEmpty: {             validate: function(value) {                 return value !== ''             },             instructions: '传入的值不能为空'         },         isNumber: {             validate: function(value) {                 return !isNaN(value);             },             instructions: '传入的值不是数字'         },         isAlphaNum: {             validate: function(value) {                 return !/[^a-z0-9]/i.test(value)             },             instructions: '传入的值只能是数字或者字母,不能是特殊字符'         }     },      config: {}, // 需要验证的类型     messages: [], // 存放错误信息     validate: function(data) { // 传入的data 为key-value的键值对         var i, type, checker, resultOk;         this.messages = []; // 首先清空错误信息         for(i in data) {             if(data.hasOwnProperty(i)) { // 判断i不是原型上的属性                 type = this.config[i]; // 获取验证类型                 if(!type) { // 没有当前校验类型直接跳过(不需要验证的)                     continue;                 }                 checker = this.types[type]; // 获取验证规则的验证类方法                 if(!checker) { // 验证规则类不存在 直接抛出异常                     throw {                         name: "ValidationError",                         message: "No handler to validate type " + type                     }                 }                 resultOk = checker.validate(data[i]);                 if(!resultOk) { // 验证不通过                     this.messages.push(checker.instructions);                 }             }         }         return this.hasErrors();     },     hasErrors: function() {         return this.messages.length !== 0;     }      }

使用方式如下:

var data = {     firstName: '',     lasName: 'shu',     age: '',     userName: 'tom shu' } validator.config = {     firstName: 'isNonEmpty',     age: 'isNumber',     userName: 'isAlphaNum' }  validator.validate(data); if(validator.hasErrors()) {     console.log(validator.messages.join("n")); } // 结果: // 传入的值不能为空 // 传入的值只能是数字或者字母,不能是特殊字符

 其它策略模式示例

jquery中使用的animate方法

$( div ).animate( {"left: 200px"}, 1000, 'linear' );  //匀速运动 $( div ).animate( {"left: 200px"}, 1000, 'cubic' );  //三次方的缓动

这 2 句代码都是让 div 在 1000ms 内往右移动 200 个像素. linear(匀速) 和 cubic(三次方缓动) 就是一种策略模式的封装.

计算奖金

比如公司的年终奖是根据员工的工资和绩效来考核的,绩效为A的人,年终奖为工资的4倍,绩效为B的人,年终奖为工资的3倍,绩效为C的人,年终奖为工资的2倍;现在我们使用一般的编码方式会如下这样编写代码:

var calculateBouns = function(salary,level) {     if(level === 'A') {         return salary * 4;     }     if(level === 'B') {         return salary * 3;     }     if(level === 'C') {         return salary * 2;     } }; // 调用如下: console.log(calculateBouns(4000,'A')); // 16000 console.log(calculateBouns(2500,'B')); // 7500

缺点:函数含有很多if语句,缺乏弹性,算法复用性差,如果其它地方有类似的算法,但是规则不一样,这些代码不能通用。

使用策略模式代码如下:

//代码如下: var obj = {         "A": function(salary) {             return salary * 4;         },         "B" : function(salary) {             return salary * 3;         },         "C" : function(salary) {             return salary * 2;         }  }; var calculateBouns =function(level,salary) {     return obj[level](salary); }; console.log(calculateBouns('A',10000)); // 40000

策略模式不仅仅只封装算法,我们还可以对用来封装一系列的业务规则,只要这些业务规则目标一致,我们就可以使用策略模式来封装它们

表单校验

比如常见的就是注册页面,需要对用户名,密码,手机号等进行规则校验,验证规则如下:

  • 用户名不能为空;
  • 密码不能小于6位;
  • 手机号码符合手机正则规则

HTML代码如下:

<form action="" id="registerForm" method="post" onsubmit="return submitValidate()">     <p>         <label>请输入用户名:</label>         <input type="text" name="userName" />     </p>     <p>         <label>请输入密码:</label>         <input type="text" name="password" />     </p>     <p>         <label>请输入手机号码:</label>         <input type="text" name="phoneNumber" />     </p>     <div>         <button type="submit">提交</button>     </div> </form>

submitValidate验证方法如下:

function submitValidate() {     var registerForm = document.getElementById("registerForm");     if(registerForm.userName.value === '') {             alert('用户名不能为空');             return false;         }     else if(registerForm.password.value.length < 6) {         alert("密码的长度不能小于6位");         return false;     }     else if(!/(^1[0-9]{10}$)/.test(registerForm.phoneNumber.value)) {         alert("手机号码格式不正确");         return false;     }     return true; }

 缺点:

  • submitValidate函数中的if else-if代码会根据验证项而逐渐变大;
  • submitValidate函数缺乏弹性,如果添加新的校验规则是添加else-if语句,但是如果是修改原来的验证规则,那么就需要改函数内的代码,违反开放-封闭原则;
  • 算法的复用性差,如果其它页面也需要用类似的校验,那么这个方法就不能共用了,可以又是复制代码。

 下面我们使用策略模式来重构上面的代码。

第一步,封装策略对象,也即是验证的不同算法,代码如下:

var strategys = {     isNotEmpty: function(value,errorMsg) {         if(value === '') {             return errorMsg;         }     },     // 限制最小长度     minLength: function(value,length,errorMsg) {         if(value.length < length) {             return errorMsg;         }     },     // 手机号格式     mobileFormat: function(value,errorMsg) {         if(!/(^1[0-9]{10}$)/.test(value)) {             return errorMsg;         }     } }

第二步,实现Validator类,Validator类在这里作为Context,负责接收用户的请求并委托给strategy 对象。

通俗的话就是添加表单中需要验证的一些规则以及获取验证结果(是否验证通过),如下代码:

function Validator() {     this.cache = []; // 保存效验规则 }  Validator.prototype = {     constructor: Validator,     add: function(dom,rule,errorMsg) {         var str = rule.split(":"); // minLength:6的场景         var fn = function() {             var strategyType = str.shift(); // 删除str数组中的第一个元素并返回,即是获取校验规则的函数名             str.unshift(dom.value); // 往数组str的第一位插入value值             str.push(errorMsg);             return strategys[strategyType].apply(dom,str);         }         this.cache.push(fn);     },     start: function() {         for(var i = 0, fn; fn = this.cache[i++];) {             var msg = fn();             if(msg) {                 return msg;             }         }     } } 

调用方式:

function submitValidate() {     var registerForm = document.getElementById("registerForm");     var validator = new Validator();     validator.add(registerForm.userName,'isNotEmpty', '用户名不能为空');     validator.add(registerForm.password,'minLength:6', '密码的长度不能小于6位');     validator.add(registerForm.phoneNumber,'mobileFormat', '手机号码格式不正确');     var resultMsg = validator.start();     if(resultMsg) {         alert(resultMsg);         return false;     }     return true; }

 完整的JS代码:

var strategys = {     isNotEmpty: function(value,errorMsg) {         if(value === '') {             return errorMsg;         }     },     // 限制最小长度     minLength: function(value,length,errorMsg) {         if(value.length < length) {             return errorMsg;         }     },     // 手机号格式     mobileFormat: function(value,errorMsg) {         if(!/(^1[0-9]{10}$)/.test(value)) {             return errorMsg;         }     } }  function Validator() {     this.cache = []; // 保存效验规则 }  Validator.prototype = {     constructor: Validator,     add: function(dom,rule,errorMsg) {         var str = rule.split(":"); // minLength:6的场景         var fn = function() {             var strategyType = str.shift(); // 删除str数组中的第一个元素并返回,即是获取校验规则的函数名             str.unshift(dom.value); // 往数组str的第一位插入value值             str.push(errorMsg);             return strategys[strategyType].apply(dom,str);         }         this.cache.push(fn);     },     start: function() {         for(var i = 0, fn; fn = this.cache[i++];) {             var msg = fn();             if(msg) {                 return msg;             }         }     } }   function submitValidate() {     var registerForm = document.getElementById("registerForm");     var validator = new Validator();     validator.add(registerForm.userName,'isNotEmpty', '用户名不能为空');     validator.add(registerForm.password,'minLength:6', '密码的长度不能小于6位');     validator.add(registerForm.phoneNumber,'mobileFormat', '手机号码格式不正确');     var resultMsg = validator.start();     if(resultMsg) {         alert(resultMsg);         return false;     }     return true; }

 以上代码我们只实现了给一个dom元素绑定一条验证规则,那如果需要绑定多条验证规则呢?

比如上面的代码我们只能效验输入框是否为空,validator.add(registerForm.userName,'isNotEmpty','用户名不能为空');但是如果我们既要效验输入框是否为空,还要效验输入框的长度不要小于10位的话,那么我们期望需要像如下传递参数:

validator.add(registerForm.userName,[{strategy:'isNotEmpty',errorMsg:'用户名不能为空'},{strategy: 'minLength:10',errorMsg:'用户名长度不能小于10位'}])

 我们只需要修改一下add方法即可,如下代码:

function Validator() {     this.cache = []; // 保存效验规则 }  Validator.prototype = {     constructor: Validator,     add: function(dom,rules) {         var self = this;         for(var i = 0, len = rules.length; i < len; i++) {             var rule = rules[i];             (function(rule){                 var str = rule.strategy.split(":"); // minLength:6的场景                 var fn = function() {                     var strategyType = str.shift(); // 删除str数组中的第一个元素并返回,即是获取校验规则的函数名                     str.unshift(dom.value); // 往数组str的第一位插入value值                     str.push(rule.errorMsg);                     return strategys[strategyType].apply(dom,str);                 }                 self.cache.push(fn);             })(rule)         }     },     start: function() {         for(var i = 0, fn; fn = this.cache[i++];) {             var msg = fn();             if(msg) {                 return msg;             }         }     } } 

调用方式改变一下:

function submitValidate() {     var registerForm = document.getElementById("registerForm");     var validator = new Validator();     validator.add(registerForm.userName, [{         strategy: 'isNotEmpty',         errorMsg: '用户名不能为空'     }, {         strategy: 'minLength:10',         errorMsg: '用户名长度不能小于10位'     }]);     validator.add(registerForm.password, [{         strategy: 'minLength:6',         errorMsg: '密码的长度不能小于6位'     }]);     validator.add(registerForm.phoneNumber, [{         strategy: 'mobileFormat',         errorMsg: '手机号码格式不正确'     }]);     var resultMsg = validator.start();     if (resultMsg) {         alert(resultMsg);         return false;     }     return true; }

完整的代码如下:

var strategys = {     isNotEmpty: function(value, errorMsg) {         if (value === '') {             return errorMsg;         }     },     // 限制最小长度     minLength: function(value, length, errorMsg) {         if (value.length < length) {             return errorMsg;         }     },     // 手机号格式     mobileFormat: function(value, errorMsg) {         if (!/(^1[0-9]{10}$)/.test(value)) {             return errorMsg;         }     } }  function Validator() {     this.cache = []; // 保存效验规则 }  Validator.prototype = {     constructor: Validator,     add: function(dom, rules) {         var self = this;         for (var i = 0, len = rules.length; i < len; i++) {             var rule = rules[i];             (function(rule) {                 var str = rule.strategy.split(":"); // minLength:6的场景                 var fn = function() {                     var strategyType = str.shift(); // 删除str数组中的第一个元素并返回,即是获取校验规则的函数名                     str.unshift(dom.value); // 往数组str的第一位插入value值                     str.push(rule.errorMsg);                     return strategys[strategyType].apply(dom, str);                 }                 self.cache.push(fn);             })(rule)         }     },     start: function() {         for (var i = 0, fn; fn = this.cache[i++];) {             var msg = fn();             if (msg) {                 return msg;             }         }     } }  function submitValidate() {     var registerForm = document.getElementById("registerForm");     var validator = new Validator();     validator.add(registerForm.userName, [{         strategy: 'isNotEmpty',         errorMsg: '用户名不能为空'     }, {         strategy: 'minLength:10',         errorMsg: '用户名长度不能小于10位'     }]);     validator.add(registerForm.password, [{         strategy: 'minLength:6',         errorMsg: '密码的长度不能小于6位'     }]);     validator.add(registerForm.phoneNumber, [{         strategy: 'mobileFormat',         errorMsg: '手机号码格式不正确'     }]);     var resultMsg = validator.start();     if (resultMsg) {         alert(resultMsg);         return false;     }     return true; }

当然我们也可以把验证各种类型的算法放到构造函数Validator原型上,这儿就不处理了。

总结

策略模式优点:

  • 策略模式利用组合,委托等技术和思想,有效的避免很多if条件语句。
  • 策略模式提供了开放-封闭原则,使代码更容易理解和扩展。
  • 策略模式中的代码可以复用。

参考