前言
本系列文章主要根據《JavaScript設計模式與開發實踐》整理而來,其中會加入了一些自己的思考。希望對大家有所幫助。
文章系列
概念
策略模式的定義是:定義一系列的演算法,把它們一個個封裝起來,並且使它們可以相互替換。
策略模式指的是定義一系列的演算法,把它們一個個封裝起來。將不變的部分和變化的部分隔開是每個設計模式的主題,策略模式也不例外,策略模式的目的就是將演算法的使用與演算法的實現分離開來。
一個基於策略模式的程式至少由兩部分組成。第一個部分是一組策略類,策略類封裝了具體 的演算法,並負責具體的計算過程。 第二個部分是環境類Context,Context 接受客戶的請求,隨後 把請求委託給某一個策略類。要做到這點,說明 Context中要維持對某個策略物件的引用。
策略模式的實現並不複雜,關鍵是如何從策略模式的實現背後,找到封裝變化、委託和多型性這些思想的價值。
場景
從定義上看,策略模式就是用來封裝演算法的。但如果把策略模式僅僅用來封裝演算法,未免有一點大材小用。在實際開發中,我們通常會把演算法的含義擴散開來,使策略模式也可以用來封裝 一系列的“業務規則”。只要這些業務規則指向的目標一致,並且可以被替換使用,我們就可以 用策略模式來封裝它們。
優缺點
優點
- 策略模式利用組合、委託和多型等技術和思想,可以有效地避免多重條件選擇語句。
- 策略模式提供了對開放—封閉原則的完美支援,將演算法封裝在獨立的strategy中,使得它們易於切換,易於理解,易於擴充套件。
- 策略模式中的演算法也可以複用在系統的其他地方,從而避免許多重複的複製貼上工作。
- 在策略模式中利用組合和委託來讓 Context 擁有執行演算法的能力,這也是繼承的一種更輕便的替代方案。
缺點
- 增加許多策略類或者策略物件,但實際上這比把它們負責的 邏輯堆砌在 Context 中要好。
- 要使用策略模式,必須瞭解所有的 strategy,必須瞭解各個 strategy 之間的不同點, 這樣才能選擇一個合適的 strategy。
但這些缺點並不嚴重
例子
計算獎金
粗糙的實現
var calculateBonus = function( performanceLevel, salary ){
if ( performanceLevel === 'S' ){
return salary * 4;
}
if ( performanceLevel === 'A' ){
return salary * 3;
}
if ( performanceLevel === 'B' ){
return salary * 2;
}
};
calculateBonus( 'B', 20000 ); // 輸出:40000
calculateBonus( 'S', 6000 ); // 輸出:24000
複製程式碼
缺點:
- calculateBonus 函式比較龐大,包含了很多 if-else 語句
- calculateBonus 函式缺乏彈性,如果增加了一種新的績效等級 C,或者想把績效 S 的獎金 係數改為 5,那我們必須深入 calculateBonus 函式的內部實現,這是違反開放封閉原則的。
- 演算法的複用性差
使用組合函式重構程式碼
var performanceS = function( salary ){
return salary * 4;
};
var performanceA = function( salary ){
return salary * 3;
};
var performanceB = function( salary ){
return salary * 2;
};
var calculateBonus = function( performanceLevel, salary ){
if ( performanceLevel === 'S' ){
return performanceS( salary );
}
if ( performanceLevel === 'A' ){
return performanceA( salary );
}
if ( performanceLevel === 'B' ){
return performanceB( salary );
}
};
calculateBonus( 'A' , 10000 ); // 輸出:30000
複製程式碼
問題依然存在:calculateBonus 函式有可能越來越龐大,而且在系統變化的時候缺乏彈性
使用策略模式重構程式碼
var performanceS = function(){};
performanceS.prototype.calculate = function( salary ){
return salary * 4;
};
var performanceA = function(){};
performanceA.prototype.calculate = function( salary ){
return salary * 3;
};
var performanceB = function(){};
performanceB.prototype.calculate = function( salary ){
return salary * 2;
};
//接下來定義獎金類Bonus:
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 ); // 把計算獎金的操作委託給對應的策略物件
};
var bonus = new Bonus();
bonus.setSalary( 10000 );
bonus.setStrategy( new performanceS() ); // 設定策略物件
console.log( bonus.getBonus() ); // 輸出:40000
bonus.setStrategy( new performanceA() ); // 設定策略物件
console.log( bonus.getBonus() ); // 輸出:30000
複製程式碼
但這段程式碼是基於傳統面嚮物件語言的模仿,下面我們用JavaScript實現的策略模式。
JavaScript 版本的策略模式
在 JavaScript 語言中,函式也是物件,所以更簡單和直接的做法是把 strategy 直接定義為函式
var strategies = {
"S": function( salary ){
return salary * 4;
},
"A": function( salary ){
return salary * 3;
},
"B": function( salary ){
return salary * 2;
}
};
var calculateBonus = function( level, salary ){
return strategies[ level ]( salary );
};
console.log( calculateBonus( 'S', 20000 ) ); // 輸出:80000
console.log( calculateBonus( 'A', 10000 ) ); // 輸出:30000
複製程式碼
es6類實現
var performanceS = function () {};
performanceS.prototype.calculate = function (salary) {
return salary * 4;
};
var performanceA = function () {};
performanceA.prototype.calculate = function (salary) {
return salary * 3;
};
var performanceB = function () {};
performanceB.prototype.calculate = function (salary) {
return salary * 2;
};
//接下來定義獎金類Bonus:
class Bonus {
constructor() {
this.salary = null; // 原始工資
this.strategy = null; // 績效等級對應的策略物件
}
setSalary(salary) {
this.salary = salary; // 設定員工的原始工資
}
setStrategy(strategy) {
this.strategy = strategy; // 設定員工績效等級對應的策略物件
}
getBonus() { // 取得獎金數額
return this.strategy.calculate(this.salary); // 把計算獎金的操作委託給對應的策略物件
}
}
var bonus = new Bonus();
bonus.setSalary(10000);
bonus.setStrategy(new performanceS()); // 設定策略物件
console.log(bonus.getBonus()); // 輸出:40000
bonus.setStrategy(new performanceA()); // 設定策略物件
console.log(bonus.getBonus()); // 輸出:30000
複製程式碼
緩動動畫
目標:編寫一個動畫類和一些緩動演算法,讓小球以各種各樣的緩動效果在頁面中運動
分析:
首先緩動演算法的職責是實現小球如何運動
然後動畫類(即context)的職責是負責:
-
初始化動畫物件
在運動開始之前,需要提前記錄一些有用的資訊,至少包括以下資訊:
- 動畫開始時的準確時間點;
- 動畫開始時,小球所在的原始位置;
- 小球移動的目標位置;
- 小球運動持續的時間。
-
計算小球某時刻的位置
-
更新小球的位置
實現:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div style="position:absolute;background:blue" id="div">我是div</div>
</body>
<script>
var tween = {
linear: function (t, b, c, d) {
return c * t / d + b;
},
easeIn: function (t, b, c, d) {
return c * (t /= d) * t + b;
},
strongEaseIn: function (t, b, c, d) {
return c * (t /= d) * t * t * t * t + b;
},
strongEaseOut: function (t, b, c, d) {
return c * ((t = t / d - 1) * t * t * t * t + 1) + b;
},
sineaseIn: function (t, b, c, d) {
return c * (t /= d) * t * t + b;
},
sineaseOut: function (t, b, c, d) {
return c * ((t = t / d - 1) * t * t + 1) + b;
}
};
var Animate = function (dom) {
this.dom = dom; // 進行運動的dom 節點
this.startTime = 0; // 動畫開始時間
this.startPos = 0; // 動畫開始時,dom 節點的位置,即dom 的初始位置
this.endPos = 0; // 動畫結束時,dom 節點的位置,即dom 的目標位置
this.propertyName = null; // dom 節點需要被改變的css 屬性名
this.easing = null; // 緩動演算法
this.duration = null; // 動畫持續時間
};
Animate.prototype.start = function (propertyName, endPos, duration, easing) {
this.startTime = +new Date; // 動畫啟動時間
this.startPos = this.dom.getBoundingClientRect()[propertyName]; // dom 節點初始位置
this.propertyName = propertyName; // dom 節點需要被改變的CSS 屬性名
this.endPos = endPos; // dom 節點目標位置
this.duration = duration; // 動畫持續事件
this.easing = tween[easing]; // 緩動演算法
var self = this;
var timeId = setInterval(function () { // 啟動定時器,開始執行動畫
if (self.step() === false) { // 如果動畫已結束,則清除定時器
clearInterval(timeId);
}
}, 16);
};
Animate.prototype.step = function () {
var t = +new Date; // 取得當前時間
if (t >= this.startTime + this.duration) { // (1)
this.update(this.endPos); // 更新小球的CSS 屬性值
return false;
}
var pos = this.easing(t - this.startTime, this.startPos, this.endPos - this.startPos, this.duration);
// pos 為小球當前位置
this.update(pos); // 更新小球的CSS 屬性值
};
Animate.prototype.update = function (pos) {
this.dom.style[this.propertyName] = pos + 'px';
};
var div = document.getElementById('div');
var animate = new Animate(div);
animate.start('left', 500, 1000, 'linear');
// animate.start( 'top', 1500, 500, 'strongEaseIn' );
</script>
</html>
複製程式碼
驗證表單
簡單的實現
<html>
<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>
複製程式碼
使用策略模式改進
<html>
<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 strategies = {
isNonEmpty: function (value, errorMsg) { // 不為空
if (value === '') {
return errorMsg;
}
},
minLength: function (value, length, errorMsg) { // 限制最小長度
if (value.length < length) {
return errorMsg;
}
},
isMobile: function (value, errorMsg) { // 手機號碼格式
if (!/(^1[3|5|8][0-9]{9}$)/.test(value)) {
return errorMsg;
}
}
};
var validataFunc = function () {
var validator = new Validator(); // 建立一個validator 物件
/***************新增一些校驗規則****************/
validator.add(registerForm.userName, 'isNonEmpty', '使用者名稱不能為空');
validator.add(registerForm.password, 'minLength:6', '密碼長度不能少於6 位');
validator.add(registerForm.phoneNumber, 'isMobile', '手機號碼格式不正確');
var errorMsg = validator.start(); // 獲得校驗結果
return errorMsg; // 返回校驗結果
}
var registerForm = document.getElementById('registerForm');
registerForm.onsubmit = function () {
var errorMsg = validataFunc(); // 如果errorMsg 有確切的返回值,說明未通過校驗
if (errorMsg) {
alert(errorMsg);
return false; // 阻止表單提交
}
};
var Validator = function () {
this.cache = []; // 儲存校驗規則
};
Validator.prototype.add = function (dom, rule, errorMsg) {
var ary = rule.split(':'); // 把strategy 和引數分開
this.cache.push(function () { // 把校驗的步驟用空函式包裝起來,並且放入cache
var strategy = ary.shift(); // 使用者挑選的strategy
ary.unshift(dom.value); // 把input 的value 新增進引數列表
ary.push(errorMsg); // 把errorMsg 新增進引數列表
return strategies[strategy].apply(dom, ary);
});
};
Validator.prototype.start = function () {
for (var i = 0, validatorFunc; validatorFunc = this.cache[i++];) {
var msg = validatorFunc(); // 開始校驗,並取得校驗後的返回資訊
if (msg) { // 如果有確切的返回值,說明校驗沒有通過
return msg;
}
}
};
</script>
</body>
</html>
複製程式碼
缺點:一 個文字輸入框只能對應一種校驗規則
再改進:可以有多個校驗規則
<html>
<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 strategies = {
isNonEmpty: function (value, errorMsg) {
if (value === '') {
return errorMsg;
}
},
minLength: function (value, length, errorMsg) {
if (value.length < length) {
return errorMsg;
}
},
isMobile: function (value, errorMsg) {
if (!/(^1[3|5|8][0-9]{9}$)/.test(value)) {
return errorMsg;
}
}
};
/***********************Validator 類**************************/
var Validator = function () {
this.cache = [];
};
Validator.prototype.add = function (dom, rules) {
var self = this;
for (var i = 0, rule; rule = rules[i++];) {
(function (rule) {
var strategyAry = rule.strategy.split(':');
var errorMsg = rule.errorMsg;
self.cache.push(function () {
var strategy = strategyAry.shift();
strategyAry.unshift(dom.value);
strategyAry.push(errorMsg);
return strategies[strategy].apply(dom, strategyAry);
});
})(rule)
}
};
Validator.prototype.start = function () {
for (var i = 0, validatorFunc; validatorFunc = this.cache[i++];) {
var errorMsg = validatorFunc();
if (errorMsg) {
return errorMsg;
}
}
};
/***********************客戶呼叫程式碼**************************/
var registerForm = document.getElementById('registerForm');
var validataFunc = function () {
var validator = new Validator();
validator.add(registerForm.userName, [{
strategy: 'isNonEmpty',
errorMsg: '使用者名稱不能為空'
}, {
strategy: 'minLength:6',
errorMsg: '使用者名稱長度不能小於10 位'
}]);
validator.add(registerForm.password, [{
strategy: 'minLength:6',
errorMsg: '密碼長度不能小於6 位'
}]);
var errorMsg = validator.start();
return errorMsg;
}
registerForm.onsubmit = function () {
var errorMsg = validataFunc();
if (errorMsg) {
alert(errorMsg);
return false;
}
};
</script>
</body>
</html>
複製程式碼