[1]獎金計算 [2]策略模式 [3]緩動動畫 [4]表單校驗 [5]總結 ...
前面的話
在程式設計中,常常遇到類似的情況,要實現某一個功能有多種方案可以選擇。比如一個壓縮文件的程式,既可以選擇zip演算法,也可以選擇gzip演算法。這些演算法靈活多樣,而且可以隨意互相替換。這種解決方案就是本文將要介紹的策略模式。策略模式是指定義一系列的演算法,把它們一個個封裝起來,並且使它們可以相互替換
獎金計算
策略模式有著廣泛的應用。以年終獎的計算為例進行介紹。很多公司的年終獎是根據員工的工資基數和年底績效情況來發放的。例如,績效為S的人年終獎有4倍工資,績效為A的人年終獎有3倍工資,而績效為B的人年終獎是2倍工資
下麵是一個名為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 ); // 輸出:40000 calculateBonus( 'S', 6000 ); // 輸出:24000
這段代碼十分簡單,但是存在著顯而易見的缺點:該函數比較龐大,包含了很多if-else語句,這些語句需要覆蓋所有的邏輯分支;該函數缺乏彈性,如果增加了一種新的績效等級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函數有可能越來越龐大,而且在系統變化的時候缺乏彈性
策略模式指的是定義一系列的演算法,把它們一個個封裝起來。策略模式的目的就是將演算法的使用與演算法的實現分離開來
在這個例子里,演算法的使用方式是不變的,都是根據某個演算法取得計算後的獎金數額。而演算法的實現是各異和變化的,每種績效對應著不同的計算規則
一個基於策略模式的程式至少由兩部分組成。第一個部分是一組策略類,策略類封裝了具體的演算法,並負責具體的計算過程。第二個部分是環境類Context,Context接受客戶的請求,隨後把請求委托給某一個策略類。要做到這點,說明Context中要維持對某個策略對象的引用
下麵用策略模式來重構上面的代碼
//定義策略類 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
策略模式
上面的代碼中,讓strategy對象從各個策略類中創建而來,這是模擬一些傳統面向對象語言的實現。實際上在javascript語言中,函數也是對象,所以更簡單和直接的做法是把strategy直接定義為函數
var strategies = { "S": function( salary ){ return salary * 4; }, "A": function( salary ){ return salary * 3; }, "B": function( salary ){ return salary * 2; } };
同樣,Context也沒有必要必須用Bonus類來表示,用calculateBonus函數充當Context來接受用戶的請求。經過改造,代碼的結構變得更加簡潔
var calculateBonus = function( level, salary ){ return strategies[ level ]( salary ); }; console.log( calculateBonus( 'S', 20000 ) ); // 輸出:80000 console.log( calculateBonus( 'A', 10000 ) ); // 輸出:30000
緩動動畫
通過使用策略模式重構代碼,消除了原程式中大片的條件分支語句。所有跟計算獎金有關的邏輯不再放在Context 中,而是分佈在各個策略對象中。Context並沒有計算獎金的能力,而是把這個職責委托給了某個策略對象。每個策略對象負責的演算法已被各自封裝在對象內部。當對這些策略對象發出“計算獎金”的請求時,它們會返回各自不同的計算結果,這正是對象多態性的體現,也是“它們可以相互替換”的目的。替換 Context 中當前保存的策略對象,便能執行不同的演算法來得到想要的結果
有一段時間網頁游戲非常流行,HTML5版本的游戲可以達到不遜於Flash游戲的效果。用javascript實現動畫效果的原理跟動畫片的製作一樣,動畫片是把一些差距不大的原畫以較快的幀數播放,來達到視覺上的動畫效果。在javascript中,可以通過連續改變元素的某個CSS屬性,比如left、top、background-position來實現動畫效果
目標是編寫一個動畫類和一些緩動演算法,讓小球以各種各樣的緩動效果在頁面中運動。 現在來分析實現這個程式的思路。在運動開始之前,需要提前記錄一些有用的信息,至少包括以下信息:動畫開始時,小球所在的原始位置;小球移動的目標位置;動畫開始時的準確時間點;小球運動持續的時間
隨後,用setInterval創建一個定時器,定時器每隔19ms迴圈一次。在定時器的每一幀里,把動畫已消耗的時間、小球原始位置、小球目標位置和動畫持續的總時間等信息傳入緩動演算法。該演算法會通過這幾個參數,計算出小球當前應該所在的位置。最後再更新該div對應的CSS屬性,小球就能夠順利地運動起來了
在實現完整的功能之前,先瞭解一些常見的緩動演算法,這些演算法最初來自Flash,但可以非常方便地移植到其他語言中。這些演算法都接受4個參數,這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; } };
接下來,開始編寫完整的代碼,首先在頁面中放置一個 div
<div style="position:absolute;background:blue" id="div">我是div</div>
接下來定義Animate類,Animate的構造函數接受一個參數:即將運動起來的dom節點
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方法負責啟動這個動畫,在動畫被啟動的瞬間,要記錄一些信息,供緩動演算法在以後計算小球當前位置的時候使用。在記錄完這些信息之後,此方法還要負責啟動定時器。代碼如下:
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 ); } }, 19 ); };
Animate.prototype.start方法接受以下4個參數
propertyName:要改變的 CSS 屬性名,比如'left'、'top',分別表示左右移動和上下移動。 endPos: 小球運動的目標位置。 duration: 動畫持續時間。 easing: 緩動演算法
再接下來是Animate.prototype.step方法,該方法代表小球運動的每一幀要做的事情。在此處,這個方法負責計算小球的當前位置和調用更新CSS屬性值的方法Animate.prototype.update。代碼如下:
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.step方法返回false,可以通知Animate.prototype.start方法清除定時器
最後是負責更新小球CSS屬性值的Animate.prototype.update方法
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, 'strongEaseOut' );
通過這段代碼,可以看到小球按照期望以各種各樣的緩動演算法在頁面中運動
表單校驗
在一個Web項目中,註冊、登錄、修改用戶信息等功能的實現都離不開提交表單。在將用戶輸入的數據交給後臺之前,常常要做一些客戶端力所能及的校驗工作,比如註冊的時候需要校驗是否填寫了用戶名,密碼的長度是否符合規定,等等。這樣可以避免因為提交不合法數據而帶來的不必要網路開銷。假設正在編寫一個註冊的頁面,在點擊註冊按鈕之前,有如下幾條校驗邏輯:1、用戶名不能為空;2、密碼長度不能少於6位;3、手機號碼必須符合格式
現在編寫表單校驗的第一個版本
<form action="http://xx.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>
這是一種很常見的代碼編寫方式,它的缺點跟計算獎金的最初版本一模一樣:registerForm.onsubmit函數比較龐大,包含了很多if-else語句,這些語句需要覆蓋所有的校驗規則;registerForm.onsubmit函數缺乏彈性,如果增加了一種新的校驗規則,或者想把密碼的長度校驗從6改成8,都必須深入registerForm.onsubmit函數的內部實現,違反開放封閉原則;演算法的復用性差,如果在程式中增加了另外一個表單,這個表單也需要進行一些類似的校驗,那很可能將這些校驗邏輯複製得漫天遍野
下麵用策略模式來重構表單校驗的代碼,很顯然第一步要把這些校驗邏輯都封裝成策略對象
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類。Validator類在這裡作為Context,負責接收用戶的請求並委托給strategy對象
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; // 阻止表單提交 } };
先創建了一個validator對象,然後通過validator.add方法,往validator對象中添加一些校驗規則。validator.add方法接受3個參數,以下麵這句代碼說明:
validator.add(registerForm.password,'minLength:6','密碼長度不能少於6位');
registerForm.password為參與校驗的input輸入框;'minLength:6'是一個以冒號隔開的字元串。冒號前面的minLength代表客戶挑選的strategy對象,冒號後面的數字6表示在校驗過程中所必需的一些參數;'minLength:6'表示校驗registerForm.password這個文本輸入框的value最小長度為6。如果這個字元串中不包含冒號,說明校驗過程中不需要額外的參數信息,比如'isNonEmpty';第3個參數是當校驗未通過時返回的錯誤信息
往validator對象里添加完一系列的校驗規則之後,會調用validator.start()方法來啟動校驗。如果validator.start()返回了一個確切的errorMsg字元串當作返回值,說明該次校驗沒有通過,此時需讓registerForm.onsubmit方法返回false來阻止表單的提交。
最後是 Validator 類的實現:
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; } } };
使用策略模式重構代碼之後,僅僅通過”配置“的方式就可以完成一個表單的校驗, 這些校驗規則也可以復用在程式的任何地方,還能作為插件的形式,方便地被移植到其他項 目中。在修改某個校驗規則的時候,只需要編寫或者改寫少量的代碼。比如想將用戶名輸入框的校驗規則改成用戶名不能少於10 個字元。可以看到,這時候的修改是毫不費力的。代碼如下:
validator.add( registerForm.userName, 'isNonEmpty', '用戶名不能為空' ); // 改成: validator.add( registerForm.userName, 'minLength:10', '用戶名長度不能小於 10 位' );
目前表單校驗實現留有一點小遺憾:一 個文本輸入框只能對應一種校驗規則,比如,用戶名輸入框只能校驗輸入是否為空:
validator.add( registerForm.userName, 'isNonEmpty', '用戶名不能為空' );
如果既想校驗它是否為空,又想校驗它輸入文本的長度不小於 10 呢?期望以這樣的形式進行校驗:
validator.add( registerForm.userName, [{ strategy: 'isNonEmpty', errorMsg: '用戶名不能為空'}, { strategy: 'minLength:6', errorMsg: '用戶名長度不能小於 10 位' }] );
下麵提供的代碼可用於一個文本輸入框對應多種校驗規則:
<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>
總結
策略模式是一種常用且有效的設計模式,它利用組合、委托和多態等技術和思想,可以有效地避免多重條件選擇語句。它提供了對開放—封閉原則的完美支持,將演算法封裝在獨立的strategy中,使得它們易於切換,易於理解,易於擴展。策略模式中的演算法也可以復用在系統的其他地方,從而避免許多重覆的複製粘貼工作。.在策略模式中利用組合和委托來讓Context擁有執行演算法的能力,這也是繼承的一種更輕便的替代方案
當然,策略模式也有一些缺點,但這些缺點並不嚴重。首先,使用策略模式會在程式中增加許多策略類或者策略對象,但實際上這比把它們負責的邏輯堆砌在Context中要好。其次,要使用策略模式,必須瞭解所有的strategy,必須瞭解各個strategy之間的不同點,這樣才能選擇一個合適的strategy