“这是我参与8月更文挑战的第10天,活动详情查看:8月更文挑战”
前言
在面试高级前端时,往往会遇到一些关于设计模式的问题,每次都回答不太理想。恰逢8月更文挑战的活动,准备用一个月时间好好理一下关于设计模式方面的知识点,给自己增加点面试的底气。
在上期的文章中详情介绍什么是策略模式及如何实现一个简单的策略模式,具体可以看『面试的底气』—— 设计模式之策略模式(一)|8月更文挑战。
在上期文章中实现的策略模式是采用类的方式实现,这是模拟一些传统面向对象语言的实现,代码量有点多。本文会介绍一个更简单和直接的做法来实现一个策略模式。
JavaScript中的策略模式
用类实现的策略模式,在使用策略时候,要把策略类实例化才能使用,而在JavaScript语言中,函数也是对象,所以更简单和直接的做法是把策略直接定义为函数。
const strategies = {
"S": (salary) => {
return salary * 0.2;
},
"A": (salary) => {
return salary * 0.1;
},
"B": (salary) => {
return salary * 0;
},
"C": (salary) => {
return salary * -0.1;
}
}
复制代码
同样,环境类也没必要用一个类来创建,直接用一个函数来创建。
const calculateBonus = (level,salary) =>{
return strategies[ level ]( salary );
};
console.log( calculateBonus( 'S', 20000 ) ); // 输出:4000
console.log( calculateBonus( 'A', 10000 ) ); // 输出:1000
复制代码
这样代码量是不是少了很多,结构也清晰明了。可以把这种策略模式称为函数形式的策略模式。
策略中不仅只有算法
策略模式:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。
从策略模式的定义上看,策略模式就是用来封装算法的。但是策略模式只用来封装算法,未免有
一点大材小用。
在实际开发中,可以把算法的含义扩散开来,使策略模式也可以用来封装一系列的“业务规则”。只要这些业务规则的目标是一致,并且可以被替换使用,就可以用策略模式来封装它们。
举一个很常见的例子–用策略模式实现表单验证。
先定义一下,表单验证的策略组。
const strategies = {
isNonEmpty: function (value, errorTip) { // 不为空
if (value === '') {
return errorTip;
}
},
minLength: function (value, length, errorTip) { // 限制最小长度
if (value.length < length) {
return errorTip;
}
},
isMobile: function (value, errorTip) { // 手机号码格式
if (!/(^1[3|5|8][0-9]{9}$)/.test(value)) {
return errorTip;
}
}
};
复制代码
下面要实现一个Validator
类作为策略模式中的环境类,负责接收用户的请求并委托给strategies
策略组对象。在实现前,先设想一下用户是如何向Validator
类发送请求的。
假设先执行new Validator
创建一个validator
对象,通过validator.add
来添加添加一些校验规则,然后执行validator.check
开始校验,最后返回校验结果。
validator.add( loginForm.password, 'minLength:6', '密码长度不能少于6位');
const errorTip = validator.check(); // 获得校验结果
复制代码
其中loginForm.password
为密码input输入框的DOM元素。minLength:6
是一个以冒号隔开的字符串。冒号前面的minLength
代表客户挑选的策略,冒号后面的数字6
表示在校验过程中所必需的一些参数。minLength:6
的意思就是校验loginForm.password
这个密码输入框的value
最小长度为6
。如果这个字符串中不包含冒号,说明校验过程中不需要额外的参数信息。第3个参数是当校验未通过时返回的错误信息。
按上面对Validator
类的设想,下面来实现一下Validator
类。
class Validator {
constructor() {
this.rule = [];//校验规则
}
add(dom, rule, errorTip) {
const param = rule.split(':'); // 把 strategy 和参数分开
this.rule.push(function () { // 把校验的步骤用空函数包装起来,并且放入 rule
const strategy = param.shift(); // 用户挑选的 strategy
param.unshift(dom.value); // 把 input 的 value 添加进参数列表
param.push(errorTip); // 把 errorTip 添加进参数列表
return strategies[strategy].apply(dom, param);
});
}
check() {
for (let i = 0, validatorFunc; validatorFunc = this.cache[i++];) {
const errorTip = validatorFunc(); // 开始校验,并取得校验后的返回信息
if (errorTip) { // 如果有确切的返回值,说明校验没有通过
return errorTip;
}
}
}
}
复制代码
最终这么使用环境类Validator
const validataFunc = function () {
const validator = new Validator(); // 创建一个 validator 对象
/***************添加一些校验规则****************/
validator.add(loginForm.userName, 'isNonEmpty', '用户名不能为空');
validator.add(loginForm.password, 'minLength:6', '密码长度不能少于 6 位');
validator.add(loginForm.phoneNumber, 'isMobile', '手机号码格式不正确');
var errorTip = validator.check(); // 获得校验结果
return errorTip; // 返回校验结果
}
const loginForm = document.getElementById('loginForm');
loginForm.onsubmit = function () {
const errorTip = validataFunc(); // 如果 errorMsg 有确切的返回值,说明未通过校验
if (errorTip) {
console.log(errorTip);
return false; // 阻止表单提交
}
};
复制代码
策略模式的优缺点
优点:
-
策略模式利用组合、委托和多态等技术和思想,可以有效地避免多重条件选择语句。
-
策略模式提供了对开放—封闭原则的完美支持,将算法封装在独立的策略中,使得它们易于切换,易于理解,易于扩展。
-
策略模式中的算法也可以复用在系统的其他地方,从而避免许多重复的复制粘贴工作。
-
在策略模式中利用组合和委托来让环境类拥有执行算法的能力,这也是继承的一种更轻便的替代方案。
缺点:
-
虽然使用策略模式会在程序中增加许多策略类或者策略对象,但是要比在环境类堆砌各种逻辑好非常多。
-
要使用策略模式,必须了解所有的策略,还要了解各个策略之间的不同点,这样才能选择一个合适的策略。比如,我们要选择一种合适的旅游出行路线,必须先了解选择飞机、动车、大巴等方案的细节。此时策略组要向其他类暴露它的所有实现,这是违反最少知识原则的。
多态思想是策略模式的核心
多态思想:多态的思想是将“做什么”和“谁去做以及怎样去做”分离开来,也就是将“不变的事物”与“可能改变的事物”分离开来。
例如在校验表单的例子中,把用什么规则校验表单(做什么)封装在环境类Validator
中,如何校验(谁去做以及怎样去做)封装在策略组strategies
中。
在环境类Validator
中添加不同的校验规则,然后进行校验表单,就会产生不同的结果,正好体现了多态的定义。
多态的含义:同一操作作用于不同的对象上面,可以产生不同的解释和不同的执行结果。