1.簡介
在于都本文之前介劫,希望大家能夠先閱讀以下JS進階系列03-JS面向?qū)ο蟮娜筇卣髦鄳B(tài)這篇文章案淋,了解JS的多態(tài)。在這篇文章踢京,我們舉了一個例子瓣距,就是選拔官員選拔合唱團成員時,他并不需要提前知道所有的成員在唱歌時具體會發(fā)出什么聲音蹈丸。他關(guān)注的只是,他發(fā)出命令“唱”時慨默,合唱團成員就會開始唱歌弧腥。至于每個成員具體唱什么,交給他們自己好了虾攻。
這其實就是一個典型的策略模式更鲁,當我們在定義一個方法時,如果涉及到了太多的條件分支時澡为,就應該思考一下,這些分支有沒有必要定義在這個方法中顶别。更準確地說,這個方法是不是需要提前知道所有的規(guī)則完慧,這些規(guī)則是不是固定不會改變的剩失。如果答案是否,那么你可以考慮將這些具體的規(guī)則剝離出來脾歧,交給傳入的參數(shù)去實現(xiàn)演熟,方法主體只需要關(guān)注你不變的目的即可。
策略模式的定義是:定義一系列的算法,把他們一個個封裝起來免猾,并且使他們可以互相替換。不過實際業(yè)務(wù)中获三,策略模式并不只是封裝算法锨苏,如果一系列業(yè)務(wù)規(guī)則指向目標一致,并且可以被互相替換使用贞谓,我們都可以用策略模式來封裝它們葵诈。下面我們舉幾個策略模式的使用場景,讓大家詳細體會一下理疙。
2. 使用策略模式計算獎金
試想如下場景:公司hr需要你來為其編寫一段代碼泞坦,用來計算年底為每個員工發(fā)放的獎金。規(guī)則是:考核A的員工發(fā)五倍月工資赃梧,考核B的員工發(fā)三倍月工資,考核C的員工只有一個月工資几睛。你很自然會寫出如下代碼:
function calculateBonus(level, salary) {
if(level === 'A') {
return salary * 5;
} else if (level === 'B') {
return salary * 3;
} else if (level === 'C') {
return salary;
}
}
calculateBonus('A', 10000); // 50000
這個函數(shù)接收兩個參數(shù)粤攒,考核等級level和月工資salary,按照不同的等級匹配不同的獎金計算規(guī)則并返回焕济。這要求我們在函數(shù)中將所有現(xiàn)階段可能出現(xiàn)的規(guī)則都列出來盔几,并且當未來對規(guī)則有刪減或者改動時,都需要重新修改該方法邏輯上鞠,這樣的方法在擴展性和可維護性上顯然是不好的芯丧。
我們考慮一下對這段代碼進行改寫,我們的目的是為了讓hr一調(diào)用該方法就能正確輸出獎金數(shù)目谴咸,而具體如何產(chǎn)生獎金骗露,這個方法并不需要去關(guān)心。所以我們可以將不變的目的隔離出來珊随,將可變的計算獎金規(guī)則封裝起來驹暑。如下:
function levelA() {}
levelA.prototype.calculate = function(salary) {
return salary * 5;
};
function levelB() {}
levelB.prototype.calculate = function(salary) {
return salary * 3;
};
function Bonus(salary, strategy) {
this.salary = salary;
this.strategy = strategy;
}
Bonus.prototype.getBonus = function() {
return this.strategy.calculate(this.salary);
};
var bonus = new Bonus(10000, new levelA);
bonus.getBonus(); // 50000
我們最終需要使用的是Bonus類优俘,只需要向其傳入salary,和考核人員所屬的類別帆焕,然后調(diào)用其getBonus() 方法即可。這個是典型的模仿傳統(tǒng)的面向?qū)ο蟮膶崿F(xiàn)方式财饥,JS是無類的钥星,其實現(xiàn)方式更為簡單。
var strategies = {
'A': function(salary) {
return salary * 5;
},
'B': function(salary) {
return salary * 3;
},
'C': function(salary) {
return salary;
}
};
function caculateBonus(salary, level) {
return strategies['level'](salary);
}
caculateBonus(10000, 'A'); // 50000
3. 使用策略模式實現(xiàn)表單校驗
表單校驗是一個很常見的需求贯莺,假設(shè)你需要為一個網(wǎng)站編寫注冊模塊宁改。用戶需要輸入用戶名,密碼和手機號以后點擊注冊按鈕進行注冊爹耗,在向后臺發(fā)起請求前谜喊,需要在前端校驗客戶輸入的合法性:用戶名不能為空,密碼長度不能少于6位讼溺,手機號碼必須符合格式最易。
先來看第一版實現(xiàn):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>表單校驗01</title>
</head>
<body>
<form action="http://xxx.com/register" id="registerForm" method="post">
請輸入用戶名:<input type="text" name="userName"/>
請輸入密碼:<input type="text" name="passWord"/>
請輸入手機號碼:<input type="text" name="phoneNumber">
<button>提交</button>
</form>
<script>
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>
這個版本在邏輯上是最容易想到的藻懒,但是其和計算獎金的最初版本擁有一樣的缺點:
- registerForm.onsubmit 函數(shù)比較龐大视译,包含了很多if-else語句,這些語句需要覆蓋所有的校驗規(guī)則鄙早。
- registerForm.onsubmit 函數(shù)缺乏彈性椅亚,如果增加了一種新的校驗規(guī)則,或者想把密碼的校驗長度從6改為8弥虐,我們都必須深入registerForm.onsubmit 函數(shù)的內(nèi)部實現(xiàn),這是違反開放-封閉原則的珠插。
- 算法的復用性差颖对,如果在程序中增加了另外一個表單,這個表單也需要進行一些類似的校驗顾患,那么我們很可能隨處都可見這些校驗邏輯規(guī)則的復制训堆。
下面,我們使用策略模式來實現(xiàn)表單校驗膘流。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>表單校驗02</title>
</head>
<body>
<form action="http://xxx.com/register" id="registerForm" method="post">
請輸入用戶名:<input type="text" name="userName"/>
請輸入密碼:<input type="text" name="passWord"/>
請輸入手機號碼:<input type="text" name="phoneNumber">
<button>提交</button>
</form>
<script>
// 封裝策略對象
var strageties = {
isNonEmpty: function (value, errMsg) {
if (value === '') {
return errMsg;
}
},
minLength: function (value, length, errMsg) {
if (value.length < length) {
return errMsg;
}
},
isMobile: function (value, errMsg) {
if(!/^1[3|5|8][0-9]{9}$/.test(value)) {
return errMsg;
}
}
};
// 校驗規(guī)則
function Validate() {
this.cache = []; // 保存校驗規(guī)則
}
Validate.prototype.add = function (dom, rule, errMsg) {
var array = rule.split(':'); //獲取用戶通過key:value指定的規(guī)則
this.cache.push(function () { // 把校驗步驟用空函數(shù)包起來呼股,并依次放入cache
var stragety = array.shift(); // 取出用戶指定的stragety
array.unshift(dom.value); // 將input的value放到參數(shù)列表最前面
array.push(errMsg); // 將errMsg放到參數(shù)列表最后面
return strageties[stragety].apply(dom, array); // 實際執(zhí)行時傳給指定stragety的參數(shù)為[dom.value,rule.value,errMsg]
})
};
Validate.prototype.start = function () {
for (var i=0,validateFunc; validateFunc = this.cache[i++];) {
var errMsg = validateFunc();
if (errMsg) {
return errMsg;
}
}
};
var registerForm = document.getElementById('registerForm');
var validateFunc = function() {
var validate = new Validate();
validate.add(registerForm.userName, 'isNonEmpty', '用戶名不能為空');
validate.add(registerForm.passWord, 'minLength:6', '密碼長度不小于6位');
validate.add(registerForm.phoneNumber, 'isMobile', '手機號碼格式不正確');
var errMsg = validate.start();
return errMsg;
};
registerForm.onsubmit = function () {
var errMsg = validateFunc();
if (errMsg) {
alert(errMsg);
return false;
}
};
</script>
</body>
</html>
這樣改寫代碼以后彭谁,如果我們需要為某個輸入框加入指定規(guī)則允扇,只需要調(diào)用add方法即可,靈活且便與擴展狭园。美中不足的是糊治,如果我們要為一個輸入框添加多個規(guī)則時,需要重復調(diào)用多次add绎谦。那么有沒有辦法粥脚,只用調(diào)用一次add就可以為輸入框添加多種規(guī)則呢?
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>表單校驗03</title>
</head>
<body>
<form action="http://xxx.com/register" id="registerForm" method="post">
請輸入用戶名:<input type="text" name="userName"/>
請輸入密碼:<input type="text" name="passWord"/>
請輸入手機號碼:<input type="text" name="phoneNumber">
<button>提交</button>
</form>
<script>
// 封裝策略對象
var strageties = {
isNonEmpty: function (value, errMsg) {
if (value === '') {
return errMsg;
}
},
minLength: function (value, length, errMsg) {
if (value.length < length) {
return errMsg;
}
},
isMobile: function (value, errMsg) {
if(!/^1[3|5|8][0-9]{9}$/.test(value)) {
return errMsg;
}
}
};
// 校驗規(guī)則
function Validate() {
this.cache = []; // 保存校驗規(guī)則
}
Validate.prototype.add = function (dom, rules) {
var self = this;
for(var i = 0,rule; rule=rules[i++];) {
(function(rule){
var array = rule.stragety.split(':');
var errMsg = rule.errMsg;
self.cache.push(function () {
var stragety = array.shift(); // 取出用戶指定的stragety
array.unshift(dom.value); // 將input的value放到參數(shù)列表最前面
array.push(errMsg); // 將errMsg放到參數(shù)列表最后面
return strageties[stragety].apply(dom, array); // 實際執(zhí)行時傳給指定stragety的參數(shù)為[dom.value,rule.value,errMsg]
})
})(rule)
}
};
Validate.prototype.start = function () {
for (var i=0,validateFunc; validateFunc = this.cache[i++];) {
var errMsg = validateFunc();
if (errMsg) {
return errMsg;
}
}
};
var registerForm = document.getElementById('registerForm');
var validateFunc = function() {
var validate = new Validate();
validate.add(registerForm.userName, [{stragety: 'isNonEmpty',errMsg: '用戶名不能為空'},{stragety:'minLength:6', errMsg: '用戶名長度不能小于6'}]);
validate.add(registerForm.passWord, [{stragety:'minLength:6', errMsg: '密碼長度不小于6位'}]);
validate.add(registerForm.phoneNumber,[{stragety: 'isMobile', errMsg: '手機號碼格式不正確'}]);
var errMsg = validate.start();
return errMsg;
};
registerForm.onsubmit = function () {
var errMsg = validateFunc();
if (errMsg) {
alert(errMsg);
return false;
}
};
</script>
</body>
</html>
4. 動態(tài)類型下的策略模式
我們說過赃蛛,JS是動態(tài)類型的搀菩,函數(shù)接受的參數(shù)并沒有限制類型肪跋,所以,我們其實不必要把策略都封裝在一個對象中州既。比如,計算bonus的題目阐虚,我們可以這樣修改:
var A = function(salary) {
return salary * 5;
},
var B = function(salary) {
return salary * 3;
},
var C = function(salary) {
return salary;
}
function caculateBonus(salary, level) {
return strategies['level'](salary);
}
caculateBonus(10000, 'A'); // 50000
5. 策略模式的優(yōu)缺點和使用
策略模式的優(yōu)點:
- 策略模式利用組合实束,委托和多態(tài)等技術(shù)思想逊彭,可以避免多重條件語句。
- 策略模式提供了對開放-封閉原則的完美支持侮叮,將算法封裝在獨立的stragety中,使得它們易于切換囊榜,理解和擴展。
- 策略模式中算法也可以在其他地方復用歹嘹,避免冗余代碼孔庭。
- 策略模式利用組合和委托是的Context具有執(zhí)行算法的能力材蛛,這也是繼承一種更輕便的替代方案。
策略模式的缺點:
- 使用策略模式會增加許多策略類或者策略對象芽淡。不過這要比將它們的邏輯堆砌在Context要好豆赏。
- 要使用stragety富稻,必須要了解所有stragety的細節(jié)椭赋。此時stragety向客戶暴露了其實現(xiàn)或杠,這是有違最少知識原則的。
總體來說向抢,使用策略模式來消除眾多的條件分支是利大于弊的挟鸠。在JS中,使用策略模式有時是隱形的艘希,不必要將策略放在特殊的類或者對象中枢冤,其策略往往是一個個單獨的函數(shù)。合理選用策略模式淹真,會讓我們的代碼更加靈活且易于擴展核蘸。
參考
BOOK-《JavaScript設(shè)計模式與開發(fā)實踐》 第5章