概念
策略模式又称政策模式,其定义一系列的算法,把他们一个个封装起来,并且使它们相互替换。封装的策略算法一般是独立的,策略模式根据输入来调整采用哪个算法。关键是策略的实现和使用分离。
现实生活策略模式
再举个栗子,一辆车的轮胎有很多规格,在泥泞路段开的多的时候可以用泥地胎,在雪地开得多可以用雪地胎,高速公路上开的多的时候使用高性能轮胎,针对不同使用场景更换不同的轮胎即可,不需更换整个车。
代码实现
场景:
某个电商网站举办一个活动,通过打折促销销售库存物品,有的商品满100减30,有的商品满200减80,有的商品直接8折出售(想起被双十一支配的恐惧么)。这样的逻辑,咋样实现的呢
- 普通编码
//普通编码
function priceCalculate(type,price) {
let result;
if(type == "max_30") {
result = price - Math.floor(price / 100) * 30;
}else if(type == "max_80") {
result = price - Math.floor(price / 200) * 80;
}else if(type == "rate_80") {
result = price * 0.8;
}
return result;
}
let a = priceCalculate("max_30", 270);//210
let b = priceCalculate("max_80", 250);//170
let c =priceCalculate("rate_80", 250);//200
console.log(a, b, c);
缺点:
- priceCalculate 函数随着折扣类型的增多,if-else 判断语句会变得越来越臃肿;
- 如果增加了新的折扣类型或者折扣类型的算法有所改变,那么需要更改 priceCalculate 函数的实现,这是违反
开放-封闭原则
的; - 可复用性差,如果在其他的地方也有类似这样的算法,但规则不一样,上述代码不能复用;
- 普通的优化1和优化2
// 优化1 :算法实现和算法使用拆分。
//把不同的算法使用一个对象封装
const DiscountMap = {
max_30(price) {
return price - Math.floor(price / 100) * 30;
},
max_80(price) {
return price - Math.floor(price / 200) * 80;
},
rate_80(price) {
return price * 0.8;
}
}
//根据不同优惠类型计算,优惠后的金额
function priceCalculate(type, price) {
return DiscountMap[type] && DiscountMap[type](price);
}
let a = priceCalculate("max_30", 270);//210
let b = priceCalculate("max_80", 250);//170
let c =priceCalculate("rate_80", 250);//200
console.log(a, b, c);
// 这样把算法的实现和算法的使用拆分开;添加算法简单
DiscountMap.max_90 = (price) => {
return price - Math.floor(price / 150) * 90;
}
// 优化2: 把算法方法隐藏起来,可以借助IIFE使用闭包方式,这时需要额外给调用者添加策略的入口,方便扩展。
//通用的封装机制,闭包实现
const PriceCalculate = (function() {
// 策略实现
const DiscountMap = {
max_30(price) {
return price - Math.floor(price / 100) * 30;
},
max_80(price) {
return price - Math.floor(price / 200) * 80;
},
rate_80(price) {
return price * 0.8;
}
}
return {
// 策略使用
priceClac(type,price) {
return DiscountMap[type] && DiscountMap[type](price);
},
//添加策略实现的方法,方便扩展。
addStrategy(type,fn) {
if(DiscountMap[type]) return;
DiscountMap[type] = fn;
}
}
})()
PriceCalculate.priceClac('minus100_30', 270) // 输出: 210
PriceCalculate.addStrategy('minus150_40', function(price) {
return price - Math.floor(price / 150) * 40
})
let d = PriceCalculate.priceClac('minus150_40', 270) // 输出: 230
console.log(d)
//这样算法就被隐藏起来,并且预留了增加策略的入口,便于扩展。
- 策略模式的通用实现
根据上面的例子提炼一下策略模式,折扣计算方式可以被认为是策略(Strategy),这些策略之间可以相互替代,而具体折扣的计算过程可以被认为是封装上下文(Context),封装上下文可以根据需要选择不同的策略。
主要有下面几个概念:
- Context :封装上下文,根据需要调用需要的策略,屏蔽外界对策略的直接调用,只对外提供一个接口,根据需要调用对应的策略;
- Strategy :策略,含有具体的算法,其方法的外观相同,因此可以互相代替;
- StrategyMap :所有策略的合集,供封装上下文调用;
const StrategyMap = {}
function context(type, ...rest) {
return StrategyMap[type] && StrategyMap[type](...rest)
}
StrategyMap.minus100_30 = function(price) {
return price - Math.floor(price / 100) * 30
}
context('minus100_30', 270) // 输出: 210
场景应用
1.Vue+ElementUI 表格formatter
用法:
这个用法和layui,themlef的框架中的formatter作用一样。
formatter 用来格式化内容 Function(row, column, cellValue, index){}。
需求:
以文件大小转化为例,后端经常会直接传 bit 单位的文件大小,那么前端需要根据后端的数据,根据需求转化为自己需要的单位的文件大小,比如 KB/MB
分析:
- 根据需求把不同转换的算法写在StrategyMap对象里。
- 根据Element formatter函数格式,使用StrategyContext方法返回一个formatter格式的函数给表格组件使用。
代码:
- 写一个utils.js文件 存放实现文件的算法
// 策略模式实现 element的表格 formatter
const StartegyMap = {
bitToKB: val => {
const num = Number(val);
return isNaN(num)? val : (num / 1024).toFixed(0) + "KB"
},
bitToMB: val => {
const num = Number(val);
return isNaN(num)? val : (num / 1024 / 1024).toFixed(0) + "MB"
}
}
// type:表示什么类型的算法;rowKey:表示element表格中的column的row中一个属性名;
export const strategyContext = function(type,rowKey) {
// 返回一个 formatter 函数格式
return function(row,column,cellValue,index) {
return StartegyMap[type](row[rowKey])
}
}
- 对应的组件调用
-
效果图
2.表单验证
除了表格中的 formatter 之外,策略模式也经常用在表单验证的场景,这里举一个 Vue + ElementUI 项目的例子,其他框架同理。
ElementUI 的 Form 表单 具有表单验证功能,用来校验用户输入的表单内容。实际需求中表单验证项一般会比较复杂,所以需要给每个表单项增加 validator 自定义校验方法。
我们可以像官网示例一样把表单验证都写在组件的状态 data 函数中,但是这样就不好复用使用频率比较高的表单验证方法了,这时我们可以结合策略模式
和函数柯里化
的知识来重构一下。首先我们在项目的工具模块(一般是 utils 文件夹)实现通用的表单验证方法
element表单 自定义校验规则用法:
var validatePass = (rule, value, callback) => {
if (value === '') {
callback(new Error('请输入密码'));
} else {
if (this.ruleForm.checkPass !== '') {
this.$refs.ruleForm.validateField('checkPass');
}
callback();
}
};
rules: {
pass: [
{ validator: validatePass, trigger: 'blur' }
],
checkPass: [
{ validator: validatePass2, trigger: 'blur' }
],
...
]
}
// src/utils/validates.js
/* 姓名校验 由2-10位汉字组成 */
export function validateUsername(str) {
const reg = /^[\u4e00-\u9fa5]{2,10}$/
return reg.test(str)
}
/* 手机号校验 由以1开头的11位数字组成 */
export function validateMobile(str) {
const reg = /^1\d{10}$/
return reg.test(str)
}
/* 邮箱校验 */
export function validateEmail(str) {
const reg = /^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/
return reg.test(str)
}
然后在 utils/index.js 中增加一个柯里化方法,用来生成表单验证函数:
// src/utils/index.js
import * as Validates from'./validates.js'
/* 生成表格自定义校验函数 */
export const formValidateGene = (key, errMsg) =>(rule, value, cb) => {
if (Validates[key](value)) {
cb()
} else {
cb(newError(errMsg))
}
}
上面的 formValidateGene 函数接受两个参数,第一个是验证规则,也就是 src/utils/validates.js 文件中提取出来的通用验证规则的方法名,第二个参数是报错的话表单验证的提示信息。
效果图
参考文献
- https://element.eleme.cn/#/zh-CN
- https://mp.weixin.qq.com/s/Kz0llZhayQ1PL3iaXh8eew
- [正则在线测试工具]https://tool.oschina.net/regex/