介紹
策略模式的意義是定義一系列的算法,把它們一個個封裝起來,並且使它們可相互替換。此模式讓算法的變化不會影響到使用算法的客戶。
實現
舉一個例子,比如我們做數據合法性校驗,一般是通過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條件語句。
- 策略模式提供了開放-封閉原則,使代碼更容易理解和擴展。
- 策略模式中的代碼可以復用。
