【學習筆記javascript設計模式與開發實踐(策略模式)—-5】

NO IMAGE

第5章策略模式

 在程式設計中我們往往會遇到實現某一功能有多種方案可以選擇。比如一個壓縮演算法,我們可以選擇zip演算法,也可以選擇gzip演算法。

這些演算法靈活多樣,而且可以隨意互相替換。這種解決方案就是本章要討論的策略模式。

定義:定義一系列的演算法,把它們一個個封裝起來,並且使它們可以相互替換。

 

5.1 使用策略模式計算獎金

1.    最初的程式碼實現

我們可以編寫一個名為calculateBonus的函式來計算每個人的獎金額。很顯然,calculateBonus函式要正確工作,就需要接收兩個引數:員工工資數額和績效考核等級。如下:

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);
calculateBonus(‘C’,6000);

可以看出程式碼十分簡單,但是也存在著顯而易見的缺點。

l   if-else分支多,這些分支要覆蓋所有的邏輯

l   calculateBonus函式缺乏彈性,如果增加了一種新的績效等級C,或是把績效S的獎金係數改為5,那麼我們必須深入calculateBonus函式的內部實現,這違反開放—封閉原則

l   演算法的複用性差,如果在程式的其他地方需要重用這些計算獎金的演算法呢?我們只有複製和貼上。

2.    使用組合函式重構程式碼

一般容易想到的辦法就是使用組合函式來重構程式碼,我們把各種演算法封閉到一個小函式裡面,這些小函式有著良好的全名,可能一目瞭然地知道它對應著哪咱演算法,它們也可以被利用在程式的其他地方:

var performanceS= function(salary){
return salary*4;
}
var performanceA= function(salary){
return salary*3;
}
var performanceB= function(salary){
return salary*2;
}
varcalculateBonus = function(performanceLevel,salary){
if(performanceLevel==”S”){
return performanceS(salary);
}
if(performanceLevel==”A”){
return performanceA(salary);
}
if(performanceLevel==”B”){
return performanceB(salary);
}
}
calculateBonus(‘A’,10000);

目前,我們的程式得到了一定的改善,但這種改善非常有限,我們依然沒有解決最重要的問題:calculateBonus函式有可能越來越龐大,而且在系統變化的時候缺乏彈性。

3.    使用策略模式重構程式碼

策略模式是指定義一系列的演算法,把它們一個個封裝起來。將不變的部分和變化的部分分隔開是每個設計模式的主題:

 策略模式的目的就是將演算法的使用與演算法的實現分離開來。

在上面的例子裡,演算法的使用方式是不變的,都是根據某個演算法取得計算後資金數額。而演算法的實現是各異和變化的,每種績效對應著不同的計算規則。

因此一個策略模式的程式至少由兩部分組成

第一個部分是一組策略類,它封裝了具體的演算法,並負責具體的計算過程。

第二個部分是環境類Context,Context接受客戶請求,隨後把請求委託給一個策略類。要做到這一點,說明Context中要維持對某個策略物件的引用

下面重構上面程式碼,傳統OOP語言中的實現:

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:

//context
var Bonus =function(){
this.salary = null;//原始工資
this.strategy = null; //績效等級對應的策略物件
}
Bonus.prototype.setSalary= function(salary){
}
Bonus.prototype.setStrategy= function(strategy){
this.strategy = strategy; //設定策略物件
}
Bonus.prototype.getBonus= function(){
return this.strategy.calculate(this.salary);//
}

再來回顧一下策略模式的思想:

定義一系列的演算法,把它們一個個封裝起來,並且使它們可以相互替換

在對客戶對Context發起請求的時候,把它們各自封裝成策略類,演算法被封裝在策略類內部的方法裡。在客戶對Context發起請求的時候,Context總是把請求委託給這些策略物件中的某一個進行計算。如下:

var bonus = newBonus();
bonus.setSalary(10000);
bonus.setStrategy(newperformanceS()); //設定策略物件
console.log(bonus.getBonus());//輸出:40000
bonus.setStrategy(newperformance()); //設定策略物件
console.log(bonus.getBonus());//輸出:30000

5.2 javascript版的策略模式

 實際上在javascript語言中,函式也是物件,所以更簡單和直接的做法是把strategy直接定義為物件

var strategies = {
“S”:function(salary){
return salary*4;
},
“A”:function(salary){
return salary*4;
},
“B”:function(salary){
return salary*4;
}
};

同樣,Context也沒有必要必須用Bonus類來表示,我們依然用calculateBonus函式來充當Context來接受使用者請求,如:

var calculateBonus =function(level,salary){
return strategies[level](salary);
}
console.log(calculateBonus(‘S’,20000)); //輸出80000
console.log(calculateBonus(‘S’,10000)); //輸出30000

5.3 多型在策略模式中的體現

通過使用策略模式重構程式碼,我們消除了原程式中大片的條件分支語句。所以跟計算獎金有關的邏輯不在放在Context中,而是分佈在各個策略物件中。Context並沒有計算獎金的能力,而是把這個職責委託給了某個策略物件。

5.4 使用策略模式實現緩動動畫

緩動演算法,最初是來自Flash,但可以非常方便的移植到其它語言中。

這些演算法接受4個引數:分別是動畫已消耗時間、原始位置、目標位置、持續時間。

如下:

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;
}
};

以下程式碼思想來源於jQuery庫,由於本節內容是策略模式,而非編寫一個完整的動畫庫,因此我們省去了動畫的佇列控制等更多完整功能。

定義一個div

<body>
<div style=’position:absolute;background:blue’ id=”div”></div>
</body>

var Animate = function(dom){
this.dom = dom;
this.startTime = 0;
this.startPos = 0;
this.endPos = 0;
this.propertyName = null;
this.easing = null; //緩動演算法
this.duration = null ; //動畫持續時間
}

接下來Animate.prototype.start方法負責啟動這個動畫,在動畫被啟動的瞬間,要記錄一些資訊,供緩動演算法在以後計算當前位置的時候使用(本例中是位置),此方法還負責啟動定時器。

Animate.prototype.start =function(propertyName,endPos,duration,easing){
this.startTime =  new Date; //動畫啟動時間
this.startPos = this.dom.getBoundingClientRect()[propertyName];
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);
}
//呼叫step
},19);
}

propertyName:要改變的CSS屬性名,如‘left’、‘top’分別表示左右移動和上下移動

endPos:小球運動的目標位置

duration:動畫持續時間

easing:緩動演算法

 

再接下來是Animate.prototype.step方法,該方法代表小球運動的每一幀要做的事情。Animate.prototype.update是用來負責計算當前位置和更新位置

Animate.prototype.step = function(){
var t =  new Date;
if(t>=this.startTime this.duration){ //(1)
this.update(this.endPos);
return false;
}
var pos =this.easing(t-this.startTime,this.startPos,this.endPos-this.startPos,this.duration);
this.update(pos);
}

 

(1)註釋的意思,如果當前時間大於開始時間加上動畫持續時間之和,說明動畫已經結束,此時要修正小球的位置。主要用於修正最終的目標位置。 

負責更新CSS屬性值的Animate.prototype.update方法:

Animate.prototype.update = function(pos){
this.dom.style[this.prototypeName]= pos ”px”;
}

可以驗證結果:

var div = document.getElementById(‘div’);
var animate = new Animate(div);
animate.start(‘left’,500,1000,’strongEaseOut’);
//animate.start(‘top’,1500,500,’strongEaseIn’);

5.5 更廣義的“演算法”

從定義上看,策略模式就是用來封裝演算法的。但如果把策略模式僅僅用來封閉演算法,未免大材小用。在實際開發中,我們通常會把演算法的含義擴散開來,使策略模式也可以用來封裝一系列“業務規則”。只要這些業務規則指向的目標一致,並且可以被替換使用,我們就可以用策略模式來封裝它。

5.6 表單校驗的第一個版本

提交表單資料,在資料交給後臺之前,常常要做的一些客戶端力所能及的校驗工作,比如註冊的時候需要校驗是否填寫了使用者名稱,密碼長度等等。這樣可以避免因為提交不合法資料而帶來的不必要網路開銷。

如下: 

<script>
var registerForm= document.getElementById(‘registerForm’);
registerForm.onsubmit= function(){
if(registerForm.userName.value===’’){
alert(‘使用者名稱不能為空’);
}
if(registerForm.password.value.length<6){
alert(‘密碼長度不能少於6位’);
}
if(!/^1[3|5|8][0-9]{9}$/.test(registerForm.phoneNumber.value)){
alert(‘手機號碼格式不正確’);
return false;
}
}
</script>

這是一種常見的程式碼編寫方式,它的缺點跟計算資金的最初版本一模一樣。

registerForm.onsubmit函式比較龐大,包含了很多if-else語句,這些語句需要覆蓋所有的校驗規則

registerForm.onsubmit函式缺乏彈性,如果增加了一種新的校驗規則,或者想把密碼的長度從6改成8,我們必須深入registerForm.onsubmit函式的內部實現,這是違反開放—封閉原則的

演算法的複用性差,如果在程式中增加另一個表單,這個表單也需要進行一些類似的校驗,那我們很可能將這些校驗邏輯複製得漫天野。

5.6.2 用策略模式重構表單校驗

下我們將用策略模式來重構表單校驗,第一步我們要把校驗邏輯都封裝成策略物件:

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]$/.test(value)){
return errorMsg;
}
}
}

接下來我們來準備一個Validator類。它用來做為Context,負責接收使用者的請求並委託給strategy物件。在給出Validator類的程式碼之前,有必要提前瞭解使用者是如何向Validateor類傳送請求的,這有助於我們知道如何去編寫Validator類的程式碼,如下:

var validataFunc = function(){
var validator = new 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(){
varerrorMsg = validataFunc(); //如果errorMsg有確切的返回值,說明未通過校驗
if(errorMsg){
alert(errorMsg);
return false;
}
}<span style="font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);"> </span>

從這段程式碼中可以看到,我們先建立了一個validator物件,然後通過validator.add方法,往validator物件中新增一些校驗規則。validator.add方法接受3個引數,元素、規則、提示資訊。

具體實現如下:

var Validator = function(){
this.cache = [];
}
Validator.prototype.add =function(dom,rule,errorMsg){
var ary = rule.split(‘:’);
this.cache.push(function(){
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;
}
}
}

使用策略模式重構程式碼之後,我們僅僅通過“配置”的方式就可以完成一個表單的校驗,這些校驗規則也可以複用在程式的任何地方,還能作為外掛的形式,方便地被移植到其它專案中。

在修改某個校驗規則的時候,只需要編寫或者改寫少量的程式碼。比如我們想將使用者名稱輸入框的校驗規則改成使用者名稱不能少於4個字元,可以看到,這時候的修改是毫不費力的如下:

validator.add(registerForm.userName,’isNonEmpty’,’使用者名稱不能為空’);
//改成:
validator.add(registerForm.userName,’minLength:10’,’使用者名稱長度不能小於10位’);

5.6.3 給某個文字輸入框新增多種校驗規則

為了讓讀者把注意力放在策略模式的使用上,目前我們的表單校驗實現留有一點小遺憾:一個文字輸入框只能對應一種校驗規則,比如,使用者名稱輸入框只能校驗輸入是否為空:

validator.add(registerForm.userName,’isNonEmpty’,’使用者名稱不能為空’);

如果我們既想校驗它是否為空,又想校驗它輸入文字的長度不小於10怎麼辦,我們期望以如下的形式進行校驗:

validator.add(
     registerForm.userName,
     [
       {strategty:’isNonEmpty’,errorMsg:’使用者名稱不能為空’}
       ,{strategy:’minLength:6’,errorMsg:’使用者名稱長度不能小於10位’}
     ]
 );

如下:

<html>
<body>
<form action=”” id = “registerForm”method=”post”>
請輸入使用者名稱:<input type=’text’ name=’userName’ />
請輸入密碼:<input type=’text’ name = ‘password’ />
請輸入手機號碼:<input type=’text’ name = ‘phoneNumber’ />
</form>
</body>
</html><span style="font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);"> </span>

/*************策略物件************/

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);
});
})(rule)
} //end for
};
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:10’,
errorMsg:’使用者名稱長度不能小於10’
}
]);
validator.add(registerForm.password,[
{
strategy:’minLength:6’,
errorMsg:’密碼長度不能小於6’
}
]);
validator.add(registerForm.phoneNumber,[
{
strategy:’isMobile’,
errorMsg:’手機號碼格式不正確’
}
]);
var errorMsg =validator.start();
return errorMsg;
}
registerForm.onsubmit = function(){
var errorMsg =validataFunc();
if(errorMsg){
alert(errorMsg);
return false;
}
}

5.7 策略模式的優缺點

策略模式是一種常用且有效的設計模式,本章提供了計算獎金、緩動動畫、表單校驗這三個例子來加深對策略模式的理解。

優點:

l   有利於組合、委託和多型等技術和思想,可以有效地避免多重條件選擇語句

l   提供了對開放—-封閉原則的完美支援,將演算法封裝在獨立的strategy中,使得它們易於切換,易於理解,易於擴充套件。

l   策略模式中的演算法也可以利用在系統的其他地方,從而避免許多重複的複製貼上工作

l   在策略模式中利用組合和委託來讓Context擁有執行演算法的能力,這也是繼承的一種更輕便的替代方案。