JS常用設(shè)計模式解析02-策略模式

1.簡介

在于都本文之前介劫,希望大家能夠先閱讀以下JS進階系列03-JS面向?qū)ο蟮娜筇卣髦鄳B(tài)這篇文章案淋,了解JS的多態(tài)。在這篇文章踢京,我們舉了一個例子瓣距,就是選拔官員選拔合唱團成員時,他并不需要提前知道所有的成員在唱歌時具體會發(fā)出什么聲音蹈丸。他關(guān)注的只是,他發(fā)出命令“唱”時慨默,合唱團成員就會開始唱歌弧腥。至于每個成員具體唱什么,交給他們自己好了虾攻。
這其實就是一個典型的策略模式更鲁,當我們在定義一個方法時,如果涉及到了太多的條件分支時澡为,就應該思考一下,這些分支有沒有必要定義在這個方法中顶别。更準確地說,這個方法是不是需要提前知道所有的規(guī)則完慧,這些規(guī)則是不是固定不會改變的剩失。如果答案是否,那么你可以考慮將這些具體的規(guī)則剝離出來脾歧,交給傳入的參數(shù)去實現(xiàn)演熟,方法主體只需要關(guān)注你不變的目的即可。
策略模式的定義是:定義一系列的算法,把他們一個個封裝起來免猾,并且使他們可以互相替換。不過實際業(yè)務(wù)中获三,策略模式并不只是封裝算法锨苏,如果一系列業(yè)務(wù)規(guī)則指向目標一致,并且可以被互相替換使用贞谓,我們都可以用策略模式來封裝它們葵诈。下面我們舉幾個策略模式的使用場景,讓大家詳細體會一下理疙。

2. 使用策略模式計算獎金

試想如下場景:公司hr需要你來為其編寫一段代碼泞坦,用來計算年底為每個員工發(fā)放的獎金。規(guī)則是:考核A的員工發(fā)五倍月工資赃梧,考核B的員工發(fā)三倍月工資,考核C的員工只有一個月工資几睛。你很自然會寫出如下代碼:

function calculateBonus(level, salary) {
    if(level === 'A') {
        return salary * 5;
    } else if (level === 'B') {
        return salary * 3;
    } else if (level === 'C') {
        return salary;
    }
}

calculateBonus('A', 10000);  // 50000

這個函數(shù)接收兩個參數(shù)粤攒,考核等級level和月工資salary,按照不同的等級匹配不同的獎金計算規(guī)則并返回焕济。這要求我們在函數(shù)中將所有現(xiàn)階段可能出現(xiàn)的規(guī)則都列出來盔几,并且當未來對規(guī)則有刪減或者改動時,都需要重新修改該方法邏輯上鞠,這樣的方法在擴展性和可維護性上顯然是不好的芯丧。

我們考慮一下對這段代碼進行改寫,我們的目的是為了讓hr一調(diào)用該方法就能正確輸出獎金數(shù)目谴咸,而具體如何產(chǎn)生獎金骗露,這個方法并不需要去關(guān)心。所以我們可以將不變的目的隔離出來珊随,將可變的計算獎金規(guī)則封裝起來驹暑。如下:

function levelA() {}
levelA.prototype.calculate = function(salary) {
    return salary * 5;
};
function levelB() {}
levelB.prototype.calculate = function(salary) {
    return salary * 3;
};
function Bonus(salary, strategy) {
    this.salary = salary;
    this.strategy = strategy;
}
Bonus.prototype.getBonus = function() {
    return this.strategy.calculate(this.salary);
};

var bonus = new Bonus(10000, new levelA);
bonus.getBonus();  // 50000

我們最終需要使用的是Bonus類优俘,只需要向其傳入salary,和考核人員所屬的類別帆焕,然后調(diào)用其getBonus() 方法即可。這個是典型的模仿傳統(tǒng)的面向?qū)ο蟮膶崿F(xiàn)方式财饥,JS是無類的钥星,其實現(xiàn)方式更為簡單。

var strategies = {
    'A': function(salary) {
        return salary * 5;
    },
    'B': function(salary) {
        return salary * 3;
    },
    'C': function(salary) {
        return salary;
    }
};
function caculateBonus(salary, level) {
    return strategies['level'](salary);
}

caculateBonus(10000, 'A');  // 50000

3. 使用策略模式實現(xiàn)表單校驗

表單校驗是一個很常見的需求贯莺,假設(shè)你需要為一個網(wǎng)站編寫注冊模塊宁改。用戶需要輸入用戶名,密碼和手機號以后點擊注冊按鈕進行注冊爹耗,在向后臺發(fā)起請求前谜喊,需要在前端校驗客戶輸入的合法性:用戶名不能為空,密碼長度不能少于6位讼溺,手機號碼必須符合格式最易。
先來看第一版實現(xiàn):

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>表單校驗01</title>
</head>
<body>
<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 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. registerForm.onsubmit 函數(shù)比較龐大视译,包含了很多if-else語句,這些語句需要覆蓋所有的校驗規(guī)則鄙早。
  2. registerForm.onsubmit 函數(shù)缺乏彈性椅亚,如果增加了一種新的校驗規(guī)則,或者想把密碼的校驗長度從6改為8弥虐,我們都必須深入registerForm.onsubmit 函數(shù)的內(nèi)部實現(xiàn),這是違反開放-封閉原則的珠插。
  3. 算法的復用性差颖对,如果在程序中增加了另外一個表單,這個表單也需要進行一些類似的校驗顾患,那么我們很可能隨處都可見這些校驗邏輯規(guī)則的復制训堆。

下面,我們使用策略模式來實現(xiàn)表單校驗膘流。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>表單校驗02</title>
</head>
<body>
<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 strageties = {
        isNonEmpty: function (value, errMsg) {
            if (value === '') {
                return errMsg;
            }
        },
        minLength: function (value, length, errMsg) {
            if (value.length < length) {
                return errMsg;
            }
        },
        isMobile: function (value, errMsg) {
            if(!/^1[3|5|8][0-9]{9}$/.test(value)) {
                return errMsg;
            }
        }
    };

    // 校驗規(guī)則
    function Validate() {
        this.cache = [];    // 保存校驗規(guī)則
    }
    Validate.prototype.add = function (dom, rule, errMsg) {
        var array = rule.split(':');    //獲取用戶通過key:value指定的規(guī)則
        this.cache.push(function () {   // 把校驗步驟用空函數(shù)包起來呼股,并依次放入cache
            var stragety = array.shift();   // 取出用戶指定的stragety
            array.unshift(dom.value);   // 將input的value放到參數(shù)列表最前面
            array.push(errMsg); //  將errMsg放到參數(shù)列表最后面
            return strageties[stragety].apply(dom, array);  // 實際執(zhí)行時傳給指定stragety的參數(shù)為[dom.value,rule.value,errMsg]
        })
    };
    Validate.prototype.start = function () {
        for (var i=0,validateFunc; validateFunc = this.cache[i++];) {
            var errMsg = validateFunc();
            if (errMsg) {
                return errMsg;
            }
        }
    };

    var registerForm = document.getElementById('registerForm');
    var validateFunc = function() {
        var validate = new Validate();
        validate.add(registerForm.userName, 'isNonEmpty', '用戶名不能為空');
        validate.add(registerForm.passWord, 'minLength:6', '密碼長度不小于6位');
        validate.add(registerForm.phoneNumber, 'isMobile', '手機號碼格式不正確');

        var errMsg = validate.start();
        return errMsg;
    };

    registerForm.onsubmit = function () {
        var errMsg = validateFunc();
        if (errMsg) {
            alert(errMsg);
            return false;
        }
    };
</script>
</body>
</html>

這樣改寫代碼以后彭谁,如果我們需要為某個輸入框加入指定規(guī)則允扇,只需要調(diào)用add方法即可,靈活且便與擴展狭园。美中不足的是糊治,如果我們要為一個輸入框添加多個規(guī)則時,需要重復調(diào)用多次add绎谦。那么有沒有辦法粥脚,只用調(diào)用一次add就可以為輸入框添加多種規(guī)則呢?

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>表單校驗03</title>
</head>
<body>
<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 strageties = {
        isNonEmpty: function (value, errMsg) {
            if (value === '') {
                return errMsg;
            }
        },
        minLength: function (value, length, errMsg) {
            if (value.length < length) {
                return errMsg;
            }
        },
        isMobile: function (value, errMsg) {
            if(!/^1[3|5|8][0-9]{9}$/.test(value)) {
                return errMsg;
            }
        }
    };

    // 校驗規(guī)則
    function Validate() {
        this.cache = [];    // 保存校驗規(guī)則
    }
    Validate.prototype.add = function (dom, rules) {
        var self = this;
        for(var i = 0,rule; rule=rules[i++];) {
            (function(rule){
                var array = rule.stragety.split(':');
                var errMsg = rule.errMsg;
                self.cache.push(function () {
                    var stragety = array.shift();   // 取出用戶指定的stragety
                    array.unshift(dom.value);   // 將input的value放到參數(shù)列表最前面
                    array.push(errMsg); //  將errMsg放到參數(shù)列表最后面
                    return strageties[stragety].apply(dom, array);  // 實際執(zhí)行時傳給指定stragety的參數(shù)為[dom.value,rule.value,errMsg]
                })
            })(rule)
        }
    };
    Validate.prototype.start = function () {
        for (var i=0,validateFunc; validateFunc = this.cache[i++];) {
            var errMsg = validateFunc();
            if (errMsg) {
                return errMsg;
            }
        }
    };

    var registerForm = document.getElementById('registerForm');
    var validateFunc = function() {
        var validate = new Validate();
        validate.add(registerForm.userName, [{stragety: 'isNonEmpty',errMsg: '用戶名不能為空'},{stragety:'minLength:6', errMsg: '用戶名長度不能小于6'}]);
        validate.add(registerForm.passWord, [{stragety:'minLength:6', errMsg: '密碼長度不小于6位'}]);
        validate.add(registerForm.phoneNumber,[{stragety: 'isMobile', errMsg: '手機號碼格式不正確'}]);

        var errMsg = validate.start();
        return errMsg;
    };

    registerForm.onsubmit = function () {
        var errMsg = validateFunc();
        if (errMsg) {
            alert(errMsg);
            return false;
        }
    };
</script>
</body>
</html>

4. 動態(tài)類型下的策略模式

我們說過赃蛛,JS是動態(tài)類型的搀菩,函數(shù)接受的參數(shù)并沒有限制類型肪跋,所以,我們其實不必要把策略都封裝在一個對象中州既。比如,計算bonus的題目阐虚,我們可以這樣修改:

var A = function(salary) {
        return salary * 5;
    },
var B = function(salary) {
        return salary * 3;
    },
var C = function(salary) {
        return salary;
    }
function caculateBonus(salary, level) {
    return strategies['level'](salary);
}

caculateBonus(10000, 'A');  // 50000

5. 策略模式的優(yōu)缺點和使用

策略模式的優(yōu)點:

  1. 策略模式利用組合实束,委托和多態(tài)等技術(shù)思想逊彭,可以避免多重條件語句。
  2. 策略模式提供了對開放-封閉原則的完美支持侮叮,將算法封裝在獨立的stragety中,使得它們易于切換囊榜,理解和擴展。
  3. 策略模式中算法也可以在其他地方復用歹嘹,避免冗余代碼孔庭。
  4. 策略模式利用組合和委托是的Context具有執(zhí)行算法的能力材蛛,這也是繼承一種更輕便的替代方案。

策略模式的缺點:

  1. 使用策略模式會增加許多策略類或者策略對象芽淡。不過這要比將它們的邏輯堆砌在Context要好豆赏。
  2. 要使用stragety富稻,必須要了解所有stragety的細節(jié)椭赋。此時stragety向客戶暴露了其實現(xiàn)或杠,這是有違最少知識原則的。

總體來說向抢,使用策略模式來消除眾多的條件分支是利大于弊的挟鸠。在JS中,使用策略模式有時是隱形的艘希,不必要將策略放在特殊的類或者對象中枢冤,其策略往往是一個個單獨的函數(shù)。合理選用策略模式淹真,會讓我們的代碼更加靈活且易于擴展核蘸。

參考

BOOK-《JavaScript設(shè)計模式與開發(fā)實踐》 第5章

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市祟峦,隨后出現(xiàn)的幾起案子徙鱼,更是在濱河造成了極大的恐慌,老刑警劉巖厌衙,帶你破解...
    沈念sama閱讀 217,657評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件绞绒,死亡現(xiàn)場離奇詭異,居然都是意外死亡喻杈,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,889評論 3 394
  • 文/潘曉璐 我一進店門缴啡,熙熙樓的掌柜王于貴愁眉苦臉地迎上來盟猖,“玉大人换棚,你說我怎么就攤上這事」淘椋” “怎么了夕玩?”我有些...
    開封第一講書人閱讀 164,057評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長禽作。 經(jīng)常有香客問我揩页,道長,這世上最難降的妖魔是什么萍程? 我笑而不...
    開封第一講書人閱讀 58,509評論 1 293
  • 正文 為了忘掉前任兔仰,我火速辦了婚禮,結(jié)果婚禮上忍法,老公的妹妹穿的比我還像新娘榕吼。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 67,562評論 6 392
  • 文/花漫 我一把揭開白布度宦。 她就那樣靜靜地躺著,像睡著了一般离唬。 火紅的嫁衣襯著肌膚如雪划鸽。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,443評論 1 302
  • 那天嫂用,我揣著相機與錄音丈冬,去河邊找鬼。 笑死往弓,一個胖子當著我的面吹牛蓄氧,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播撇寞,決...
    沈念sama閱讀 40,251評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼重抖,長吁一口氣:“原來是場噩夢啊……” “哼祖灰!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起局扶,我...
    開封第一講書人閱讀 39,129評論 0 276
  • 序言:老撾萬榮一對情侶失蹤三妈,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后悠鞍,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體模燥,經(jīng)...
    沈念sama閱讀 45,561評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡掩宜,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,779評論 3 335
  • 正文 我和宋清朗相戀三年牺汤,在試婚紗的時候發(fā)現(xiàn)自己被綠了浩嫌。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,902評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡追迟,死狀恐怖伐坏,靈堂內(nèi)的尸體忽然破棺而出桦沉,到底是詐尸還是另有隱情,我是刑警寧澤纯露,帶...
    沈念sama閱讀 35,621評論 5 345
  • 正文 年R本政府宣布埠褪,位于F島的核電站,受9級特大地震影響贷掖,放射性物質(zhì)發(fā)生泄漏渴语。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,220評論 3 328
  • 文/蒙蒙 一牙甫、第九天 我趴在偏房一處隱蔽的房頂上張望调违。 院中可真熱鬧,春花似錦且轨、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,838評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽玷过。三九已至,卻和暖如春粤蝎,著一層夾襖步出監(jiān)牢的瞬間袋马,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,971評論 1 269
  • 我被黑心中介騙來泰國打工碑宴, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留桑谍,地道東北人。 一個月前我還...
    沈念sama閱讀 48,025評論 2 370
  • 正文 我出身青樓贞间,卻偏偏與公主長得像雹仿,于是被迫代替她去往敵國和親胧辽。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,843評論 2 354

推薦閱讀更多精彩內(nèi)容

  • 工廠模式類似于現(xiàn)實生活中的工廠可以產(chǎn)生大量相似的商品础浮,去做同樣的事情奠骄,實現(xiàn)同樣的效果;這時候需要使用工廠模式。簡單...
    舟漁行舟閱讀 7,758評論 2 17
  • 工廠模式 單體模式 模塊模式 代理模式 職責鏈模式 命令模式 模板方法模式 策略模式 發(fā)布-訂閱模式 中介者模式 ...
    HelloJames閱讀 1,008評論 0 6
  • 單例模式 適用場景:可能會在場景中使用到對象,但只有一個實例鸭廷,加載時并不主動創(chuàng)建,需要時才創(chuàng)建 最常見的單例模式辆床,...
    Obeing閱讀 2,067評論 1 10
  • 我倆鬧了點小矛盾讼载。 也不是我倆,我單方面的不理他咨堤,沒有多嚴重,我只是想知道他有多在乎我而已驱还,女人嘛凸克,難免俗俗的,尤...
    竹子開花9090閱讀 225評論 0 1
  • ?人類和動物最大的區(qū)別在于人類是會思考的蘆葦,而動物也是停留在思維萌芽的階段鸟雏。動物知道什么叫知足常樂,人則不然炊琉。這...
    張怪怪閱讀 275評論 1 3