設計模式是: 在物件導向軟體過程中針對特定問題的簡潔而優雅的解決方案. 通過對封裝、繼承、多型、組合等技術的反覆利用, 提煉出可重複使用物件導向的設計技巧.
JavaScript 可以模擬實現傳統面嚮物件語言的設計模式. 事實上也的確如此, 好多的程式碼 demo 都是沿著這個思路分析的. 看完後心裡不免有種一萬頭?在奔騰, 還順便飄來了六個字, 走(qu)你(de), 設計模式.
然而僅僅是生搬硬套, 未免會失去 JavaScript 的靈活性. 不如溯本求源, 看看這些設計模式到底在傳達什麼, 然後遵循此點.
策略模式定義
策略模式: 定義一系列的演算法, 把它們一個個封裝起來, 並且使它們可以相互替換.
字面意思, 就是定義封裝多種演算法, 且各個演算法相互獨立. 當然, 也不僅僅是演算法. 只要定義一些規則, 經處理後輸出我們想要的結果就成. 在此我們稱單個封裝後的演算法為一個策略. 一系列封裝後的演算法稱為一組策略.
一個基於策略模式的程式至少由兩部分組成. 第一部分是一組策略類, 策略類封裝了具體的演算法, 並負責具體的計算過程. 第二部分是環境類 Context, Context 接受客戶的請求, 隨後把請求委託給某一個策略類.
這是面向傳統面嚮物件語言中的說法. 在物件導向思想中, 通過對組合, 多型等技術的使用來實現一個策略模式. 在 JavaScript 中, 對於一個簡單的需求來說, 這麼做就有點大材小用了.
所以, 上面的那句話, 我們換種說法就是, 策略模式需要至少兩部分, 一部分是儲存著一組策略. 另一部分則是如何分配這些策略, 即如何把請求委託給某個/些策略. 其實這也是策略模式的目的, 將演算法的使用與演算法的實現分離.
評級
快到年底了, 公司打算制定一個標準用來給員工評級發福利.
考核專案\等級 | 甲 | 乙 | 丙 |
---|---|---|---|
A | 100>a>=90 | 90>a>=80 | 80>a>=70 |
B | 100>b>=90 | 90>b>=80 | 80>b>=70 |
以A、B考核專案來評定甲乙丙等級. 現有考核人員:
考核專案\考核人 | person_1 | person_2 | person_3 |
---|---|---|---|
A | 80 | 93 | 92 |
B | 85 | 70 | 90 |
const persons = [
{ A: 80, B: 85 },
{ A: 93, B: 70 },
{ A: 92, B: 90 }
]
複製程式碼
常規操作
甲乙丙等級對 A、B 的分值要求是不一樣的. 所以我們可以這麼做:
function rating(person) {
let a = person.A;
let b = person.B;
if (a >= 90 && b >= 90) {
return '甲';
} else if (a >= 80 && b >= 80) {
return '乙';
} else if (a >= 70 && b >= 70) {
return '丙'
} else {
console.log('想啥呢, 還不趕緊卷鋪走人');
}
}
persons.forEach(person => {
person.rate = rating(person);
})
// > persons
// [ { A: 80, B: 85, rate: '乙' },
// { A: 93, B: 70, rate: '丙' },
// { A: 92, B: 90, rate: '甲' } ]
複製程式碼
策略模式下的評級
在策略模式中一部分, 我們提到的分配策略. 要想分配策略, 首先就要知道所有的策略, 只有這樣我們才能針對性的委託給某個/些策略. 這, 也是策略模式的一個缺點.
如果換成策略模式, 第一部分就是儲存一組策略. 現在我們以甲乙丙三種定級標準來制定三種策略, 用物件 strategies
來存貯策略. 考慮到以後可能有 D、E、F 等考核專案的存在, 我們稍微改一下:
const strategies = {
'甲': (person, items) => {
const boolean = items.every(item => {
return person[item] >= 90;
});
if (boolean) return '甲';
},
'乙': (person, items) => {
const boolean = items.every(item => {
return person[item] >= 80;
});
if (boolean) return '乙';
},
'丙': (person, items) => {
const boolean = items.every(item => {
return person[item] >= 70;
});
if (boolean) return '丙';
}
}
複製程式碼
策略就制定好了. 物件的鍵對應著策略的名稱, 物件的值對應著策略的實現.然而, 我們發現, 任何一個策略都不能單獨完成等級的評定.
可是, 我們有說一組策略只能選擇其中一個麼? 為了達成某個目的, 策略組封裝了一組相互獨立平等替換的策略. 一個策略不行, 那就組合唄. 這也是策略模式另一部分存在的意義, 即如何分配策略. rating
函式封裝瞭如何委託策略.
function rating(person, items) {
return strategies['甲'](person, items)
|| strategies['乙'](person, items)
|| strategies['丙'](person, items)
}
persons.forEach(person => {
person.rate = rating(person, ['A', 'B'])
})
// > persons
// [ { A: 80, B: 85, rate: '乙' },
// { A: 93, B: 70, rate: '丙' },
// { A: 92, B: 90, rate: '甲' } ]
複製程式碼
邏輯的轉移
所有的設計模式都遵循一條原則. 即 “找出程式中變化的地方, 並將變化封裝起來”.
將不變的隔離開來, 變化的封裝起來. 策略模式中, 策略組對應著程式中不變的地方. 將策略組制定好存貯起來, 然後想著如何去分配使用策略.
當然, 如何制定策略和如何分配策略之間的關係十分緊密, 可以說兩者相互影響.
再次看看制定的策略, “找出程式中變化的地方, 並將變化封裝起來”, 我們可以再次改造一下策略組物件 strategies
和負責分配策略的函式 rating
.
const strategies = {
'甲': 90,
'乙': 80,
'丙': 70,
}
function rating(person, items) {
const level = value => {
return (person, items) => {
const boolean = items.every(item => {
return person[item] >= strategies[value];
});
if (boolean) return value;
}
}
return level('甲')(person, items)
|| level('乙')(person, items)
|| level('丙')(person, items)
}
persons.forEach(person => {
person.rate = rating(person, ['A', 'B'])
})
// > persons
// [ { A: 80, B: 85, rate: '乙' },
// { A: 93, B: 70, rate: '丙' },
// { A: 92, B: 90, rate: '甲' } ]
複製程式碼
在上面的這種做法中, 我們把制定策略的邏輯挪到了分配策略裡了. 所以說, 如何制定策略和如何分配策略, 依情況而定.
不過回頭在看一看這段程式碼, 是不是和平時用物件對映的做法很相似.
當然, 策略模式的用法還有很多, 使用形式也有不同, 要根據場景找到最適合的策略.
小結
總結一下:
-
策略模式至少包括兩部分, 制定策略和分配策略.
-
策略模式的目的在於, 將策略制定和策略分配隔離開來.
-
策略制定和策略分配關係密切, 相互影響.