1.定义
定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。
2.使用策略模式算奖金
业务场景:根据绩效等级算奖金
普通写法
var calculateBounds = function (level, salary) {
if (level === 'S') {
return salary * 4
}
if (level === 'A') {
return salary * 3
}
if (level === 'B') {
return salary * 2
}
}
calculateBounds('S', 1000)
calculateBounds('A', 500)
可以发现,这段代码十分简单,但是存在着显而易见的缺点。
- calculateBonus 函数比较庞大,包含了很多 if-else 语句,这些语句需要覆盖所有的逻辑分支。
- calculateBonus 函数缺乏弹性,如果增加了一种新的绩效等级 C,或者想把绩效 S 的奖金系数改为5,那我们必须深入calculateBonus 函数的内部实现,这是违反开放-封闭原则的。
- 算法的复用性差,如果在程序的其他地方需要重用这些计算奖金的算呢?我们的选择只有复制和粘贴。 因此,我们需要重构这段代码。
策略模式指的是定义一系列的算法,把它们一个个封装起来。将不变的部分和变化的部分隔开是每个设计模式的主题,策略模式也不例外,策略模式的目的就是将算法的使用与算法的实现分离开来
在这个例子里,算法的使用方式是不变的,都是根据某个算法取得计算后的奖金数额。而算法的实现是各异和变化的
使用策略模式写法
// 策略类
var strategies = {
'S': function (salary) {
return salary * 4
},
'A': function (salary) {
return salary * 3
},
'B': function (salary) {
return salary * 2
}
}
// 环境类 context
var calculateBounds = function (level, salary) {
return strategies[level](salary)
}
console.log(calculateBounds('S', 1000))
console.log(calculateBounds('A', 500))
一个基于策略模式的程序至少由两部分组成。
- 第一个部分是一组策略类,策略类封装了具体的算法,并负责具体的计算过程。
- 第二个部分是环境类Context,Context 接受客户的请求,随后把请求委托给某一个策略类。
- 要做到这点,说明Context 中要维持对某个策略对象的引用。
3.多态在策略模式中的体现
通过使用策略模式重构代码,我们消除了原程序中大片的条件分支语句。所有跟计算奖金有关的逻辑不再放在Context 中,而是分布在各个策略对象中。Context 并没有计算奖金的能力,而是把这个职责委托给了某个策略对象。每个策略对象负责的算法已被各自封装在对象内部。当我们对这些策略对象发出“计算奖金”的请求时,它们会返回各自不同的计算结果,这正是对象多态性的体现,也是“它们可以相互替换”的目的。替换 Context 中当前保存的策略对象,便能执行不同的算法来得到我们想要的结果。
4.表单校验的策略设计模式
业务背景:假设我们正在编写一个注册的页面,在点击注册按钮之前,有如下几条校验逻辑。
- 用户名不能为空。
- 密码长度不能少于6 位。
- 手机号码必须符合格式。
普通写法
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;
}
};
这是一种很常见的代码编写方式,它的缺点跟计算奖金的最初版本一模一样。
- gisterForm.onsubmit 函数比较庞大,包含了很多 if-else 语句,这些语句需要覆盖所有 的校验规则。
- registerForm.onsubmit 函数缺乏弹性,如果增加了一种新的校验规则,或者想把密码的长度校验从6 改成8,我们都必须深入registerForm.onsubmit 函数的内部实现,这是违反开放—封闭原则的。
- 的复用性差,如果在程序中增加了另外一个表单,这个表单也需要进行一些类似的校验,那我们很可能将这些校验逻辑复制得漫天遍野
使用策略模式写法
// 策略对象
var 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|7][0-9]{9}$)/.test(value)) {
return errorMsg;
}
},
};
// 环境类Context,Context 接受客户的请求,随后把请求委托给某一个策略类
var Validator = function () {
this.cache = []; // 保存校验规则
};
Validator.prototype.add = function (dom, rules) {
var self = this;
for (var i = 0; i < rules.length; i++) {
(function (rule) {
var strategyAry = rule.strategy.split(":");
var errorMsg = rule.errorMsg;
self.cache.push(function () {
var strategy = strategyAry.shift();
strategyAry.unshift(dom.value);
strategyAry.push(errorMsg);
return strategies[strategy].apply(dom, strategyAry);
});
})(rules[i]);
}
};
Validator.prototype.start = function () {
for (var i = 0; i < this.cache.length; i++) {
var ruleFunc = this.cache[i];
var msg = ruleFunc();
if (msg) {
return msg;
}
}
};
// 用户调用时
var validataFunc = function () {
var validator = new Validator();
validator.add(registerForm.userName, [
{
strategy: "isNonEmpty",
errorMsg: "用户名不能为空",
},
{
strategy: "minLength:6",
errorMsg: "用户名长度不能少于6位",
},
]);
validator.add(registerForm.password, [
{
strategy: "minLength:6",
errorMsg: "密码长度不能少于6位",
},
]);
validator.add(registerForm.userName, [
{
strategy: "isMobile",
errorMsg: "手机号格式不正确",
},
]);
var errorMsg = validator.start();
return errorMsg;
};
registerForm.onsubmit = function () {
var errorMsg = validataFunc();
if (errorMsg) {
alert(errorMsg);
return false;
}
};
validator.add 方法接受3 个参数,以下面这句代码说明:
validator.add( registerForm.password, 'minLength:6', '密码长度不能少于6 位' );
-
registerForm.password 为参与校验的input 输入框。
-
'minLength:6'是一个以冒号隔开的字符串。冒号前面的minLength代表客户挑选的strategy对象,冒号后面的数字6 表示在校验过程中所必需的一些参数。'minLength:6'的意思就是校验registerForm.password 这个文本输入框的value 最小长度为6。如果这个字符串中不包含冒号,说明校验过程中不需要额外的参数信息,比如'isNonEmpty'。
-
第3 个参数是当校验未通过时返回的错误信息。
当我们往 validator 对象里添加完一系列的校验规则之后,会调用 validator.start()方法来启动校验。如果validator.start()返回了一个确切的errorMsg 字符串当作返回值,说明该次校验没有通过,此时需让registerForm.onsubmit 方法返回false 来阻止表单的提交
使用策略模式重构代码之后,我们仅仅通过“配置”的方式就可以完成一个表单的校验,这些校验规则也可以复用在程序的任何地方,还能作为插件的形式,方便地被移植到其他项目中
策略模式的优缺点
策略模式优点:
- 策略模式利用组合、委托和多态等技术和思想,可以有效地避免多重条件选择语句。
- 策略模式提供了对开放—封闭原则的完美支持,将算法封装在独立的strategy 中,使得它们易于切换,易于理解,易于扩展。
- 策略模式中的算法也可以复用在系统的其他地方,从而避免许多重复的复制粘贴工作。
- 在策略模式中利用组合和委托来让Context 拥有执行算法的能力,这也是继承的一种更轻便的替代方案。
策略模式缺点:
- 用策略模式会在程序中增加许多策略类或者策略对象
- 要使用策略模式,必须了解所有的strategy,必须了解各个strategy 之间的不同点,这样才能选择一个合适的strategy
- strategy 要向客户暴露它的所有实现,这是违反最少知识原则的
总结
在JavaScript 语言的策略模式中,策略类往往被函数所代替,这时策略模式就成为一种“隐形”的模式。
在函数作为一等对象的语言中,策略模式是隐形的。strategy 就是值为函数的变量