javascript 设计模式之策略模式

概念

策略模式:定义一系列的算法(这些算法目标一致),把它们一个个封装起来,并且使它们可以相互替换。

从公司绩效谈起

每家公司年终奖的发放都会根据该年度员工表现给予一定的奖惩,当然 A 公司也不例外, A 公司的年终绩效制度如下:

  1. 等级为 S 的,年终奖为工资的 4倍
  2. 等级为 A 的,年终奖为工资的 3 倍
  3. 等级为 B 的,年终奖为工资的 2 倍
  4. 等级为 C 的,年终奖为工资的 0.3 倍

代码实现如下:

var calculateBonus = function (performanceLevel, salary) {
	if (performanceLevel == 'S') {
		return salary * 4
	}
	if (performanceLevel == 'A') {
		return salary * 3
	}
	if (performanceLevel == 'B') {
		return salary * 2
	}
	if (performanceLevel == 'C') {
		return salary * 0.3
	}
}
console.info(calculateBonus('S', 20000)) // 80000
console.info(calculateBonus('C', 15000)) // 4500

功能上可以正常使用,但存在如下几个缺点:

  1. 违反单一功能原则,在一个函数里处理了四个绩效逻辑,绝对的胖逻辑,这种的铁定要拆
  2. 违反开放封闭原则,如果要再加个绩效为 D 的逻辑,还得往 calculateBonus 函数里添加一段逻辑,这样提测的时候,还要求测试人员从头将所有绩效等级重头测遍。
  3. 算法复用性差,如果其他地方需要用到这种计算规则,只能重新输出(复制、粘贴)

优化一(组合函数)

让我们先来解决第三点算法复用性差问题。
把各种算法(即年终奖的计算规则)封装到一个个独立的小函数中。
代码改成如下:

var performanceS = function (salary) {
	return salary * 4
}
var performanceA = function (salary) {
	return salary * 3
}
var performanceB = function (salary) {
	return salary * 2
}
var performanceC = function (salary) {
	return salary * 0.3
}
var calculateBonus = function (performanceLevel, salary) {
	if (performanceLevel == 'S') {
		return performanceS(salary)
	}
	if (performanceLevel == 'A') {
		return performanceA(salary)
	}
	if (performanceLevel == 'B') {
		return performanceB(salary)
	}
	if (performanceLevel == 'C') {
		return performanceC(salary)
	}
}
console.info(calculateBonus('S', 20000)) // 80000
console.info(calculateBonus('C', 15000)) // 4500

采用组合函数,将每个绩效算法抽离成单独的函数,是解决了复用问题,如果有别的地方要计算 S 等级的薪资,直接调用 performanceS 函数即可。
但上面1、2两个问题仍然存在,我们继续优化,引出主角策略模式

优化二(策略模式)

设计模式中很重要的一点就是将不变的部分和变化的部分分离出来,而策略模式的目的就是将算法的使用和算法的实现分离开来。
在这个例子中,算法的使用方式是不变的,都是根据等级和薪资计算年终奖;算法的实现是变化的,比如 S 有 S 等级的计算方式, A 有 A 的。
策略模式的组成:

  1. 一组策略类,策略类封装了具体的算法,并负责具体的计算过程。
  2. 环境类Context,Context接收客户的请求,随后把请求委托给某一个策略类。

定义策略类:

//策略类(S)
class performanceS {
    calculate(salary) {
        return salary * 4;
    }
}

//策略类(A)
class performanceA {
    calculate(salary) {
        return salary * 3;
    }
}

//策略类(B)
class performanceB {
    calculate(salary) {
        return salary * 2;
    }
}

//策略类(C)
class performanceC {
    calculate(salary) {
        return salary * 0.3;
    }
}

定义环境类:

// 环境类
class Bonus {
    constructor() {
        this.salary = null;     //原始工资
        this.strategy = null;  //绩效公司对应的策略对象
    }
    setSalary(salary) {
        this.salary = salary;  //设置原始工资
    }
    setStrategy(strategy) {
        this.strategy = strategy; //设置员工绩效等级对应的策略对象
    }
    getBonus() {//取得奖金数额
        //维持对策略对象的引用
        return this.strategy.calculate( this.salary );  //委托给对应的策略对象
    }
}

验证:

const bonus = new Bonus()
bonus.setSalary( 20000 );
bonus.setStrategy( new performanceS() ); //设置策略对象

console.info(bonus.getBonus()) // 80000

上述展示了策略模式的应用,代码比较清晰,解决了上述的几大问题,要再增加个绩效 D 的逻辑,不会动到 Bonus 类,只要再定义个 performanceD 策略类即可,在 Bonus 类里做的事情也很单一,负责接收策略类实例,并调用。
上述展示的策略模式是基于传统的面向对象语言,可以进一步对这段代码进行优化,变成JavaScript版本的策略模式。

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

在 JS 中,函数也是对象,所以可以将策略类直接定义为函数,并以对象映射形式展示。

//策略对象
var strategies = {
  //一系列算法
  "S": function ( salary ) {
    return salary * 4;
  },
  "A": function ( salary ) {
    return salary * 3;
  },
  "B": function ( salary ) {
    return salary * 2;
  },
  "C": function (salary) {
    return salary * 0.3;
  },
};

同时,也可以将环境类定义为函数,改成如下:

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

验证下:

console.log( calculateBonus('S', 20000)); // 80000

再来看下策略模式的定义

定义一系列的算法(这些算法目标一致),把它们一个个封装起来,并且使它们可以相互替换。

  • 算法:绩效的计算方法
  • 封装:计算方法被封装在策略对象内部,达到可复用
  • 相互替换:要更改某个绩效时,只要改变参数,不影响函数的调用

更广义的"算法"

策略模式指的是定义一系列的算法,并且把它们封装起来,但是策略模式不仅仅只封装算法,我们还可以对用来封装一系列的业务规则,只要这些业务规则目标一致,我们就可以使用策略模式来封装它们;比如表单验证

表单验证

需求:

  1. 用户名不能为空
  2. 密码长度不能少于6位
  3. 手机号码必须符合格式

简单实现

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>表单验证</title>
</head>
<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>

跟上述绩效奖金,存在一样的问题,函数过于庞大、缺乏弹性以及复用性差,下面采用策略模式优化

优化一(策略模式)

采用策略模式,首先要定义策略类,那策略类要先找到算法具体是指什么:表单验证逻辑的业务规则
所以定义策略类如下:

// 定义策略类
const 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;
        }
    }
}

接着要定义环境类,在定义环境类之前,先看下客户一般是怎么使用的?

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 类代码

class Validator {
    constructor() {
        this.cache = [];  //保存校验规则 
    }
    //添加检验规则函数
    add(dom,rule,errorMsg){
        //把strategy和参数分开'minLength:6' 如'minLength:6' -> ["minLength", "6"]
        let ary = rule.split(':'); 
        this.cache.push ( function () {
            let strategy = ary.shift(); //用户挑选的strategy ["minLength", "6"] -> 'minLength' 
            ary.unshift( dom.value ); //把input的value添加进参数列表
            ary.push( errorMsg ); //把errorMsg添加进参数列表
            return strategies[ strategy ].apply( dom, ary ); //委托策略对象调用
        })
    }
    start(){
        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中。

add (dom, rules) {
    let self = this;
    for (let i = 0,rule; rule = rules[i++];) {
        (function ( rule ) {
            let strategyAry = rule.strategy.split( ':' );
            let errorMsg = rule.errorMsg;
            
            self.cache.push( function () {
                let strategy = strategyAry.shift();
                strategyAry.unshift( dom.value );
                strategyAry.push( errorMsg );
                return strategies[ strategy ].apply( dom, strategyAry )
            })
        })(rule)
    }
}

总结

优点:

  • 策略之间相互独立,但策略可以自由切换,这个策略模式的特点给策略模式带来很多灵活性,也提高了策略的复用率;
  • 如果不采用策略模式,那么在选策略时一般会采用多重的条件判断,采用策略模式可以避免多重条件判断,增加可维护性;
  • 可扩展性好,策略可以很方便的进行扩展;

缺点:

  • 在策略模式中,我们会增加很多策略类、策略对象
  • 要使用策略模式,我们必须了解到所有的 strategy 、必须了解各个 strategy 之间的不同点,才能选择一个适合的 strategy 。比如,我们要选择一种合适的旅游出行路线,必须先了解选择飞机、火车、自行车等方案的细节。此时 strategy 要向客户暴露它的所有实现,这是违反最少知识原则的。

使用场景:

  1. 多个算法只在行为上稍有不同的场景,这时可以使用策略模式来动态选择算法;
  2. 算法需要自由切换的场景;
  3. 有时需要多重条件判断,那么可以使用策略模式来规避多重条件判断的情况;

参考链接

JavaScript设计模式与开发实践

结语

你的点赞是对我最大的肯定,如果觉得有帮助,请留下你的赞赏,谢谢!!!

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值