策略模式指的是定义一些列的算法,并且把他们封装起来。这篇文章我们继续接着上篇文章继续探讨。从定义上来看,策略模式就是用来封装算法的,但如果策略模式仅仅是用来封装算法的,未免有点大材小用。在实际开发中,我们通常将这个“算法”的概念扩散开来,使用策略模式也能用来封装一些列的业务规则。只要这些业务规则指向一致,并且可以被替换使用,我们就可以用策略模式来封装他们。
这篇文章我们就来用表单校验的例子来深化我们对策略模式的理解
一个web项目中,注册、登录、修改用户信息等功能都离不开表单的提交,在一个用户输入数据交给后台之前,尝尝前端开发人员会对用户输入的数据加以校验,校验输入的数据是否符合规范,长度啊、格式啊等等,这样可以避免因为提交的数据存在问题而带来不必要的开销。
那么好,问题来了,假设我们正在编写一个注册页面,在点击注册之前,有如下几条校验规则
- 用户名不能为空
- 密码长度不能小于6位
- 手机号必须符合格式
未引入策略模式的“最笨方法”
<body>
<div>
请输入用户名:<input type="text" name="username"><br>
请输入密码:<input type="text" name="password"><br>
请输入手机号:<input type="text" name="phoneNumber"><br>
<button class="btn">提交</button>
</div>
<script>
const registerForm = document.getElementsByClassName("registerForm")
const inputArr = document.querySelectorAll("input")
const btn = document.querySelector(".btn")
btn.onclick = function () {
if (inputArr[0].value === "") {
alert("用户名不能为空!")
}
if (inputArr[1].value.length < 6) {
alert("密码长度不能小于6")
}
if (!/(^1[3|5|8][0-9]{9}$)/.test(inputArr[2].value)) {
alert("手机号格式不正确")
}
}
</script>
</body>
结果也显而易见,并不用我多说,这始终很常见的写法,想必大家也写过类似的代码,问题和上篇文章的计算奖金的“最笨方法”如出一辙,缺点也十分明显,
- 触发的函数较为庞大,包含了很多if-else分支,这些分支要覆盖所有的规则
- 函数缺乏弹性,如果要增加规则或者修改规则,就必须去函数内部修改代码
- 函数复用性很差
下面我们用策略模式重构代码,将校验规则分别抽离封装到校验对象中,作为策略类,将具体的业务初始化逻辑,包括参数传递,参数处理,规则调用,封装成另一个函数对象,最为环境类(Context)。直接上代码(可能环境类中处理参数和整体封装过程中有点繁琐,但是作为一个较为具体的业务场景,这种的复杂度是应该的,请仔细阅读)
<body>
请输入用户名:<input type="text" name="username"><br>
请输入密码:<input type="text" name="password"><br>
请输入手机号:<input type="text" name="phoneNumber"><br>
<button class="btn">提交</button>
<script>
// 封装策略对象
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
}
}
}
// 构建环境类
const Validator = function () {
this.cache = [] // 用于保存校验规则
}
Validator.prototype.add = function (dom, rule, errorMsg) {
const arr = rule.split(":") // 将传入的规则和参数分开
this.cache.push(function () { // 将校验的步骤封装成函数,存入缓存(cache)
let strategy = arr.shift() // 拿到用户传入的规则名称
arr.unshift(dom.value) // 将输入的数值添加至参数列表
arr.push(errorMsg) // 将错误对象添加至参数列表
return strategies[strategy].apply(dom, arr) // 返回对应规则调用结果
})
}
// 规则校验启动入口
Validator.prototype.start = function () {
for (var i = 0; i < this.cache.length; i++) {
let validatorFunc = this.cache[i]
let msg = validatorFunc()
if (msg) return msg
}
}
// 实现类 具体实现的代码,不是核心代码,上方两个类为核心代码
const volidataFunc = function () {
const validator = new Validator()
// 添加校验规则
validator.add(inputArr[0], 'isNonEmpty', '用户名不能为空')
validator.add(inputArr[1], 'minLength:6', '密码长度不能小于6位')
validator.add(inputArr[2], 'isMobile', '手机号码格式不正确')
console.log(validator.cache)
let errorMsg = validator.start()
return {errorMsg}
}
let inputArr = document.querySelectorAll("input")
let btn = document.querySelector(".btn")
btn.onclick = function () {
let {errorMsg} = new volidataFunc()
if (errorMsg) {
alert(errorMsg)
return false // 阻止表单提交(这里并未用表单结构,大家看看就行)
} else {
alert("登录成功")
}
}
</script>
</body>
使用策略模式重构代码后,我们仅仅可以只通过配置新的规则或者修改规则,然后将所需要的用到规则的节点传入规则就可以完成表单校验。嗯....其实这个版本以及可以满足大部分的表单校验了,但是,我们可以设想一下,如果一个输入项的校验不止一条规则怎么办?当然很简单,将数据项所对应的规则项里面多加判断呗,但是这样就又回到了之前的“最笨的写法”,最好的办法就是再加条规则,然后在校验时,用多条规则去校验所需的数据项。 那么我们需要怎么在哪改动代码呢?在添加规则的时候,添加多条规则吧,我想你应该也想到了,就是下方这样的写法(可以看成伪码)
validator.add(dom , [
{strategy: 'xxx' , errorMsg: 'xxx'}, // 规则一
{strategy: 'xxx' , errorMsg: 'xxx'} // 规则二
...
])
那么下面我们直接给出最终版本,没有注释哦,看看自己是否能理解,代码改动并不大
<body>
请输入用户名:<input type="text" name="username"><br>
请输入密码:<input type="text" name="password"><br>
请输入手机号:<input type="text" name="phoneNumber"><br>
<button class="btn">提交</button>
<script>
// 封装策略对象
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
}
}
}
// 构建环境类
const Validator = function () {
this.cache = [] // 用于保存校验规则
}
Validator.prototype.add = function (dom, rules) {
let self = this
for (let i = 0; i < rules.length; i++) {
let rule = rules[i];
(function (rule) {
let arr = rule.strategy.split(":")
let errorMsg = rule.errorMsg
self.cache.push(function () {
let strategy = arr.shift() // 拿到用户传入的规则名称
arr.unshift(dom.value) // 将输入的数值添加至参数列表
arr.push(errorMsg) // 将错误对象添加至参数列表
return strategies[strategy].apply(dom, arr) // 返回对应规则调用结果
})
})(rule)
}
}
// 规则校验启动入口
Validator.prototype.start = function () {
for (var i = 0; i < this.cache.length; i++) {
let validatorFunc = this.cache[i]
let msg = validatorFunc()
if (msg) return msg
}
}
let inputArr = document.querySelectorAll("input")
let btn = document.querySelector(".btn")
// 实现类
const volidataFunc = function () {
const validator = new Validator()
// 添加校验规则
validator.add(inputArr[0], [
{
strategy: 'isNonEmpty',
errorMsg: '用户名不能为空'
},
{
strategy: 'minLength:10',
errorMsg: '用户名长度不能小于10'
}
])
validator.add(inputArr[1], [
{
strategy: 'minLength:6',
errorMsg: '密码长度不能小于6'
}
])
validator.add(inputArr[2], [
{
strategy: 'isMobile',
errorMsg: '手机号格式不正确'
}
])
console.log(validator.cache)
let errorMsg = validator.start()
return { errorMsg }
}
btn.onclick = function () {
let { errorMsg } = new volidataFunc()
if (errorMsg) {
alert(errorMsg)
return false // 阻止表单提交(这里并未用表单结构,大家看看就行)
} else {
alert("登录成功")
}
}
</script>
</body>
以上就是整个通过策略模式实现表单验证,还是优化程度很高的
策略模式的优点与缺点
- 通过利用组合、委托和多态等技术和思想,可以避免多重条件选择的重复粘贴工作
- 其对开放-封闭原则完美支持
- 算法复用性强
当然策略模式也有缺点,但是并不严重,在使用策略模式时,同样是无法避免的会增加策略类的大小,而且分离与内容类外,代码可读性较差。哈哈哈,书上是这么说的,其实大家可以不用太过多考虑这个缺点,代码的复用性以及效率的提高,有时候必然会牺牲一定的空间和代码可读性,这里其实就是作为一些套话,让大家知道这个缺点,还有另一个缺点,属于核心缺点,会到最初的那个问题,出去旅行,我们需要怎么选择出行方式呢?当时我们的考虑是,根据自身的情况然后选择合适的方式吧,但是,我们必须得知道所有可能的路线,最后选择最优的那个方式吧,这就是策略模式的比较核心的缺点,我们不得不知道有哪些策略,最终再选其中某些策略去应用到内容中,最好的方式应该是,我们不需要知道里面总共有哪些策略,我们只需要提供内容,然后算法根据内容给出最优策略,这样应该是最优秀的业务解决方案,但是在策略模式中,这个问题是很难避免的,除非花费大量事件设计一个优秀的算法去匹配策略,这样就太麻烦了
其实策略模式已经在JavaScript这种高度函数化编码的语言中潜移默化的植入了,想必大家一定用过高阶函数吧,就是那种返回值是一个函数,或者传入的参数是一个函数的那种用法,是不是和策略模式很像呢,传入的函数参数就是一个策略,他可以通过不同的封装移植到不同的地方,不同的函数也可以返回不同的结果。
以上及时策略模式的所有了
内容摘自《JavaScript设计模式与开发实践》· 曾探 · 著