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

第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.    使用策略模式重構代碼

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

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

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

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

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

第二個部分是環境類ContextContext接受客戶請求,隨後把請求委託給一個策略類。要做到這一點,說明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.οnsubmit= 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擁有執行算法的能力,這也是繼承的一種更輕便的替代方案。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章