概念
策略模式:定义一系列的算法(这些算法目标一致),把它们一个个封装起来,并且使它们可以相互替换。
从公司绩效谈起
每家公司年终奖的发放都会根据该年度员工表现给予一定的奖惩,当然 A 公司也不例外, A 公司的年终绩效制度如下:
- 等级为 S 的,年终奖为工资的 4倍
- 等级为 A 的,年终奖为工资的 3 倍
- 等级为 B 的,年终奖为工资的 2 倍
- 等级为 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
功能上可以正常使用,但存在如下几个缺点:
- 违反单一功能原则,在一个函数里处理了四个绩效逻辑,绝对的胖逻辑,这种的铁定要拆
- 违反开放封闭原则,如果要再加个绩效为 D 的逻辑,还得往 calculateBonus 函数里添加一段逻辑,这样提测的时候,还要求测试人员从头将所有绩效等级重头测遍。
- 算法复用性差,如果其他地方需要用到这种计算规则,只能重新输出(复制、粘贴)
优化一(组合函数)
让我们先来解决第三点算法复用性差问题。
把各种算法(即年终奖的计算规则)封装到一个个独立的小函数中。
代码改成如下:
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 的。
策略模式的组成:
- 一组策略类,策略类封装了具体的算法,并负责具体的计算过程。
- 环境类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
再来看下策略模式的定义
定义一系列的算法(这些算法目标一致),把它们一个个封装起来,并且使它们可以相互替换。
- 算法:绩效的计算方法
- 封装:计算方法被封装在策略对象内部,达到可复用
- 相互替换:要更改某个绩效时,只要改变参数,不影响函数的调用
更广义的"算法"
策略模式指的是定义一系列的算法,并且把它们封装起来,但是策略模式不仅仅只封装算法,我们还可以对用来封装一系列的业务规则,只要这些业务规则目标一致,我们就可以使用策略模式来封装它们;比如表单验证
表单验证
需求:
- 用户名不能为空
- 密码长度不能少于6位
- 手机号码必须符合格式
简单实现
<!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 要向客户暴露它的所有实现,这是违反最少知识原则的。
使用场景:
- 多个算法只在行为上稍有不同的场景,这时可以使用策略模式来动态选择算法;
- 算法需要自由切换的场景;
- 有时需要多重条件判断,那么可以使用策略模式来规避多重条件判断的情况;
参考链接
结语
你的点赞是对我最大的肯定,如果觉得有帮助,请留下你的赞赏,谢谢!!!