懂一点设计模式-策略模式

403 阅读3分钟

场景

商家大促活动:根据不同种类的商品,分别有不同的折扣

商品种类折扣
A90%
B85%
C80%
D75%

粗暴手段

   function getSalePrice(type, originPrice) {
       let salePrice = originPrice;
       if (type === 'A') {
           salePrice *= 0.9;
       } else if (type === 'B') {
           salePrice *= 0.85;
       } else if (type === 'C') {
           salePrice *= 0.8;
       } else if (type === 'D') {
           salePrice *= 0.75;
       }
       return salePrice;
   }

对于上述场景来说,getSalePrice能够根据不同商品种类计算出折扣后的出售价格,在这种比较简单没那么复杂的场景下,已经满足使用了;但如果活动效果很好,商家决定临时增加多几个商品,那又得去修改getSalePrice函数

   function getSalePrice(type, originPrice) {
       let salePrice = originPrice;
       if (type === 'A') {
           salePrice *= 0.9;
       } else if (type === 'B') {
           salePrice *= 0.85;
       } else if (type === 'C') {
           salePrice *= 0.8;
       } else if (type === 'D') {
           salePrice *= 0.75;
       } else if (type === 'E') {
           salePrice *= 0.6;
       } else if (type === 'F') {
           salePrice *= 0.5;
       } else if (type === 'G') {
           salePrice *= 0.4;
       }
       return salePrice;
   }

这样会导致 getSalePrice 越来越胖越来越臃肿,而且if-else条件判断太多,这非常不利于后续的维护,相信大家都不想别人接受自己的代码的时候,大吼一声“shit”,就算做不到“Wow”,也应该能达到“Great”的地步;

对于这种场景,策略模式就派上用场了,对于不同的商品,用对应不同的策略处理价格;

策略模式

// 价格策略
var priceStrategies = {
    'A': function(originPrice) {
        return originPrice *= 0.9;
    },
    'B': function(originPrice) {
        return originPrice *= 0.85;
    },
    'C': function(originPrice) {
        return originPrice *= 0.8;
    },
    'D': function(originPrice) {
        return originPrice *= 0.75;
    },
    'E': function(originPrice) {
        return originPrice *= 0.75;
    },
    'F': function(originPrice) {
        return originPrice *= 0.75;
    },
    'G': function(originPrice) {
        return originPrice *= 0.75;
    },
    // 更多商品种类策略
}

将对应的种类以及出售价格进行对象key-value映射,形成对应的不同商品的价格策略;

function getSalePrice(type, originPrice) {
    return priceSrategies[type](originPrice);
}

这样整理之后,getSalePrice则不再继续处理商品价格的多少,只需交给priceStrategies这个价格策略去执行,返回具体商品的价格,也方便不同策略的扩展;同时测试再回归的时候,不需要对原有的商品进行回归,因为你修改的并不是getSalePrice函数,不影响原有逻辑👍.

实战

其实以上的场景是非常简单的,实际上这种场景非常理想,即使不用策略模式,也能写出“Great”代码;

假设场景如下: 表单验证,当前注册页面有

  1. 账户名
  2. 密码
  3. 手机号

这三项表单需要验证

未用策略模式


// html
<form id="registerForm">
    请输入账户: <input type="text" name="account" id="account"/>
    请输入密码: <input type="password" name="password" id="password"/>
    请输入手机号: <input type="text" name="phoneNumber" id="phoneNumber"/>
</form>
<button id="submit">提交</button>

// js
const submit = document.querySelector('#submit');
const account = document.getElementById('account');
const password = document.getElementById('password');
const phone = document.getElementById('phone');
// 1、未使用策略模式
submit.addEventListener('click', function() {
    if (account.value === '') {
        alert('用户名不能为空');
        return false;
    }

    if (password.value.length < 10) {
        alert('密码长度不能小于10个字符');
        return false;
    }

    if (!/^1[3-8][0-9]{9}$/.test(phoneNumber.value)) {
        alert('手机号码格式不正确');
        return false;
    }

    // do other things
});

同样的,如果对表单项进行改动,对于提交动作submit都要进行修改,更有甚者,如果多个页面,都需要类似的表单验证,类似这种写法,可能需要将这段代码进行CTRL-CCTRL-V,当然也可以将各种验证函数抽出来,如:

function validatePhone(phone) {}
function validatePassword(password) {}
function validateAccount(account) {}

以上写法当然可以,但是没使用策略模式

使用策略模式


// 验证策略
var validateStrategies = {
    isEmpty: function(value, errMsg) {
        if (value === '') {
            return errMsg;
        }
    },
    isPhone: function(value, errMsg) {
        if (!/^1[3-8][0-9]{9}$/.test(value)) {
            return errMsg;
        }
    },
    minLength: function(value, length, errMsg) {
        if (value.length < Number(length)) {
            return errMsg;
        }
    }
}

function Validator() {
    this.validators = [];
}

Validator.prototype.register = function(dom, rules, errMsg) {
    const params = rules.split(':');
    this.validators.push(function() {
        const rule = params.shift();
        const value = dom.value;
        params.unshift(value);
        params.push(errMsg);  // [value, length?, errMsg]
        return validateStrategies[rule].apply(dom, params);
    })
}

Validator.prototype.validate = function() {
    for(var i = 0; i < this.validators.length; i++) {
        const msg = this.validators[i]();
        if (msg) {
            return msg;
        }
    }
}

submit.addEventListener('click', function() {
    const validator = new Validator();
    validator.register(account, 'isEmpty', '用户名不能为空');
    validator.register(password, 'minLength:10', '密码长度不能小于10个字符');
    validator.register(phone, 'isPhone', '手机号码格式不正确');
    const errMsg = validator.validate();
    if (errMsg) alert(errMsg);
    return false;
});

在这里通过策略模式: 将不同表单项的验证分为不同的策略

  1. 账户验证
  2. 密码验证
  3. 手机格式验证

同时通过Validator类,进行对当前所需的验证策略做对应的注册-register,在后续用到相关验证的时候,只需要在策略中新增,并在实际环境中注册,即可增加新的策略功能;方便业务扩展的同时,又使代码不会这么臃肿,Great👍

总结

总的来说,使用策略模式,能够将一些繁复且臃肿的代码,变得纤细且简洁,相信大家都不喜欢那种臃肿并且充斥着if-else的逻辑代码,在遇上这种场景,可以抽象出不同分支对应的策略,使主函数更加简洁,同时通过策略的方式,更容易扩展业务逻辑,并且减少代码改动导致的错误。

参考书籍

《JavaScript设计模式与开发实践》-曾探