花森

策略模式在数据校验中的应用

最近更新:

策略模式在数据校验中的应用 封面

# 策略模式在数据校验中的应用

设计模式就是在**面向对象**软件设计过程中针对**特定问题**的简洁而优雅的解决方案,包含`发布者订阅者模式`、`策略模式`、`单例模式`等模式,分别作用于不同场景,本文便涉及到策略模式在数据校验中的应用。



## 名词规约

1. 策略模式:设计模式中的策略模式;
2. 开放封闭原则:简称OCP,是所有面向对象原则的核心;
3. 知识最少原则:迪米特法则,简称LOD,如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,其目的是降低类之间的耦合度,提高模块的相对独立性;



## 介绍和理解

关于策略模式,即定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。光从字面上理解,算是比较晦涩难懂,接下来我们将用小篇幅的例子介绍理解一下策略模式。

### 问题场景

假设某公司的年终奖金由个人工资基数+个人评级组成,关系如下所示:

1. S级,个人工资基数*4;
2. A级,个人工资基数*3;
3. B级,个人工资基数*2;

### 实现代码

我们需要编写一段程序计算员工的年终奖金,于是有如下代码段:

```javascript
// 方法封装
let calculateBonus = function(level, salary) {
if ( level === 'S' ){
return salary * 4;
}
if ( level === 'A' ){
return salary * 3;
}
if ( level === 'B' ){
return salary * 2;
}
}

// 使用实例
calculateBonus("B", 1000); // 2000
calculateBonus("S", 1000); // 4000
```

我们不难发现代码逻辑很简单,但是缺陷也很明显,具体由如下几点:

- 当条件过多时,出现较多if-else,并且要覆盖所有的逻辑分支;
- 封装的方法缺乏弹性,若增加一个等级,则需要深入方法内部改动,违反开发封闭原则;
- 复用性较差;

### 重构代码

尝试使用策略模式,重构本场景中的方法,本着定义一定的算法,一个个封装起来,把**不变**的部分和**变动**的部分隔离开的思想。基于策略模式的程序至少有**策略类**和**环境类**组成,环境类不参与业务逻辑,而是将请求委托给某个策略类完成。Javascript不同于其他传统的面向对象语言,所以实现更为简便,具体重构代码如下所示:

```javascript
// 策略组
let strategies = {
"S": function(salary) {
reture salary * 4;
},
"A": function(salary) {
reture salary * 3;
},
"B": function(salary) {
reture salary * 2;
}
};

// 环境组
let calculateBonus = function(level, salary) {
reture strategies[level](salary);
}
```

由上面的例子,我们不难发现,环境组不直接执行业务相关,而是借助一定的算法,委托给策略类进行处理返回。如此一来可以轻易地将不变的部分抽离成库,方便复用拓展,如下图所示:

![vUoMuctBL29XE7l](https://s2.loli.net/2022/05/31/vUoMuctBL29XE7l.png)



## 表单校验中的运用

在日常开发中,我们会遇到注册、登陆、信息填写等功能,需要对大量的表单输入进行验证,而且重复性较大,所以我们照葫芦画瓢,尝试使用策略模式优化重构代码。

### 核心代码

环境类定义使用规则,通过算法转发校验请求,策略类处理环境类转发的**校验规则**和**数据**,属于动态变化的部分,属于团队间不断拓展完善的部分,建议抽离成单独文件。

```javascript
// 策略类
let strategies = {
isNotEmpty: function (value, errorMsg) {
if (value === '') {
return errorMsg || '非空'
}
},
minLength: function (value, length, errorMsg) {
if (value && value.length < length) {
return errorMsg || `长度不小于${length}`
}
},
maxLength: function (value, length, errorMsg) {
if (value && value.length > length) {
return errorMsg || `长度不大于${length}`
}
},
isMobile: function (value, errorMsg) {
if (!/(^1[3|5|8][0-9]{9}$)/.test(value)) {
return errorMsg || '输入正确的手机号'
}
},
isChinese: function (value, errorMsg) {
if (!/^[\u4E00-\u9FA5\uf900-\ufa2d0-9a-zA-Z]+$/.test(value)) {
return errorMsg || '输入字母/汉字/数字'
}
},
isIDCard: function (value, errorMsg) {
if (!/(^\d{15}$)|(^\d{17}(\d|X|x)$)/.test(value)) {
return errorMsg || '输入正确的身份证'
}
},
}

// 环境类
class Validator {
constructor(name) {
this.caches = [];
}
// 添加校验规则的方法
add(value, rules) {
rules.map(rule => {
// 处理策略标识,支持minLength:5规则写法,可以根据需要自定义使用规则
let strategyArr = rule.strategy.split(/:|:/)
// 1.获取策略标识
let strategy = strategyArr.shift()
// 2.压入校验的值
strategyArr.unshift(value) // 头插入校验值
// 3.压入错误提示文字
strategyArr.push(rule.errMsg)
// 压入待校验的规则集合
this.caches.push(() => {
return strategies[strategy].apply(this, strategyArr)
})

})
}
// 开始执行校验
start() {
for (let validatorFun of this.caches) {
let errText = validatorFun()
if (errText) {
return errText
}
}
}
}
```

### 直接单独使用

默认已经导入`Validator`类,通过`new`关键词实例,存在`add`添加表单项和校验规则,执行`start`方法,若校验不通过,则返回提示信息,否则返回`undefined`,接下来查看演示代码段:

```javascript
let validA = new Validator();
let validB = new Validator();
let aValue = ''; // 表单项a
let bValue = '1234'; // 表单项b
validA.add(aValue, [
{
strategy: 'isNotEmpty',
errMsg: '不能为空'
},
{
strategy: 'minLength:5',
errMsg: '长度不能小于5'
}
])
validB.add(bValue, [
{
strategy: 'isNotEmpty',
errMsg: '不能为空'
},
{
strategy: 'minLength:5',
errMsg: '长度不能小于5'
}
])
console.log(validA.start()) // 不能为空
console.log(validB.start()) // 长度不能小于5
```

### 一次校验多个表单项

回顾`直接单独使用`演示的实例,代码量不少,何来优雅之谈。不可以一次校验多个表单项,返回校验不通过的信息,所以我们可以基于之前的**策略类**和**环境类**,简单封装一个使用方法,主要代码如下:

```javascript
// 策略类
...

// 环境类
...

// 使用辅助方法,返回最先校验不通过的提示
function checkParamsByRules(arr) {
for (let item of arr) {
let v = new Validator();
v.add(item.value, item.rules);
let errText = v.start();
if (errText) {
return errText;
}
}
}

// 使用示例
let errText = checkParamsByRules([
{
value: '123567',
rules: [
{
strategy: 'isNotEmpty',
errMsg: '请输入信息'
},
{
strategy: 'minLength:5',
errMsg: '长度不能小于5'
},
]
},
{
value: '0123456',
rules: [
{
strategy: 'isIDCard',
errMsg: '请输入身份证'
},
]
},
])

console.log(errText) // 请输入身份证
```

### HUI/ElementUI中使用

当然可以用于结合`el-form`组件使用,我们可以自定义使用规则,常规使用如下所示:

```vue
// template模版的内容
<template>
<el-form
:rules="siteRules"
:model="siteForm"
>
<el-form-item label="名称" prop="name">
<el-input v-model="siteForm.name" placeholder="请输入网站名"></el-input>
</el-form-item>
</el-form>
</template>

// script模版的内容
<script>
export default {
name: 'Demo'
data(){
// 定义校验规则
const checkEmpty = (rule, value, cb) => {
let vali = new Validator();
vali.add(value, [
{
strategy: 'isNotEmpty',
errMsg: '必填项',
},
]);
let errText = vali.start();
if (errText) cb(new Error(errText)); // 存在报错则输出报错
cb(); // 正常放行
};
reture {
// el-form校验规则集
siteRules: {
name: [{ validator: checkEmpty, trigger: 'blur' }],
},
}
}
}
</script>
```



## 策略模式的利弊

### 优点

1. 策略模式利于组合、委托、多态等技术思想,可以有效避免过多重复的条件选择语句;
2. 完美地符合开放封闭原则,将算法封装在独立的策略类中,便于理解、替换、拓展;
3. 自定义环境类中的定义属于团队自己的使用规则;

### 缺点

1. 违反知识最少原则,我们必须知道策略类内的各种策略算法的含义,才能上手使用;
2. 一定程度上策略类会堆积很多算法;