JavaScript程序設(shè)計(jì)模式小技巧——策略模式徐裸,快看快用!F锼睢次企!

JavaScript程序設(shè)計(jì)模式小技巧——策略模式缸棵,快看快用6碌凇Lぶ尽胀瞪!

何為策略模式赏廓?

  • 比如在業(yè)務(wù)邏輯或程序設(shè)計(jì)中比如要實(shí)現(xiàn)某個(gè)功能幔摸,有多種方案可供我們選擇既忆。比如要壓縮一個(gè)文件患雇,我們既可以選擇 ZIP 算法苛吱,也可以選擇 GZIP 算法翠储。
  • 這些算法靈活多樣,可隨意切換庐舟,而這種解決方案就是我們所要學(xué)習(xí)的策略模式挪略。

定義或概念

  • 策略模式:定義一系列的算法杠娱,將他們一個(gè)個(gè)封裝墨辛,并使他們可相互替換睹簇。

策略模式的最佳實(shí)踐

例子1:獎(jiǎng)金計(jì)算

  • 題目:在很多公司的年終獎(jiǎng)都是按照員工的工資基數(shù)和年底績(jī)效情況來(lái)發(fā)放的太惠,例如凿渊,績(jī)效為 S 的人年終獎(jiǎng)有 4 倍工資埃脏,A 的人年終獎(jiǎng)有 3 倍彩掐,B 的人年終獎(jiǎng)有 2 倍堵幽。要求我們寫(xiě)出一個(gè)程序來(lái)更快的計(jì)算員工的年終獎(jiǎng)朴下。(編寫(xiě)一個(gè)名為 calcBonus 方法來(lái)計(jì)算每個(gè)員工的獎(jiǎng)金數(shù)額)
  • 可能有些人一上來(lái)直接就在一個(gè)方法中進(jìn)行很多 if...else 或 switch...case 判斷, 然后通過(guò)這個(gè)方法進(jìn)行計(jì)算殴胧。我們可以來(lái)試著寫(xiě)一下:
/**
 *
 * @param {*} level 績(jī)效等級(jí)
 * @param {*} salary 工資基數(shù)
 * @returns 年終獎(jiǎng)金額
 */
var calcBonus = function (level, salary) {
    if (level === "S") {
        return salary * 4;
    } else if (level === "A") {
        return salary * 3;
    } else if (level === "B") {
        return salary * 2;
    }
};

calcBonus('A', 20000); // 60000
calcBonus('B', 8000); // 16000
  • 我想在我們每個(gè)人初學(xué)代碼時(shí)肯定都寫(xiě)出過(guò)這樣的代碼。其實(shí)這段代碼有顯而易見(jiàn)的缺點(diǎn):
    1. calcBonus 函數(shù)邏輯太多
    2. calcBonus 函數(shù)缺乏彈性佩迟,比如如果我們需要增加一個(gè)等級(jí) C团滥,那就必須要去修改 calcBonus 函數(shù)免胃。這就違反了開(kāi)放-封閉原則
    3. 復(fù)用性差惫撰。如果后續(xù)還要重用這個(gè)程序去計(jì)算獎(jiǎng)金羔沙,我們只有去 C,V。
  • 此時(shí)厨钻,可能會(huì)想對(duì) calcBonus 函數(shù)進(jìn)行封裝扼雏,如我們使用組合函數(shù)的形式,如下:
var totalS = function (salary) {
    return salary * 4;
};
var totalA = function (salary) {
    return salary * 3;
};
var totalB = function (salary) {
    return salary * 2;
};

var calcBonus = function (level, salary) {
    if (level === "S") {
        return totalS(salary);
    } else if (level === "A") {
        return totalA(salary);
    } else if (level === "B") {
        return totalB(salary);
    }
};

calcBonus('A', 20000); // 60000
calcBonus('B', 8000); // 16000
  • 這樣夯膀,我們將程序進(jìn)行了進(jìn)一步改善诱建,但改善微乎其微,依舊沒(méi)有解決最重要的問(wèn)題,calcBonus 函數(shù)還是有可能會(huì)很龐大汽馋,并且也沒(méi)有彈性铁蹈。
  • 那我們?cè)賹⑺M(jìn)行一次改造便锨,使用策略模式:將其定義為一系列的算法,將他們每一個(gè)封裝起來(lái)厘托,將不變的部分和變化的部分隔開(kāi)饺藤。
  • 在這段程序中,算法的使用方式是不變的,都是根據(jù)某個(gè)算法獲取最后的獎(jiǎng)金金額。而在每個(gè)算法的內(nèi)部實(shí)現(xiàn)卻是不同的蔽挠,每一個(gè)等級(jí)對(duì)應(yīng)著不同的計(jì)算規(guī)則
  • 在策略模式程序中:最少由兩部分組成量窘,一部分是一組策略類嫩海,在策略類中封裝了具體的算法,并負(fù)責(zé)具體的計(jì)算過(guò)程。一部分是環(huán)境類 context,接受用戶的請(qǐng)求,并將請(qǐng)求委托給某一個(gè)策略類。
  • 如下:
var strategies = {
    S: function (salary) {
        return salary * 4;
    },
    A: function (salary) {
        return salary * 3;
    },
    B: function (salary) {
        return salary * 2;
    },
};

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

calcBonus('A', 20000); // 60000
calcBonus('B', 8000); // 16000
  • 其實(shí)挤聘,策略模式的實(shí)現(xiàn)并不復(fù)雜步淹,關(guān)鍵是如何從策略模式的實(shí)現(xiàn)背后澈驼,找到封裝變化内边,委托和多態(tài)性這些思想的價(jià)值

例子2:表單驗(yàn)證

  • 題目:在 Web 開(kāi)發(fā)中眶俩,表單校驗(yàn)是一個(gè)常見(jiàn)的話題线罕,要求使用策略模式來(lái)完成表單驗(yàn)證喇闸。
  • 比如:
    1. 用戶名不能為空
    2. 密碼長(zhǎng)度不能少于 6 位
    3. 手機(jī)號(hào)碼必須符合正確格式
  • 讓我們來(lái)實(shí)現(xiàn)一下吧:
function submit() {
    let { username, password, tel } = infoForm;
    if (username === "") {
        Toast("用戶名不能為空");
        return false;
    }
    if (password.length < 6) {
        Toast("密碼不能少于 6 位");
        return false;
    }
    if (!/(^1[3|5|8][0-9]{9}$)/.test(tel)) {
        Toast("手機(jī)號(hào)碼格式不正確");
        return false;
    }

    // .....
}
  • 這是我們常見(jiàn)的實(shí)現(xiàn)方式舆瘪,它的缺點(diǎn)跟計(jì)算獎(jiǎng)金一例類似:
    1. submit 函數(shù)龐大箕戳,包含了很多 if...else 語(yǔ)句
    2. submit 函數(shù)缺乏彈性环础,如果對(duì)其新加一些新的校驗(yàn)規(guī)則角雷,如果我們把密碼長(zhǎng)度從 6 改到 8.那我們就必須要改動(dòng) submit 函數(shù)胯舷,否則無(wú)法實(shí)現(xiàn)該校驗(yàn)。這也是違反開(kāi)放-封閉原則废菱。
    3. 復(fù)用差我磁,如果說(shuō)我們程序中還有另一個(gè)表達(dá)需要驗(yàn)證霞势,也是進(jìn)行類似的校驗(yàn),那我們可能會(huì)進(jìn)行 C, V 操作菌湃。
  • 使用策略模式來(lái)進(jìn)行重構(gòu)
let infoForm = {
    username: "我是某某某",
    password: 'zxcvbnm',
    tel: 16826384655,
};

var strategies = {
    isEmpty: function (val, msg) {
        if (!val) return msg;
    },
    minLength: function (val, length, msg) {
        if (val.length < length) return msg;
    },
    isTel: function (val, msg) {
        if (!/(^1[3|5|8][0-9]{9}$)/.test(val)) return msg;
    },
};

var validFn = function () {
    var validator = new Validator();

    let { username, password, tel } = infoForm;

    validator.add(username, "isEmpty", "用戶名不能為空");
    validator.add(password, "minLength:6", "密碼不能少于 6 位");
    validator.add(tel, "isTel", "手機(jī)號(hào)碼格式不正確");

    var msg = validator.start();
    return msg;
};

class Validator {
    constructor() {
        this.cache = [];
    }
    add(attr, rule, msg) {
        var ruleArr = rule.split(":");
        this.cache.push(function () {
            var strategy = ruleArr.shift();
            ruleArr.unshift(attr);
            ruleArr.push(msg);
            return strategies[strategy].apply(attr, ruleArr);
        });
    }

    start() {
        for (let i = 0; i < this.cache.length; i++) {
            var msg = this.cache[i]();
            if (msg) return msg;
        }
    }
}

function submit() {
    let msg = validFn();
    if (msg) {
        Toast(msg);
        return false;
    }
    console.log('verify success');

    // .....
}

submit();
  • 使用策略模式重構(gòu)后,我們后續(xù)僅需配置的方式來(lái)完成。
  • 擴(kuò)展題目:那如果想給用戶名還想再添加一個(gè)規(guī)則,那如何完成呢朗兵?
  • 添加規(guī)則方式如下:
validator.add(username, [
    {
        strategy: "isEmpty",
        msg: "用戶名不能為空"
    },
    {
        strategy: 'minLength:6',
        msg: '密碼不能少于 6 位'
    }
]);
  • 實(shí)現(xiàn):
let infoForm = {
    username: "阿斯頓發(fā)生的",
    password: "ss1sdf",
    tel: 15829485647,
};

var strategies = {
    isEmpty: function (val, msg) {
        if (!val) return msg;
    },
    minLength: function (val, length, msg) {
        if (val.length < length) return msg;
    },
    isTel: function (val, msg) {
        if (!/(^1[3|5|8][0-9]{9}$)/.test(val)) return msg;
    },
};

var validFn = function () {
    var validator = new Validator();

    let { username, password, tel } = infoForm;

    validator.add(username, [
        {
            strategy: "isEmpty",
            msg: "用戶名不能為空",
        },
        {
            strategy: "minLength:6",
            msg: "密碼不能少于 6 位",
        },
    ]);
    validator.add(password, [
        {
            strategy: "minLength:6",
            msg: "密碼不能少于 6 位",
        },
    ]);
    validator.add(tel, [
        {
            strategy: "isTel",
            msg: "手機(jī)號(hào)碼格式不正確",
        },
    ]);

    var msg = validator.start();
    return msg;
};

class Validator {
    constructor() {
        this.cache = [];
    }
    add(attr, rules) {
        for (let i = 0; i < rules.length; i++) {
            var rule = rules[i];
            var ruleArr = rule.strategy.split(":");
            var msg = rule.msg;
            var cacheItem = this.createCacheItem(ruleArr, attr, msg);
            this.cache.push(cacheItem);
        }
    }

    start() {
        for (let i = 0; i < this.cache.length; i++) {
            var msg = this.cache[i]();
            if (msg) return msg;
        }
    }

    createCacheItem(ruleArr, attr, msg) {
        return function () {
            var strategy = ruleArr.shift();
            ruleArr.unshift(attr);
            ruleArr.push(msg);
            return strategies[strategy].apply(attr, ruleArr);
        };
    }
}

function submit() {
    let msg = validFn();
    if (msg) {
        Toast(msg);
        return false;
    }
    console.log("verify success");

    // .....
}

submit();

策略模式的優(yōu)缺點(diǎn)

  • 優(yōu)點(diǎn):
    1. 利用組合节预,委托,多態(tài)等技術(shù)有效避免了多重條件語(yǔ)句
    2. 提供了對(duì)開(kāi)封-封閉原則的完美支持
    3. 復(fù)用性較強(qiáng),避免許多重復(fù)的 C,V 工作
  • 缺點(diǎn):
    1. 客戶端要先了解所有的策略類秒裕,才能選擇合適的策略類袱蚓。

策略模式的角色

  1. Context(環(huán)境類):持有一個(gè) Strategy 類的引用,用一個(gè) ConcreteStrategy 對(duì)象來(lái)配置
  2. Strategy(環(huán)境策略類):定義了所有支持的算法的公共接口几蜻,通常是以一個(gè)接口或抽象來(lái)實(shí)現(xiàn)喇潘。Context 使用這個(gè)接口來(lái)調(diào)用其 ConcreteStrategy 定義的算法体斩。
  3. ConcreteStrategy(具體策略類):以 Strategy 接口實(shí)現(xiàn)某種算法
  • 比如以上的例子算法:


    e9a698551f352.png

策略模式的應(yīng)用場(chǎng)景

  1. 想使用對(duì)象中各種不同算法變體來(lái)在運(yùn)行時(shí)切換算法時(shí)
  2. 擁有很多在執(zhí)行某些行為時(shí)有著不同的規(guī)則時(shí)

Tip: 文章部分內(nèi)容參考于曾探大佬的《JavaScript 設(shè)計(jì)模式與開(kāi)發(fā)實(shí)踐》。文章僅做個(gè)人學(xué)習(xí)總結(jié)和知識(shí)匯總

特殊字符描述:

  1. 問(wèn)題標(biāo)注 Q:(question)
  2. 答案標(biāo)注 R:(result)
  3. 注意事項(xiàng)標(biāo)準(zhǔn):A:(attention matters)
  4. 詳情描述標(biāo)注:D:(detail info)
  5. 總結(jié)標(biāo)注:S:(summary)
  6. 分析標(biāo)注:Ana:(analysis)
  7. 提示標(biāo)注:T:(tips)
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末颖低,一起剝皮案震驚了整個(gè)濱河市絮吵,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌忱屑,老刑警劉巖蹬敲,帶你破解...
    沈念sama閱讀 206,482評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異莺戒,居然都是意外死亡伴嗡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)脏毯,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)闹究,“玉大人幔崖,你說(shuō)我怎么就攤上這事食店。” “怎么了赏寇?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,762評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵吉嫩,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我嗅定,道長(zhǎng)自娩,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,273評(píng)論 1 279
  • 正文 為了忘掉前任渠退,我火速辦了婚禮忙迁,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘碎乃。我一直安慰自己姊扔,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,289評(píng)論 5 373
  • 文/花漫 我一把揭開(kāi)白布梅誓。 她就那樣靜靜地躺著恰梢,像睡著了一般。 火紅的嫁衣襯著肌膚如雪梗掰。 梳的紋絲不亂的頭發(fā)上嵌言,一...
    開(kāi)封第一講書(shū)人閱讀 49,046評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音及穗,去河邊找鬼摧茴。 笑死,一個(gè)胖子當(dāng)著我的面吹牛埂陆,可吹牛的內(nèi)容都是我干的苛白。 我是一名探鬼主播尘分,決...
    沈念sama閱讀 38,351評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼丸氛!你這毒婦竟也來(lái)了培愁?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 36,988評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤缓窜,失蹤者是張志新(化名)和其女友劉穎定续,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體禾锤,經(jīng)...
    沈念sama閱讀 43,476評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡私股,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,948評(píng)論 2 324
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了恩掷。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片倡鲸。...
    茶點(diǎn)故事閱讀 38,064評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖黄娘,靈堂內(nèi)的尸體忽然破棺而出峭状,到底是詐尸還是另有隱情,我是刑警寧澤逼争,帶...
    沈念sama閱讀 33,712評(píng)論 4 323
  • 正文 年R本政府宣布优床,位于F島的核電站,受9級(jí)特大地震影響誓焦,放射性物質(zhì)發(fā)生泄漏胆敞。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,261評(píng)論 3 307
  • 文/蒙蒙 一杂伟、第九天 我趴在偏房一處隱蔽的房頂上張望移层。 院中可真熱鬧,春花似錦赫粥、人聲如沸观话。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,264評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)匪燕。三九已至,卻和暖如春喧笔,著一層夾襖步出監(jiān)牢的瞬間帽驯,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,486評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工书闸, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留尼变,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,511評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像嫌术,于是被迫代替她去往敵國(guó)和親哀澈。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,802評(píng)論 2 345

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