设计模式
是一套被反复使用、多数人知晓、经过分类编目的、代码设计经验的总结。它是为了可重用代码,让代码更容易的被他人理解并保证代码的可靠性。设计模式合集链接:
概述
策略模式(Strategy Pattern)
指的是定义一系列的算法,把它们一个个封装
起来,并且使它们可以互相替换
。封装的策略算法一般是独立
的,策略模式根据输入来调整采用哪个算法。⽬的就是
将策略的实现和使用分离
。
策略模式中主要有下面概念:
Context
:封装上下文,根据需要调用需要的策略,屏蔽外界对策略的直接调用,只对外提供一个接口,根据需要调用对应的策略;Strategy
:策略,含有具体的算法,其方法的外观相同,因此可以互相代替;StrategyMap
:所有策略的合集,供封装上下文调用;
结构如下
:
日常示例
在生活中,螺丝的规格有很多,螺丝刀尺寸也不少,如果每碰到一种规格就买一个螺丝刀,那就得堆满了螺丝刀。所以现在人们都用多功能的螺丝刀套装,螺丝刀把只需要一个,碰到不同规格的螺丝只要换螺丝刀头就行了,很方便,体积也变小很多。
再举个栗子,一辆车的轮胎有很多规格,在泥泞路段开的多的时候可以用泥地胎,在雪地开得多可以用雪地胎,高速公路上开的多的时候使用高性能轮胎,针对不同使用场景更换不同的轮胎即可,不需更换整个车。
在这些场景中,有以下特点:
1、螺丝刀头/轮胎(策略)之间相互独立,但又可以相互替换;
2、螺丝刀/车(封装上下文)可以根据需要的不同选用不同的策略;
以上例子都很好地展示了策略模式的概念:根据需求动态切换不同的策略,而不是一直保留所有策略的实现。
实现
公司的年终奖
举例:公司的年终奖是根据员⼯的⼯资和绩效来考核的,绩效为A的⼈,年终奖为⼯资的4倍,绩效为B的⼈,年终奖为⼯资的3倍,绩效为C的⼈,年终奖为⼯资的2倍。
若使⽤ if 来实现,代码如下:
js
function calculateBonus(salary, performance) {
let bonus = 0;
if (performance === 'A') {
bonus = salary * 4;
} else if (performance === 'B') {
bonus = salary * 3;
} else if (performance === 'C') {
bonus = salary * 2;
} else {
console.log('Invalid performance level.');
}
return bonus;
}
const salary = 5000;
const performanceA = 'A';
const performanceB = 'B';
const performanceC = 'C';
console.log(`员工绩效为A,年终奖为:${calculateBonus(salary, performanceA)}`);
console.log(`员工绩效为B,年终奖为:${calculateBonus(salary, performanceB)}`);
console.log(`员工绩效为C,年终奖为:${calculateBonus(salary, performanceC)}`);
虽然使用 if-else 可以实现相同的功能,但是这样的做法的缺点也很明显:
- 随着条件的增多,代码会变得
臃肿和难以维护
。 - 后续改正的时候,需要在函数内部添加逻辑,
违反了开放封闭原则
. 可复用性差
,如果在其他的地方也有类似这样的算法,但规则不一样,上述代码不能复用;
相比之下,策略模式将不同的策略封装成独立的类,使得代码更加结构化,易于理解和修改。
⽽如果使⽤策略模式,就是先定义⼀系列算法,把它们⼀个个封装起来,将不变的部分和变化的部分隔开,如下:
js
const calculateBonus = {
A: function (salary) {
return salary * 4;
},
B: function (salary) {
return salary * 3;
},
C: function (salary) {
return salary * 2;
},
};
// 计算总绩效
function salaryCalculate(level, salary) {
return calculateBonus[level] && calculateBonus[level](salary);
}
console.log(salaryCalculate("A", 12000));
console.log(salaryCalculate("B", 12500));
这样算法的实现和算法的使用就被分开了,想添加新的算法也变得十分简单:
js
calculateBonus.D = function (salary) {
return salary * 0.5;
},
如果希望计算算法隐藏起来,那么可以借助 IIFE 使用闭包的方式,这时需要添加增加策略的入口,以方便扩展:
js
const calculateBonus = (function () {
const calculateBonusMap = {
A: function (salary) {
return salary * 4;
},
B: function (salary) {
return salary * 3;
},
C: function (salary) {
return salary * 2;
},
};
return {
salaryCalculate: function (level, salary) {
return calculateBonusMap[level] && calculateBonusMap[level](salary);
},
addStrategy: function (level, fn) {
// 注册新计算方式
if (calculateBonusMap[level]) return;
calculateBonusMap[level] = fn;
},
};
})();
console.log(calculateBonus.salaryCalculate("A", 12000));
console.log(calculateBonus.salaryCalculate("B", 12500));
calculateBonus.addStrategy("E", (salary) => {
return salary * 0.5;
});
console.log(calculateBonus.salaryCalculate("E", 12500));
这样算法就被隐藏起来,并且预留了增加策略的入口,便于扩展。
下面就来实现一个通用的策略模式,可以根据上面的例子提炼一下策略模式,绩效计算方式可以被认为是策略(Strategy),这些策略之间可以相互替代,而具体的计算过程可以被认为是封装上下文(Context),封装上下文可以根据需要选择不同的策略。
下面使用通用化的方法实现一下。
js
const StrategyMap = {};
function context(type, ...rest) {
return StrategyMap[type] && StrategyMap[type](...rest);
}
StrategyMap.A = function (salary) {
return salary * 4;
};
console.log(context("A", 27000));
表单验证
表单验证是一个常见的应用场景,而策略模式可以很好地应用于表单验证的实现。通过策略模式,可以将不同的验证规则封装成策略对象,根据具体的情况选择相应的验证策略进行验证。这样可以实现更加灵活和可扩展的表单验证功能。
下面是一个简单的示例代码,演示如何使用策略模式来实现表单验证:
js
// 定义表单验证策略对象
const strategies = {
isNonEmpty(value, errorMsg) {
if (value === '') {
return errorMsg;
}
},
isEmail(value, errorMsg) {
const emailReg = /^\w+([-+.]\w+)*@\w+([-.]\w+)*.\w+([-.]\w+)*$/;
if (!emailReg.test(value)) {
return errorMsg;
}
},
minLength(value, length, errorMsg) {
if (value.length < length) {
return errorMsg;
}
},
};
// 表单验证类
class Validator {
constructor() {
this.rules = [];
}
addRule(value, rule, errorMsg) {
this.rules.push(() => strategies[rule](value, errorMsg));
}
validate() {
for (let rule of this.rules) {
const errorMsg = rule();
if (errorMsg) {
return errorMsg;
}
}
}
}
// 使用策略模式进行表单验证
const validator = new Validator();
validator.addRule('example@example.com', 'isNonEmpty', '邮箱不能为空');
validator.addRule('example@example.com', 'isEmail', '请输入有效的邮箱地址');
const error = validator.validate();
if (error) {
console.log(error);
} else {
console.log('表单验证通过');
}
在这个示例中,我们首先定义了不同的验证策略(isNonEmpty、isEmail、minLength),然后创建了一个 Validator 类来管理表单验证规则,通过 addRule 方法添加需要验证的规则,最后使用 validate 方法进行表单验证。
通过策略模式,我们可以灵活地添加新的验证规则并组合不同的规则来实现复杂的表单验证逻辑,同时保持代码的可维护性和可扩展性。这样可以更好地应对不断变化的表单验证需求,并提供更好的用户体验。
优缺点
策略模式将算法的实现和使用拆分,这个特点带来了很多优点
:
策略之间相互独立
,但策略可以自由切换,这个策略模式的特点给策略模式带来很多灵活性,也提高了策略的复用率;- 如果不采用策略模式,那么在选策略时一般会采用多重的条件判断,采用策略模式可以
避免多重条件判断,增加可维护性
; 可扩展性好
,策略可以很方便的进行扩展;
缺点
:
- 策略相互独立,因此一些
复杂的算法逻辑无法共享,造成一些资源浪费
; - 如果用户想采用什么策略,必须了解策略的实现,因此所有策略都需向外暴露,这是违背迪米特法则/最少知识原则的,也
增加了用户对策略对象的使用成本
。
应用场景
策略模式的使用场景如下:
- 多个算法只在行为上稍有不同的场景,这时可以使用策略模式来动态选择算法;
- 算法需要自由切换的场景;
- 有时需要多重条件判断,那么可以使用策略模式来规避多重条件判断的情况;
策略模式可以⽤来封装⼀系列的"业务规则"
,只要这些业务规则指向的⽬标⼀致,并且可以被替换使⽤,我们就可以⽤策略模式来封装它们。