Javascript策略模式理解以及應用

arzh發表於2018-12-17

最近一直在看Javascript設計模式,想通過寫文章來增加自己對策略模式的理解,同時記錄自己學習的經歷。希望大家看完之後覺得有收穫可以幫忙點個贊表示支援。

策略模式的定義

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

每次遇到這種設計模式的定義,第一眼的感覺總是很懵逼,不知所云。其實有一個辦法:素質三連問。我們可以把定義細化,然後分析對應每個欄位的含義,組合起來,就能明白定義的真正想表達的意思。

所以針對策略模式的定義我們就可以來一波素質三連問:

  1. 這裡的演算法是指什麼
  2. 為啥需要一個個封裝起來
  3. 相互替換又是指啥

在回答素質三連問之前,我們可以從生活中入手來看策略模式的應用場景。其實很多場景我們都可以使用到不同到策略來解決問題。

旅遊

  1. 如果你是土豪或者時間緊,可以選擇搭飛機
  2. 如果你是小資生活,也不趕時間,可以選擇搭高鐵
  3. 如果你是窮遊,那你可以選擇騎自行車等等

壓縮檔案

  1. zip演算法
  2. gzip演算法

由此可以看出,其實我們的策略就是解決我們的問題的一種方法,這種方法我們可以定義為一種演算法。

策略模式的應用

講了這麼多,其實大家最關心的還是策略模式的應用場景。接下來我們用年終獎的例子為大家一步一步解決我們的素質三連問。年終獎是根據員工的工資基數以及年底績效情況來發放的。如:績效為S的年終獎有4倍工資、績效為A的年終獎有3倍工資,績效為B的年終獎只能有2倍的工資。這種邏輯我們可以用基本程式碼實現

var calculateBonus = function (performanceLevel, salary) {
    if (performanceLevel === 'S') {
        return salary * 4;
    };
    if (performanceLevel === 'A') {
        return salary * 3;
    };
    if (performanceLevel === 'B') {
        return salary * 2;
    };
}

//獲得績效為B的員工
calculateBonus('B', 20000);
//獲得績效為S的員工
calculateBonus('S', 6000);
複製程式碼

顯而易見,這段程式碼雖然實現了我們想要的功能,但是很侷限,有以下幾種缺點

  1. 包含了過多的if-else語句,使函式過於龐大
  2. calculateBonus函式缺乏彈性,如果需要新增加不同的績效等級,需要更改其內部實現,違反了開放-封閉原則
  3. 演算法複用性差,如果其他地方需要用到這種計算規則,只能重新輸出(複製、貼上)

優化一(組合函式)

我們可以通過組合函式來重構這段程式碼,把各種演算法(即年終獎的計算規則)封裝到一個個獨立的小函式中,同時給個良好的命名,那麼我們就可以解決上面的缺點3演算法複用的問題。程式碼如下

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

複製程式碼

通過組合函式,我們可以看出我們的演算法被封裝成一個個小函式,從而可以解決函式複用的問題。但是我們的核心問題,也就是上述的缺點1、缺點2並沒有解決,以此我們繼續進行改進,這次通過策略模式來優化。

優化二(策略模式)

首先我們應該瞭解將不變的部分和變化的部分隔開是每個設計模式的主題,而策略模式的目的就是將演算法的使用和演算法的實現分離開來。那麼在此例子中,演算法的使用方式是不變的,根據某個演算法計算獎金數額,但是演算法的實現是可變的,如績效S、A的實現方式。

策略模式的組成:

  1. 策略類,策略類封裝了具體的演算法(績效的計算方式),並負責具體的計算過程。
  2. 環境類(Context),Context接受客戶的請求,隨後把請求委託給某一個策略類。
  3. 橋樑,Context中要維持對某個策略物件的引用
//策略類(S)
var performanceS = function () {}
//演算法S內部具體實現
performanceS.prototype.calculate = function ( salary ) {
    return salary * 4;
}
//策略類(A)
var performanceA = function () {}
//演算法A內部具體實現
performanceA.prototype.calculate = function ( salary ) {
    return salary * 3;
}
//策略類(B)
var performanceB = function () {}
//演算法B內部具體實現
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.portotype.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() ); //把請求委託給了之前儲存好的策略物件
複製程式碼

以上的例子展示了策略模式的應用,使程式碼變得更加清晰,各個類職責更加鮮明,也解決了以上普通函式呼叫的缺點一、缺點二。那麼這種類的實現其實是基於傳統的面嚮物件語言模仿的,因此我們可以進一步對這段程式碼進行優化,變成JavaScript版本的策略模式

優化三(JavaScript版本策略模式)

為什麼JavaScript版本的策略模式跟傳統的面嚮物件語言的策略模式不同呢,實際上在JavaScript語言中,函式也是物件,所以可以直接把strategy類直接定義為函式。程式碼如下:

//策略物件
var strategies = {
    //一系列演算法
    "S" : function ( salary ) {
        return salary * 4;
    },
    "A" : function ( salary ) {
        return salary * 3;
    },
    "B" : function ( salary ) {
        return salary * 2;
    }
};
複製程式碼

同樣,我們也可以直接用calculateBonus函式充當Context來接受使用者的請求,並不需要Bonus類來表示

var calculateBonus = function ( level, salary) {
    return strategies[ level ]( salary );    
}

console.log( calculateBonus('S',20000));
複製程式碼

從以上的例子我們其實已經回答了策略模式的定義中的素質三連問,策略模式的演算法是指的什麼(績效的計算方法)、為什麼要封裝(可複用)、相互替換又是指啥(績效可發生變化、但是不影響函式的呼叫,只需改變引數)

延伸擴充套件

上述一直在定義策略模式中演算法的概念,實際開發中,我們通常可以把演算法的含義擴散開來,使得策略模式也可以用來封裝一系列的'業務規則'。只要這些業務規則指向的目標一致,並且可以被替換使用,我們就可以用策略模式來封裝它們 那麼在我們的'業務規則'中,表單的校驗就符合我們使用策略模式。

表單驗證

假設我們正在編寫一個註冊頁面,在點選按鈕之前,有如下幾條校驗規則:

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

根據這樣的要求,在我們沒有引入策略模式之前,我們可以通過如下程式碼編寫

<html>
    <body>
        <form action='xxx.com' 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>
複製程式碼

這樣的程式碼同樣有著跟上述年終獎一樣的缺點,函式過於龐大、缺乏彈性以及複用性差,那麼我們學了策略模式,肯定需要對這種情況進行優化

優化一

  1. 明確在此場景中,演算法具體是什麼,很明顯可以看出,這裡的演算法指的就是我們表單驗證邏輯的業務規則。因此我們可以把這些業務規則封裝成相對應的策略物件:
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;
        }
    ]
}
複製程式碼

實現了策略物件的演算法,那麼我們還需要一個環境類來負責接受使用者的請求並委託給strategy物件。但是在我們實現之前,我們需要明白環境類與策略物件直接的橋樑是怎麼樣的,也就是使用者是如何向validator類傳送請求的。這樣可以方便我們實現環境類,也就是這裡的Validator類。 如下是我們使用者向validator類傳送請求的程式碼:

var validataFunc = function () {
    //建立一個validator物件
    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 () {
    var errorMsg = validataFunc();   //如果存在,則說明未通過校驗
    if ( errorMsg ) {
        alert( errorMsg );
        return false; //阻止表單提交
    }
}
複製程式碼

從上述程式碼中,我們可以明確在我們的Validator中有add方法,通過add方法來新增校驗規則,同時有start方法,通過start方法開始我們的校驗,如果有錯誤,那麼就返回錯誤資訊(errorMsg) 有了策略物件以及策略物件與環境類(Validator)的橋樑,我們便可以寫出我們的Validator類程式碼

var validator = function () {
    this.cache = [];  //儲存校驗規則
};
//新增檢驗規則函式
validator.prototype.add = function (dom, rule, errorMsg) {
    //把strategy和引數分開'minLength:6''minLength:6' -> ["minLength", "6"]
    var ary = rule.split(':'); 
    this.cache.push ( function () {
        var strategy = ary.shift(); //使用者挑選的strategy ["minLength", "6"] -> 'minLength' 
        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 ) {  //如果msg存在,則說明校驗不通過
            return msg; 
        }
    }
}
複製程式碼

在上述中,我們通過對業務規則這種演算法的抽象,通過策略模式來完成我們的表單檢驗,在修改某個校驗規則的時候,我們只有修改少量程式碼即可。如我們想把使用者名稱的輸入改成不能少於4個字元,只需要把我們的minLength:6改為minLength:4即可

優化二(多個校驗規則)

其實到這裡為止,我們的策略模式的理解以及應用的基本概念都已經通過上述的例子闡述完畢了,但是目前我們實現的表單校驗有一點小瑕疵,就是我們一個文字輸入框只有對應一種校驗規則。那麼如果我們想要新增多種檢驗規則,可以通過以下方式新增:

validator.add( registerForm.userName, [{
    strategy: 'isNonEmpty',
    errorMsg: '使用者名稱不能為空'
},{
    strategy: 'minLength:10',
    errorMsg: '使用者名稱長度不能小於10位'
}])
複製程式碼

那我們可以修改我們的Validator中的add方法,通過遍歷的方式,把我們的多個檢驗規則新增到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 )
    }
};
複製程式碼

策略模式的優缺點

從上述的例子中,很明顯能總結出策略模式的優點

  1. 採用組合、委託和多型等技術和思想、有效避免了多重條件選擇語句
  2. 採用了開放-封閉原則,將演算法封裝在獨立的strategy中,易於理解、切換、擴充
  3. 策略模式中的演算法可以進行復用,從而避免很多地方的複製貼上

同時策略模式也有其缺點,但是並不影響我們對策略模式的使用

  1. 在策略模式中,我們會增加很多策略類、策略物件
  2. 要使用策略模式,我們必須瞭解到所有的strategy、必須瞭解各個strategy之間的不同點,才能選擇一個適合的strategy。

結語

大家看完之後,如果覺得有啥不對的地方,請大家提出建議。也希望這篇文章如果對你有幫助,請大家多多點贊、轉發支援!

文章借鑑於:曾探老師的《JavaScript設計模式與開發實踐

相關文章