JavaScript程序設(shè)計(jì)模式小技巧——策略模式缸棵,快看快用6碌凇Lぶ尽胀瞪!
何為策略模式赏廓?
- 比如在業(yè)務(wù)邏輯或程序設(shè)計(jì)中比如要實(shí)現(xiàn)某個(gè)功能幔摸,有多種方案可供我們選擇既忆。比如要壓縮一個(gè)文件患雇,我們既可以選擇 ZIP 算法苛吱,也可以選擇 GZIP 算法翠储。
- 這些算法靈活多樣,可隨意切換庐舟,而這種解決方案就是我們所要學(xué)習(xí)的策略模式挪略。
定義或概念
策略模式:定義一系列的算法杠娱,將他們一個(gè)個(gè)封裝墨辛,并使他們可相互替換睹簇。
策略模式的最佳實(shí)踐
例子1:獎(jiǎng)金計(jì)算
- 題目:在很多公司的年終獎(jiǎng)都是按照員工的工資基數(shù)和年底績(jī)效情況來(lái)發(fā)放的太惠,例如凿渊,績(jī)效為 S 的人年終獎(jiǎng)有 4 倍工資埃脏,A 的人年終獎(jiǎng)有 3 倍彩掐,B 的人年終獎(jiǎng)有 2 倍堵幽。要求我們寫(xiě)出一個(gè)程序來(lái)更快的計(jì)算員工的年終獎(jiǎng)朴下。(編寫(xiě)一個(gè)名為 calcBonus 方法來(lái)計(jì)算每個(gè)員工的獎(jiǎng)金數(shù)額)
- 可能有些人一上來(lái)直接就在一個(gè)方法中進(jìn)行很多 if...else 或 switch...case 判斷, 然后通過(guò)這個(gè)方法進(jìn)行計(jì)算殴胧。我們可以來(lái)試著寫(xiě)一下:
/**
*
* @param {*} level 績(jī)效等級(jí)
* @param {*} salary 工資基數(shù)
* @returns 年終獎(jiǎng)金額
*/
var calcBonus = function (level, salary) {
if (level === "S") {
return salary * 4;
} else if (level === "A") {
return salary * 3;
} else if (level === "B") {
return salary * 2;
}
};
calcBonus('A', 20000); // 60000
calcBonus('B', 8000); // 16000
- 我想在我們每個(gè)人初學(xué)代碼時(shí)肯定都寫(xiě)出過(guò)這樣的代碼。其實(shí)這段代碼有顯而易見(jiàn)的缺點(diǎn):
- calcBonus
函數(shù)邏輯太多
- calcBonus 函數(shù)
缺乏彈性
佩迟,比如如果我們需要增加一個(gè)等級(jí) C团滥,那就必須要去修改 calcBonus 函數(shù)免胃。這就違反了開(kāi)放-封閉原則
。 -
復(fù)用性差
惫撰。如果后續(xù)還要重用這個(gè)程序去計(jì)算獎(jiǎng)金羔沙,我們只有去 C,V。
- calcBonus
- 此時(shí)厨钻,可能會(huì)想對(duì) calcBonus 函數(shù)進(jìn)行封裝扼雏,如我們使用組合函數(shù)的形式,如下:
var totalS = function (salary) {
return salary * 4;
};
var totalA = function (salary) {
return salary * 3;
};
var totalB = function (salary) {
return salary * 2;
};
var calcBonus = function (level, salary) {
if (level === "S") {
return totalS(salary);
} else if (level === "A") {
return totalA(salary);
} else if (level === "B") {
return totalB(salary);
}
};
calcBonus('A', 20000); // 60000
calcBonus('B', 8000); // 16000
- 這樣夯膀,我們將程序進(jìn)行了進(jìn)一步改善诱建,但改善微乎其微,依舊沒(méi)有解決最重要的問(wèn)題,calcBonus 函數(shù)還是有可能會(huì)很龐大汽馋,并且也沒(méi)有彈性铁蹈。
- 那我們?cè)賹⑺M(jìn)行一次改造便锨,使用策略模式:
將其定義為一系列的算法,將他們每一個(gè)封裝起來(lái)厘托,將不變的部分和變化的部分隔開(kāi)饺藤。
- 在這段程序中,
算法的使用方式是不變的,都是根據(jù)某個(gè)算法獲取最后的獎(jiǎng)金金額。而在每個(gè)算法的內(nèi)部實(shí)現(xiàn)卻是不同的蔽挠,每一個(gè)等級(jí)對(duì)應(yīng)著不同的計(jì)算規(guī)則
。 - 而
在策略模式程序中:最少由兩部分組成量窘,一部分是一組策略類嫩海,在策略類中封裝了具體的算法,并負(fù)責(zé)具體的計(jì)算過(guò)程。一部分是環(huán)境類 context,接受用戶的請(qǐng)求,并將請(qǐng)求委托給某一個(gè)策略類。
- 如下:
var strategies = {
S: function (salary) {
return salary * 4;
},
A: function (salary) {
return salary * 3;
},
B: function (salary) {
return salary * 2;
},
};
var calcBonus = function (level, salary) {
return strategies[level](salary);
}
calcBonus('A', 20000); // 60000
calcBonus('B', 8000); // 16000
- 其實(shí)挤聘,
策略模式的實(shí)現(xiàn)并不復(fù)雜步淹,關(guān)鍵是如何從策略模式的實(shí)現(xiàn)背后澈驼,找到封裝變化内边,委托和多態(tài)性這些思想的價(jià)值
。
例子2:表單驗(yàn)證
- 題目:在 Web 開(kāi)發(fā)中眶俩,表單校驗(yàn)是一個(gè)常見(jiàn)的話題线罕,要求使用策略模式來(lái)完成表單驗(yàn)證喇闸。
- 比如:
- 用戶名不能為空
- 密碼長(zhǎng)度不能少于 6 位
- 手機(jī)號(hào)碼必須符合正確格式
- 讓我們來(lái)實(shí)現(xiàn)一下吧:
function submit() {
let { username, password, tel } = infoForm;
if (username === "") {
Toast("用戶名不能為空");
return false;
}
if (password.length < 6) {
Toast("密碼不能少于 6 位");
return false;
}
if (!/(^1[3|5|8][0-9]{9}$)/.test(tel)) {
Toast("手機(jī)號(hào)碼格式不正確");
return false;
}
// .....
}
- 這是我們常見(jiàn)的實(shí)現(xiàn)方式舆瘪,它的缺點(diǎn)跟計(jì)算獎(jiǎng)金一例類似:
- submit 函數(shù)龐大箕戳,包含了很多 if...else 語(yǔ)句
- submit 函數(shù)缺乏彈性环础,如果對(duì)其新加一些新的校驗(yàn)規(guī)則角雷,如果我們把密碼長(zhǎng)度從 6 改到 8.那我們就必須要改動(dòng) submit 函數(shù)胯舷,否則無(wú)法實(shí)現(xiàn)該校驗(yàn)。這也是違反開(kāi)放-封閉原則废菱。
- 復(fù)用差我磁,如果說(shuō)我們程序中還有另一個(gè)表達(dá)需要驗(yàn)證霞势,也是進(jìn)行類似的校驗(yàn),那我們可能會(huì)進(jìn)行 C, V 操作菌湃。
- 使用策略模式來(lái)進(jìn)行重構(gòu)
let infoForm = {
username: "我是某某某",
password: 'zxcvbnm',
tel: 16826384655,
};
var strategies = {
isEmpty: function (val, msg) {
if (!val) return msg;
},
minLength: function (val, length, msg) {
if (val.length < length) return msg;
},
isTel: function (val, msg) {
if (!/(^1[3|5|8][0-9]{9}$)/.test(val)) return msg;
},
};
var validFn = function () {
var validator = new Validator();
let { username, password, tel } = infoForm;
validator.add(username, "isEmpty", "用戶名不能為空");
validator.add(password, "minLength:6", "密碼不能少于 6 位");
validator.add(tel, "isTel", "手機(jī)號(hào)碼格式不正確");
var msg = validator.start();
return msg;
};
class Validator {
constructor() {
this.cache = [];
}
add(attr, rule, msg) {
var ruleArr = rule.split(":");
this.cache.push(function () {
var strategy = ruleArr.shift();
ruleArr.unshift(attr);
ruleArr.push(msg);
return strategies[strategy].apply(attr, ruleArr);
});
}
start() {
for (let i = 0; i < this.cache.length; i++) {
var msg = this.cache[i]();
if (msg) return msg;
}
}
}
function submit() {
let msg = validFn();
if (msg) {
Toast(msg);
return false;
}
console.log('verify success');
// .....
}
submit();
- 使用策略模式重構(gòu)后,我們后續(xù)僅需配置的方式來(lái)完成。
- 擴(kuò)展題目:那如果想給用戶名還想再添加一個(gè)規(guī)則,那如何完成呢朗兵?
- 添加規(guī)則方式如下:
validator.add(username, [
{
strategy: "isEmpty",
msg: "用戶名不能為空"
},
{
strategy: 'minLength:6',
msg: '密碼不能少于 6 位'
}
]);
- 實(shí)現(xiàn):
let infoForm = {
username: "阿斯頓發(fā)生的",
password: "ss1sdf",
tel: 15829485647,
};
var strategies = {
isEmpty: function (val, msg) {
if (!val) return msg;
},
minLength: function (val, length, msg) {
if (val.length < length) return msg;
},
isTel: function (val, msg) {
if (!/(^1[3|5|8][0-9]{9}$)/.test(val)) return msg;
},
};
var validFn = function () {
var validator = new Validator();
let { username, password, tel } = infoForm;
validator.add(username, [
{
strategy: "isEmpty",
msg: "用戶名不能為空",
},
{
strategy: "minLength:6",
msg: "密碼不能少于 6 位",
},
]);
validator.add(password, [
{
strategy: "minLength:6",
msg: "密碼不能少于 6 位",
},
]);
validator.add(tel, [
{
strategy: "isTel",
msg: "手機(jī)號(hào)碼格式不正確",
},
]);
var msg = validator.start();
return msg;
};
class Validator {
constructor() {
this.cache = [];
}
add(attr, rules) {
for (let i = 0; i < rules.length; i++) {
var rule = rules[i];
var ruleArr = rule.strategy.split(":");
var msg = rule.msg;
var cacheItem = this.createCacheItem(ruleArr, attr, msg);
this.cache.push(cacheItem);
}
}
start() {
for (let i = 0; i < this.cache.length; i++) {
var msg = this.cache[i]();
if (msg) return msg;
}
}
createCacheItem(ruleArr, attr, msg) {
return function () {
var strategy = ruleArr.shift();
ruleArr.unshift(attr);
ruleArr.push(msg);
return strategies[strategy].apply(attr, ruleArr);
};
}
}
function submit() {
let msg = validFn();
if (msg) {
Toast(msg);
return false;
}
console.log("verify success");
// .....
}
submit();
策略模式的優(yōu)缺點(diǎn)
- 優(yōu)點(diǎn):
- 利用組合节预,委托,多態(tài)等技術(shù)有效避免了多重條件語(yǔ)句
- 提供了對(duì)開(kāi)封-封閉原則的完美支持
- 復(fù)用性較強(qiáng),避免許多重復(fù)的 C,V 工作
- 缺點(diǎn):
- 客戶端要先了解所有的策略類秒裕,才能選擇合適的策略類袱蚓。
策略模式的角色
-
Context(環(huán)境類)
:持有一個(gè) Strategy 類的引用,用一個(gè) ConcreteStrategy 對(duì)象來(lái)配置 -
Strategy(環(huán)境策略類)
:定義了所有支持的算法的公共接口几蜻,通常是以一個(gè)接口或抽象來(lái)實(shí)現(xiàn)喇潘。Context 使用這個(gè)接口來(lái)調(diào)用其 ConcreteStrategy 定義的算法体斩。 -
ConcreteStrategy(具體策略類)
:以 Strategy 接口實(shí)現(xiàn)某種算法
-
比如以上的例子算法:
策略模式的應(yīng)用場(chǎng)景
- 想使用對(duì)象中各種不同算法變體來(lái)在運(yùn)行時(shí)切換算法時(shí)
- 擁有很多在執(zhí)行某些行為時(shí)有著不同的規(guī)則時(shí)
Tip: 文章部分內(nèi)容參考于曾探
大佬的《JavaScript 設(shè)計(jì)模式與開(kāi)發(fā)實(shí)踐》。文章僅做個(gè)人學(xué)習(xí)總結(jié)和知識(shí)匯總
特殊字符描述:
- 問(wèn)題標(biāo)注
Q:(question)
- 答案標(biāo)注
R:(result)
- 注意事項(xiàng)標(biāo)準(zhǔn):
A:(attention matters)
- 詳情描述標(biāo)注:
D:(detail info)
- 總結(jié)標(biāo)注:
S:(summary)
- 分析標(biāo)注:
Ana:(analysis)
- 提示標(biāo)注:
T:(tips)