javascript設計模式 之 2 策略模式

zhaoyezi發表於2018-05-29

1 策略模式的定義

定義一系列的演算法,把它們一個個封裝起來,並且使用它們可以相互替換。 舉個例子:在程式設計中,我們需要實現某一個功能其實有多種方案可以選擇,例如壓縮檔案的程式,我們可以選擇zip的演算法,也可以選擇gzip的演算法。這些演算法靈活多樣,而且可以隨意相互替換。

2 使用策略模式計算獎金

在單位年終的時候,會對職員進行年終評價,不同等級獲得不一樣的獎金。A級獲得4倍工資,B級獲得3倍工資,C級獲得2倍工資,D級只有一倍工資。我們需要設計一個計算年終的函式。

function calculate = function(salary, level) {
    switch(level) {
        case 'A':
            return salary * 4;
        case 'B':
            return salary * 3;
        case 'C':
            return salary * 2;
        default:  
            return salary;
    }
}
複製程式碼

上面是一般我們會選擇的計算方式,這樣設計有缺點:

  • calculate的函式比較大,Swicth-case中需要覆蓋所有的邏輯分支
  • calculate的函式缺乏彈性,如果需要新增一種等級D,那麼就需要修改calculate函式內部的實現。
  • 演算法的複用性差,如果程式的其他地方需要重用這些計算獎金的演算法,我們只能選擇貼上複製

2.1 使用策略模式重構程式碼

策略模式是定義一系列的演算法,把它們一個個封裝起來。將不變的部分變化的部分隔開是每個設計模式的主題,策略模式也不例外,策略模式的目的就是將演算法的使用演算法的實現分離開來。例子中,可以按一下方式理解:

  • 演算法的實現是變化的,每種績效對應的不同的計算規則
  • 演算法的使用方式是不變的,都是根據某個演算法取得計算後的獎金數額

策略模式的程式至少由兩部分組成:

  • 策略類: 封裝具體的演算法,並負責具體的計算過程
  • 環境類Context: 接收使用者請求,隨後將請求委託給策略類

2.1.1 模仿物件導向的方式實現

  • performanceA, performanceB, performanceC, performanceOther :都是可變的策略類,封裝計算規則。
  • Bonus: 環境類部分,接收使用者請求,委託給策略類進行計算
// 策略類
var performanceA = function() {}
performanceA.calculate = function(salary) {
    return salary * 4;
}

var performanceB = function() {}
performanceB.calculate = function(salary) {
    return salary * 3;
}

var performanceC = function() {}
performanceC.calculate = function(salary) {
    return salary * 2;
}

var performanceOther = function() {}
performanceOther.calculate = function(salary) {
    return salary;
}

// 環境類Context
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);
}


複製程式碼

2.1.2 javascript版本的策略模式

上面strategy物件是從各個策略類中建立出來的,那是模擬傳統的面嚮物件語言實現的。在javascript中,函式也就是物件,所以能夠更加簡單和直接的把strategy直接定義為函式。

var strategies = {
    'A': function(salary) {
        return 4 * salary;
    },
    'B': function(salary) {
        return 3 * salary;
    },
    'C': function(salary) {
        return 2 * salary;
    }
}

var calculateBonus = function(salary, level) {
    return strategies[level](salary);
}
複製程式碼

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

通過上面的重構,我們消除了switch分支的條件語句。把計算獎金的邏輯不再放入到context中,而是分佈在各個策略物件中。

  • 每個策略物件負責的演算法被各自封在了物件內部
  • Context沒有計算獎金的能力,通過職責委託給了某個策略物件
    當對這些策略發起請求時計算獎金時,會根據各自不同的計算返回不同的結果,而這也是物件多型的體現。也是它們能夠相互替換的目的。替換context中的當前儲存的策略,遍能夠知曉不同的演算法來得到我們想要的結果。

3 使用策略模式進行表單校驗

我們在編寫註冊介面的時候,點選註冊按鈕前需要對錶單進行校驗工作:

  • 使用者名稱不能為空
  • 密碼長度不能少於6位
  • 手機號碼必須符合格式

3.1 普通實現

首先我們不使用策略模式進行實現。該實現方式與計算獎金的實現問題一模一樣。

  • registerForm.onSubmit函式很龐大,包含了if-else, 包含了所有的校驗規則
  • registerForm.onSubmit函式缺乏彈性,如果想新增一個校驗規則,或則修改規則,那麼就需要深入到該函式的內部實現。違背了開放-封閉原則
  • 演算法複用性差。如果專案的其他位置也需要相同的校驗,需要拷貝複製
<html>
    <body>
    <form name="registerForm">
        請輸入使用者名稱: <input type="text" name="userName"/ >
        請輸入密碼: <input type="text" name="password"/ >
        請輸入手機號碼: <input type="text" name="phoneNumber"/ >
        <button>提交</button>
    </form>
    <script>
        var registerForm = document.forms['registerForm'];
        registerForm.onsubmit = function(event){
            if ( registerForm.userName.value === '' ){
                alert ( '使用者名稱不能為空' );
                event.preventDefault();
            }
            if ( registerForm.password.value.length < 6 ){
                alert ( '密碼長度不能少於 6 位' );
                 event.preventDefault();
            }
            if ( !/(^1[3|5|8][0-9]{9}$)/.test( registerForm.phoneNumber.value ) ){
                alert ( '手機號碼格式不正確' );
                 event.preventDefault();
            }
        }
    </script>
    </body>
</html>
複製程式碼

3.2 使用策略模式重構

我們需要遵循的規則,依然是這兩條:

  • 提取所有的可變原則,將校驗規則封裝起來作為策略類
  • 提取context內容,接收使用者請求,通過委託給策略類進行計算 下面,我們實現的內容需求:
  • 呼叫validate.add()方法:新增校驗規則(引數1:需要校驗的字串, 引數2:校驗的規則陣列,引數4:可選的正則)
  • 呼叫valiadte.start()方法:開始校驗
<html>
    <body>
    <form name="registerForm">
        請輸入使用者名稱: <input type="text" name="userName"/ >
        請輸入密碼: <input type="text" name="password"/ >
        請輸入手機號碼: <input type="text" name="phoneNumber"/ >
        <button>提交</button>
    </form>
    <script>
    // 策略類
    var strategies = {
        isNotEmpty: function(str, errorMsg) {
            if (str === '') {
                return errorMsg;
            }
        },
        minLength: function(str, errorMsg, length) {
            if (str.length < length) {
                return errorMsg;
            }
        },
        isRegExp: function(str, errorMsg, regExp) {
            if (!regExp.test(str)) {
                return errorMsg;
            }
        }
    }

    // context類: 負責接收使用者傳入的請求,並委託給策略類。不可變
    var Validate =  function() {
        var cache = [];
        return {
            add: function(str, rules, regExp) {
                rules.map(function(rule) {
					var key = Object.keys(rule)[0];
                    var errorMsg = rule[key];
                    var ary = key.split(':');
					console.log(key, ary);
                    cache.push(function() {
                        // 加入有:分割,第一個則是策略
                        var strategy = ary.shift();
                        return strategies[strategy].call(null, str, errorMsg, regExp || ary.shift());
                    });
                }); 
               
            },
            start: function() {
				var msg = '';
				for (var i = 0; i < cache.length ; i++) {
					 msg = cache[i]();
					if (msg) {
                        alert(msg);
						break;
					}
				}
				return msg;
            }
        }
    };
    
    function validateRegister(registerForm) {
        var validate = Validate();
        validate.add(registerForm.userName.value, [{'isNotEmpty': '使用者名稱不能為空'},{'minLength:3':'密碼長度不能少於 3 位'}]);
        validate.add(registerForm.password.value, [{'minLength:6':'密碼長度不能少於 6 位'}]);
        validate.add(registerForm.phoneNumber.value, [{'isRegExp':'手機號碼格式不正確'}], /(^1[3|5|8][0-9]{9}$)/);
        var returnMsg = validate.start();
        return returnMsg ? false : true;
    }
    registerForm.onsubmit = function(event){
        var isPass = validateRegister(registerForm);
        if (!isPass) {
			event.preventDefault();
             console.log('no validate');
        } else {
           console.log('pass');
		   registerForm.submit();
        }
    }
    </script>
    </body>
</html>
複製程式碼

4 策略模式的優缺點

策略模式有點:

  • 利用組合,委託和多型等技術和思想,可以有效避免多重條件選擇語句
  • 提供了對外開放-封閉的原則的完美支援。將演算法封裝在獨立的strategy內,使得它們容易切換,易於理解和擴充
  • 策略模式中的演算法可以提供給其他地方,避免了重複貼上複製
  • 利用組合與委託讓context擁有執行演算法的能力。這也是繼承的一種更輕便的替代方案

缺點:

  • 使用策略模式會讓程式增加許多策略類或者策略物件。
  • 使用策略模式,必須要了解所有的strategy之前的不同點,這樣才能選擇一個適合的strategy。

相關文章