这是我参与8月更文挑战的第13天,活动详情查看:8月更文挑战
何为策略模式
定义一系列算法,把他们一个个都封装起来,并且使他们可以相互替换。
在我首次看定义的时候,就觉得是一系列的分支语句,比如:
function stragey(key){
switch(key){
case 'a': return 1;
case 'b':return 2;
....
}
}
stragey('b')
但如果要增加新的策略 c,就要深入函数内部,在switch语句中重新加一个分支。
使用策略模式重构
如果我们要重构一段代码,就要把可变的部分与不可变的部分区别开来,一个策略模式至少由两个部分组成,一是策略类,二是环境类。策略类我们都了解了,他是封装了具体的算法,负责具体的计算过程;环境类,就是接受客户的请求,然后把请求委托给某个策略。
我们从类的角度来实现一下:
// 策略类
class strategyA{...}
class strategyB{...}
// 环境类
class Context{
constructor(){
this.obj=null
this.strategy=null
}
setObj=(obj)=>{
this.obj=obj
}
setStrategy=(strategy)=>{
this.strategy=strategy
}
getResult=()=>{
return this.strategy.calculate(this.obj)
}
}
js 版本的策略模式实例
比如说,公司年底要做绩效统计生成年终奖,如果考评为 S 的会发 6 个月的工资并列入年度最佳员工列表,考评为 A 的会发 4 个月工资,考评为 B 的会发两个月工资,考评为 C 的会发 1 个月的工资,考评为 D 的直接解雇。
比如说我们的员工列表:
const employeeList = [
{ name: "朱一旦", level: "S", salary: 50 },
{ name: "马小男", level: "A", salary: 15 },
{ name: "马小玲", level: "B", salary: 7 },
{ name: "马小浩", level: "D", salary: 30 },
];
上一篇文章单例模式中说过,在 js 里写类生成唯一一个对象,如同“脱裤放屁————多此一举”,不如直接定义一个“对象”就好了,不同策略即是对象中不同的方法。
年终奖策略:
const FireList=[]
const strategy={
'S':(employee)=>{
pushToBestList(employee)
return employee.salary*6
}
'A':(employee)=>{
return employee.salary*4
}
'B':(employee)=>{
return employee.salary*2
}
'C':(employee)=>{
return employee.salary*1
}
'D':(employee)=>{
pushToFireList(employee)
return 0
}
}
我们要给财务一份年终表的表单,就可以这样计算:
const yearEndSalaryList=employeeList.map(employee=>{
const {level}=employee
return strategy[level](employee)
})
console.log(yearEndSalaryList) // 年终奖汇总
console.log(FireList) //解雇名单
用策略模式重构你的表单
如果你写过 vanillaJS,对以下表单校验的代码绝对不陌生:
const form = document.getElementById("form");
form.onsubmit = () => {
if (!form.accont.value) {
alert("账户名不能为空");
return;
}
if (form.password.value.length < 10) {
alert("密码长度需大于10位");
return;
}
};
这里我们直接获取表单的某些值,分别对某个属性进行处理。这段代码非常常见,但他的可复用性并不强,如果需要多个表单又得复制一通。那我们用策略模式将其整理一下:
这里的逻辑是:校验不通过则抛出带有错误信息的错误。
const createError = (msg) => {
const err = new Error();
err.msg = msg;
return err;
};
const strategy = {
required: (value, message) => {
if (!value || value.length === 0) {
throw createError(message);
}
},
minLength: (value, message, len) => {
if (!value || value.length < len) {
throw createError(message);
}
},
};
这样,策略模式中的策略类就 ok 了,接下来实现环境类。表单里的环境类,用于将表单数据委托给策略类进行处理。这里的逻辑是,先要将每个委托逐一与策略绑定,然后统一执行。
class Validator {
constructor() {
this.cache = [];
}
add(val, rule, msg) {
const [strategyName, param = null] = rule.split(":");
this.cache.push(function () {
strategy[strategyName].apply(null, [val, msg, param]);
});
}
validate() {
this.cache.forEach((val) => {
val();
});
}
}
const validate = (form) => {
const validator = new Validator();
validator.add(form.account, "required", "账户名不能为空");
validator.add(form.pswd, "minLength:10", "密码长度需大于10位");
try {
validator.validate();
} catch (err) {
alert(err.msg);
}
};
const mockForm = {
account: "",
pswd: "11",
};
validate(mockForm);
当然一个表单属性可能有多个校验规则,我们可以通过多加策略的方式进行多种规则校验,如下
const validate = (form) => {
const validator = new Validator();
validator.add(form.account, "required", "账户名不能为空");
validator.add(form.account, "minLength:5", "账户长度需大于5位");
validator.add(form.pswd, "minLength:10", "密码长度需大于10位");
......
};
也可以通过类似于 antd 表单校验规则的方式:
const stockValidator = (_: any, value: string) => {
if (
isNaN(Number(value)) ||
value.length !== 6 ||
(value[0] !== "3" && value[0] !== "6" && value[0] !== "0")
)
return Promise.reject("请输入正确的代码");
return Promise.resolve();
};
...
const rules = [
{ required: true, message: "输入股票代码" },
{ validator: stockValidator },
];
....
总结
策略模式利用组合、委托、多态来避免多重条件分支,使代码复用性更强,也是开放封闭原则的最好支持之一。但策略模式的缺点是,会在程序中增加许多策略类,每个策略类中的每条策略之间也必须清楚他们的区分。
最近在看《js 设计模式与开发实践》,本文是我对此书的一些概括与扩展。下一篇文章写代理模式~~