面試官:策略模式有使用過嗎着饥?我:沒有......
何為策略模式犀农?
- 比如在業(yè)務邏輯或程序設計中比如要實現某個功能,有多種方案可供我們選擇宰掉。比如要壓縮一個文件呵哨,我們既可以選擇 ZIP 算法谤逼,也可以選擇 GZIP 算法。
- 這些算法靈活多樣仇穗,可隨意切換流部,而這種解決方案就是我們所要學習的策略模式。
定義或概念
策略模式:定義一系列的算法纹坐,將他們一個個封裝枝冀,并使他們可相互替換。
策略模式的最佳實踐
例子1:獎金計算
- 題目:在很多公司的年終獎都是按照員工的工資基數和年底績效情況來發(fā)放的耘子,例如果漾,績效為 S 的人年終獎有 4 倍工資,A 的人年終獎有 3 倍谷誓,B 的人年終獎有 2 倍绒障。要求我們寫出一個程序來更快的計算員工的年終獎。(編寫一個名為 calcBonus 方法來計算每個員工的獎金數額)
- 可能有些人一上來直接就在一個方法中進行很多 if...else 或 switch...case 判斷, 然后通過這個方法進行計算捍歪。我們可以來試著寫一下:
/**
*
* @param {*} level 績效等級
* @param {*} salary 工資基數
* @returns 年終獎金額
*/
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
- 我想在我們每個人初學代碼時肯定都寫出過這樣的代碼户辱。其實這段代碼有顯而易見的缺點:
- calcBonus 函數邏輯太多
- calcBonus 函數缺乏彈性,比如如果我們需要增加一個等級 C糙臼,那就必須要去修改 calcBonus 函數庐镐。這就違反了開放-封閉原則。
- 復用性差变逃。如果后續(xù)還要重用這個程序去計算獎金必逆,我們只有去 C,V。
- 此時揽乱,可能會想對 calcBonus 函數進行封裝名眉,如我們使用組合函數的形式,如下:
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
- 這樣凰棉,我們將程序進行了進一步改善损拢,但改善微乎其微,依舊沒有解決最重要的問題渊啰,calcBonus 函數還是有可能會很龐大探橱,并且也沒有彈性。
- 那我們再將它進行一次改造绘证,使用策略模式:
將其定義為一系列的算法隧膏,將他們每一個封裝起來,將不變的部分和變化的部分隔開嚷那。
- 在這段程序中胞枕,
算法的使用方式是不變的,都是根據某個算法獲取最后的獎金金額魏宽。而在每個算法的內部實現卻是不同的腐泻,每一個等級對應著不同的計算規(guī)則
决乎。 - 而
在策略模式程序中:最少由兩部分組成,一部分是一組策略類派桩,在策略類中封裝了具體的算法构诚,并負責具體的計算過程。一部分是環(huán)境類 context铆惑,接受用戶的請求范嘱,并將請求委托給某一個策略類。
- 如下:
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
- 其實员魏,
策略模式的實現并不復雜丑蛤,關鍵是如何從策略模式的實現背后,找到封裝變化撕阎,委托和多態(tài)性這些思想的價值
受裹。
例子2:表單驗證
- 題目:在 Web 開發(fā)中,表單校驗是一個常見的話題虏束,要求使用策略模式來完成表單驗證棉饶。
- 比如:
- 用戶名不能為空
- 密碼長度不能少于 6 位
- 手機號碼必須符合正確格式
- 讓我們來實現一下吧:
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("手機號碼格式不正確");
return false;
}
// .....
}
- 這是我們常見的實現方式,它的缺點跟計算獎金一例類似:
- submit 函數龐大魄眉,包含了很多 if...else 語句
- submit 函數缺乏彈性砰盐,如果對其新加一些新的校驗規(guī)則,如果我們把密碼長度從 6 改到 8.那我們就必須要改動 submit 函數坑律,否則無法實現該校驗。這也是違反開放-封閉原則囊骤。
- 復用差晃择,如果說我們程序中還有另一個表達需要驗證,也是進行類似的校驗也物,那我們可能會進行 C, V 操作宫屠。
- 使用策略模式來進行重構
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", "手機號碼格式不正確");
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();
- 使用策略模式重構后,我們后續(xù)僅需配置的方式來完成滑蚯。
- 擴展題目:那如果想給用戶名還想再添加一個規(guī)則浪蹂,那如何完成呢?
- 添加規(guī)則方式如下:
validator.add(username, [
{
strategy: "isEmpty",
msg: "用戶名不能為空"
},
{
strategy: 'minLength:6',
msg: '密碼不能少于 6 位'
}
]);
- 實現:
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: "手機號碼格式不正確",
},
]);
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)缺點
- 優(yōu)點:
- 利用組合告材,委托坤次,多態(tài)等技術有效避免了多重條件語句
- 提供了對開封-封閉原則的完美支持
- 復用性較強,避免許多重復的 C,V 工作
- 缺點:
- 客戶端比如了解所有的策略類斥赋,并選擇合適的策略類缰猴。
策略模式的角色
- Context(環(huán)境類):持有一個 Strategy 類的引用,用一個 ConcreteStrategy 對象來配置
- Strategy(環(huán)境策略類):定義了所有支持的算法的公共接口疤剑,通常是以一個接口或抽象來實現滑绒。Context 使用這個接口來調用其 ConcreteStrategy 定義的算法闷堡。
- ConcreteStrategy(具體策略類):以 Strategy 接口實現某種算法
-
比如以上的例子算法:
策略模式的應用場景
- 想使用對象中各種不同算法變體來在運行時切換算法時
- 擁有很多在執(zhí)行某些行為時有著不同的規(guī)則時