一、什么是策略模式
定义: 定义一系列的算法,把它们一个个封装起来,并且使它们可以互相替换。
一个基于策略模式的程序至少由两部分组成。
第一个部分是一组策略类 strategy
,策略类封装了具体的算法,并负责具体的计算过程。
第二个部分是环境类 Context
, Context
接受客户的请求,随后把请求委托给某一个策略类
二、策略模式的作用
在现实中,很多时候也有多种途径到达同一个目的地。比如我们要去某个地方旅游,可以根据具体的实际情况来选择出行的线路。
- 如果没有时间但是不在乎钱,可以选择坐飞机。
- 如果没有钱,可以选择坐大巴或者火车。
- 如果再穷一点,可以选择骑自行车。
在程序设计中,我们也常常遇到类似的情况,要实现某一个功能有多种方案可以选择。比如一个压缩文件的程序,既可以选择zip算法,也可以选择gzip算法。
这些算法灵活多样,而且可以随意互相替换。这种解决方案就是本章将要介绍的策略模式。
三、策略模式案例
1、计算奖金
案例描述:某公司的年终奖是根据员工的工资基数和年底绩效来发放的。例如,绩效为S的人年终奖有4倍工资,绩效为A的人年终奖有3倍工资,绩效为B的人年终奖有2倍工资,财务部要求我们提供一段代码,来方便他们计算员工的年终奖。
计算奖金:最初版本
const calculateBouns = function(level: string,salary: number) :number {
if (level === 'S') {
return salary * 4;
}
if (level === 'A') {
return salary * 3;
}
if (level === 'B') {
return salary * 2;
}
}
console.log(calculateBouns('S',4000)); // 输出16000
console.log(calculateBouns('A',3000)); // 输出9000
console.log(calculateBouns('B',2000)); // 输出4000
** 分析 **:
calculateBonus
函数比较庞大,包含了很多if-else
语句,这些语句需要覆盖所有的逻辑分支。calculateBonus
函数缺乏弹性,如果增加了一种新的绩效等级C,或者想把绩效S的奖金系数改为5,那我们必须深入calculateBonus
函数的内部实现,这是违反开放-封闭原则的。- 算法的复用性差,如果在程序的其他地方需要重用这些计算奖金的算法呢?我们的选择只有复制和粘贴。
计算奖金:(使用策略模式)面向对象完善版本
// 计算奖金:面向对象完善版本
class PerformanceS {
calculate(salary: number): number {
return salary * 4
}
}
class PerformanceA {
calculate(salary: number): number {
return salary * 3
}
}
class PerformanceB {
calculate(salary: number): number {
return salary * 2
}
}
interface strategy {
calculate: (salary: number) => number;
}
先创建一个 bonus(Context)
对象,并且给 bonus
对象设置一些原始的数据,比如员工的原始工资数额。
接下来把某个计算奖金的策略对象也传入bonus对象内部保存起来。
当调用 bonus.getBonus()
来计算奖金的时候,bonus
对象本身并没有能力进行计算,
而是把请求委托给了之前保存好的策略对象:
// Context 对象
class Bouns {
public salary: number; // 原始工资
public strategy: strategy; // 绩效等级对应的策略对象
setSalary(salary: number) {
this.salary = salary; // 设置员工的原始工资
}
setStrategy(strategy: strategy) {
this.strategy = strategy // 设置员工绩效等级对应的策略对象
}
getBouns() { // 取得奖金数额
return this.strategy.calculate(this.salary); // 把计算奖金的操作委托给对应的策略对象
}
}
const bouns = new Bouns();
bouns.setSalary(4000);
bouns.setStrategy(new PerformanceS());
console.log(bouns.getBouns()); // 输出16000
bouns.setSalary(3000);
bouns.setStrategy(new PerformanceA());
console.log(bouns.getBouns()); // 输出9000
bouns.setSalary(2000);
bouns.setStrategy(new PerformanceB());
console.log(bouns.getBouns()); // 输出4000
我们再来回顾一下策略模式的思想:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。这句话如果说得更详细一点,就是:定义一系列的算法,把它们各自封装成策略类,算法被封装在策略类内部的方法里。在客户对 Context
发起请求的时候,Context
总是把请求委托给这些策略对象中间的某一个进行计算。
计算奖金:JavaScript的完善版本
// 计算奖金:JavaScript的完善版本
// 在JavaScript语言中,函数也是对象,所以更简单和直接的做法是把strategy直接定义为函数
interface strategy {
S:(salary: number) => number;
A:(salary: number) => number;
B:(salary: number) => number;
}
const strategy: strategy= {
S: function(salary: number): number {
return salary * 4;
},
A: function(salary: number): number {
return salary * 3;
},
B: function(salary: number): number {
return salary * 2;
}
}
// Context
var calcluateBouns = function(level: string,salary: number): number{
return strategy[level](salary);
}
console.log(calcluateBouns('S',4000)); // 输出16000
console.log(calcluateBouns('A',3000)); // 输出9000
console.log(calcluateBouns('B',2000)); // 输出4000
2、表单验证
- 用户名(验证是否为空)
- 密码(验证长度不能小于6位)
- 手机号(验证是否是手机号格式)
表单验证:最初版本
<html>
<body>
<form action="http://xxx.com/register" id="registerForm" method="post">
请输入用户名:<input type="text" name="userName"/ >
请输入密码:<input type="text" name="password"/ >
请输入手机号码:<input type="text" name="phoneNumber"/ >
<button>提交</button>
</form>
<script>
var registerForm = document.getElementById( 'registerForm' );
registerForm.onsubmit = function(){
if ( registerForm.userName.value === '' ){
alert ( '用户名不能为空' );
return false;
}
if ( registerForm.password.value.length < 6 ){
alert ( '密码长度不能少于6位' );
return false; }
if ( !/(^1[3|5|8][0-9]{9}$)/.test( registerForm.phoneNumber.value ) ){
alert ( '手机号码格式不正确' );
return false;
}
}
</script>
</body>
</html>
分析:
registerForm.onsubmit
函数比较庞大,包含了很多if-else
语句,这些语句需要覆盖所有的校验规则。registerForm.onsubmit
函数缺乏弹性,如果增加了一种新的校验规则,或者想把密码的长度校验从6改成8,我们都必须深入registerForm.onsubmit
函数的内部实现,这是违反开放—封闭原则的。- 算法的复用性差,如果在程序中增加了另外一个表单,这个表单也需要进行一些类似的校验,那我们很可能将这些校验逻辑复制得漫天遍野
表单验证:策略模式案例
// 策略对象
const strategies: Object = {
isEmpty(value: string, errMsg: string): string {
if(value === '') {
return errMsg
}
},
minLength(value: string, length: number, errMsg: string) : string{
if (value.length<length) {
return errMsg
}
},
isMobile(value: string,errMsg: string): string {
if (!(/^1[34578]\d{9}$/.test(value))) {
return errMsg
}
}
}
// Context
class Validator {
public cache: Array<Function>;
constructor() {
this.cache = []
}
add(value: string, rule: string, msg: string) {
const params: Array<string> = rule.split(':');
this.cache.push(() => {
const strategy: string = params.shift();
params.unshift(value);
params.push(msg);
return strategies[strategy].apply(null, params)
})
}
check(): string {
let value: Function;
for (value of this.cache) {
const msg = value();
if (msg) {
return msg
}
}
}
}
var submitBtn = document.getElementById('submitBtn');
var registerForm = document.getElementById('registerForm');
var validateFunc = function() {
var validator = new Validator();
// 添加规则
validator.add(registerForm.username.value,'isEmpty','用户名不能为空');
validator.add(registerForm.password.value,'minLength:6','密码长度不能小于6位');
validator.add(registerForm.phone.value,'isMobile','手机号格式不正确');
// 校验结果
var errMsg = validator.check();
return errMsg;
}
submitBtn.onclick = function() {
var errMsg = validateFunc();
if(errMsg) {
console.log(errMsg);
return false;
} else {
console.log('表单验证成功')
}
}
四、策略模式的优缺点
优点:
- 策略模式利用组合、委托和多态等技术和思想,可以有效地避免多重条件选择语句。
- 策略模式提供了对开放—封闭原则的完美支持,将算法封装在独立的
strateg
(策略)中,使得它们易于切换,易于理解,易于扩展。 - 策略模式中的算法也可以复用在系统的其他地方,从而避免许多重复的复制粘贴工作。
- 在策略模式中利用组合和委托来让
Context
拥有执行算法的能力,这也是继承的一种更轻便的替代方案。
缺点:
1、使用策略模式会在程序中增加许多策略类或者策略对象,但实际上这比把它们负责的逻辑堆砌在 Context
中要好。
2、要使用策略模式,必须了解所有的 strategy
,必须了解各个 strategy
之间的不同点,这样才能选择一个合适的strategy
。比如,我们要选择一种合适的旅游出行路线,必须先了解选择飞机、火车、自行车等方案的细节。此时strategy
要向客户暴露它的所有实现,这是违反最少知识原则的。