1 策略模式的定義
定義一系列的演算法,把它們一個個封裝起來,並且使用它們可以相互替換。 舉個例子:在程式設計中,我們需要實現某一個功能其實有多種方案可以選擇,例如壓縮檔案的程式,我們可以選擇zip的演算法,也可以選擇gzip的演算法。這些演算法靈活多樣,而且可以隨意相互替換。
2 使用策略模式計算獎金
在單位年終的時候,會對職員進行年終評價,不同等級獲得不一樣的獎金。A級獲得4倍工資,B級獲得3倍工資,C級獲得2倍工資,D級只有一倍工資。我們需要設計一個計算年終的函式。
function calculate = function(salary, level) {
switch(level) {
case 'A':
return salary * 4;
case 'B':
return salary * 3;
case 'C':
return salary * 2;
default:
return salary;
}
}
複製程式碼
上面是一般我們會選擇的計算方式,這樣設計有缺點:
- calculate的函式比較大,Swicth-case中需要覆蓋所有的邏輯分支
- calculate的函式缺乏彈性,如果需要新增一種等級
D
,那麼就需要修改calculate函式內部的實現。 - 演算法的複用性差,如果程式的其他地方需要重用這些計算獎金的演算法,我們只能選擇貼上複製
2.1 使用策略模式重構程式碼
策略模式是定義一系列的演算法,把它們一個個封裝起來。將不變的部分
和變化的部分
隔開是每個設計模式的主題,策略模式也不例外,策略模式的目的就是將演算法的使用
與演算法的實現
分離開來。例子中,可以按一下方式理解:
- 演算法的實現是變化的,每種績效對應的不同的計算規則
- 演算法的使用方式是不變的,都是根據某個演算法取得計算後的獎金數額
策略模式的程式至少由兩部分組成:
- 策略類: 封裝具體的演算法,並負責具體的計算過程
- 環境類Context: 接收使用者請求,隨後將請求委託給策略類
2.1.1 模仿物件導向的方式實現
- performanceA, performanceB, performanceC, performanceOther :都是可變的策略類,封裝計算規則。
- Bonus: 環境類部分,接收使用者請求,委託給策略類進行計算
// 策略類
var performanceA = function() {}
performanceA.calculate = function(salary) {
return salary * 4;
}
var performanceB = function() {}
performanceB.calculate = function(salary) {
return salary * 3;
}
var performanceC = function() {}
performanceC.calculate = function(salary) {
return salary * 2;
}
var performanceOther = function() {}
performanceOther.calculate = function(salary) {
return salary;
}
// 環境類Context
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);
}
複製程式碼
2.1.2 javascript版本的策略模式
上面strategy物件是從各個策略類中建立出來的,那是模擬傳統的面嚮物件語言實現的。在javascript中,函式也就是物件,所以能夠更加簡單和直接的把strategy直接定義為函式。
var strategies = {
'A': function(salary) {
return 4 * salary;
},
'B': function(salary) {
return 3 * salary;
},
'C': function(salary) {
return 2 * salary;
}
}
var calculateBonus = function(salary, level) {
return strategies[level](salary);
}
複製程式碼
2.1.3 多型在策略模式中的體現
通過上面的重構,我們消除了switch分支的條件語句。把計算獎金的邏輯不再放入到context中,而是分佈在各個策略物件中。
- 每個策略物件負責的演算法被各自封在了物件內部
- Context沒有計算獎金的能力,通過職責委託給了某個策略物件
當對這些策略發起請求時計算獎金
時,會根據各自不同的計算返回不同的結果,而這也是物件多型的體現。也是它們能夠相互替換
的目的。替換context中的當前儲存的策略,遍能夠知曉不同的演算法來得到我們想要的結果。
3 使用策略模式進行表單校驗
我們在編寫註冊介面的時候,點選註冊按鈕前需要對錶單進行校驗工作:
- 使用者名稱不能為空
- 密碼長度不能少於6位
- 手機號碼必須符合格式
3.1 普通實現
首先我們不使用策略模式進行實現。該實現方式與計算獎金的實現問題一模一樣。
- registerForm.onSubmit函式很龐大,包含了if-else, 包含了所有的校驗規則
- registerForm.onSubmit函式缺乏彈性,如果想新增一個校驗規則,或則修改規則,那麼就需要深入到該函式的內部實現。違背了
開放-封閉
原則 - 演算法複用性差。如果專案的其他位置也需要相同的校驗,需要拷貝複製
<html>
<body>
<form name="registerForm">
請輸入使用者名稱: <input type="text" name="userName"/ >
請輸入密碼: <input type="text" name="password"/ >
請輸入手機號碼: <input type="text" name="phoneNumber"/ >
<button>提交</button>
</form>
<script>
var registerForm = document.forms['registerForm'];
registerForm.onsubmit = function(event){
if ( registerForm.userName.value === '' ){
alert ( '使用者名稱不能為空' );
event.preventDefault();
}
if ( registerForm.password.value.length < 6 ){
alert ( '密碼長度不能少於 6 位' );
event.preventDefault();
}
if ( !/(^1[3|5|8][0-9]{9}$)/.test( registerForm.phoneNumber.value ) ){
alert ( '手機號碼格式不正確' );
event.preventDefault();
}
}
</script>
</body>
</html>
複製程式碼
3.2 使用策略模式重構
我們需要遵循的規則,依然是這兩條:
- 提取所有的可變原則,將校驗規則封裝起來作為策略類
- 提取context內容,接收使用者請求,通過委託給策略類進行計算 下面,我們實現的內容需求:
- 呼叫validate.add()方法:新增校驗規則(引數1:需要校驗的字串, 引數2:校驗的規則陣列,引數4:可選的正則)
- 呼叫valiadte.start()方法:開始校驗
<html>
<body>
<form name="registerForm">
請輸入使用者名稱: <input type="text" name="userName"/ >
請輸入密碼: <input type="text" name="password"/ >
請輸入手機號碼: <input type="text" name="phoneNumber"/ >
<button>提交</button>
</form>
<script>
// 策略類
var strategies = {
isNotEmpty: function(str, errorMsg) {
if (str === '') {
return errorMsg;
}
},
minLength: function(str, errorMsg, length) {
if (str.length < length) {
return errorMsg;
}
},
isRegExp: function(str, errorMsg, regExp) {
if (!regExp.test(str)) {
return errorMsg;
}
}
}
// context類: 負責接收使用者傳入的請求,並委託給策略類。不可變
var Validate = function() {
var cache = [];
return {
add: function(str, rules, regExp) {
rules.map(function(rule) {
var key = Object.keys(rule)[0];
var errorMsg = rule[key];
var ary = key.split(':');
console.log(key, ary);
cache.push(function() {
// 加入有:分割,第一個則是策略
var strategy = ary.shift();
return strategies[strategy].call(null, str, errorMsg, regExp || ary.shift());
});
});
},
start: function() {
var msg = '';
for (var i = 0; i < cache.length ; i++) {
msg = cache[i]();
if (msg) {
alert(msg);
break;
}
}
return msg;
}
}
};
function validateRegister(registerForm) {
var validate = Validate();
validate.add(registerForm.userName.value, [{'isNotEmpty': '使用者名稱不能為空'},{'minLength:3':'密碼長度不能少於 3 位'}]);
validate.add(registerForm.password.value, [{'minLength:6':'密碼長度不能少於 6 位'}]);
validate.add(registerForm.phoneNumber.value, [{'isRegExp':'手機號碼格式不正確'}], /(^1[3|5|8][0-9]{9}$)/);
var returnMsg = validate.start();
return returnMsg ? false : true;
}
registerForm.onsubmit = function(event){
var isPass = validateRegister(registerForm);
if (!isPass) {
event.preventDefault();
console.log('no validate');
} else {
console.log('pass');
registerForm.submit();
}
}
</script>
</body>
</html>
複製程式碼
4 策略模式的優缺點
策略模式有點:
- 利用組合,委託和多型等技術和思想,可以有效避免多重條件選擇語句
- 提供了對外開放-封閉的原則的完美支援。將演算法封裝在獨立的strategy內,使得它們容易切換,易於理解和擴充
- 策略模式中的演算法可以提供給其他地方,避免了重複貼上複製
- 利用組合與委託讓context擁有執行演算法的能力。這也是繼承的一種更輕便的替代方案
缺點:
- 使用策略模式會讓程式增加許多策略類或者策略物件。
- 使用策略模式,必須要了解所有的strategy之前的不同點,這樣才能選擇一個適合的strategy。