策略模式

概念

策略模式又称政策模式,其定义一系列的算法,把他们一个个封装起来,并且使它们相互替换。封装的策略算法一般是独立的,策略模式根据输入来调整采用哪个算法。关键是策略的实现和使用分离。

现实生活策略模式

再举个栗子,一辆车的轮胎有很多规格,在泥泞路段开的多的时候可以用泥地胎,在雪地开得多可以用雪地胎,高速公路上开的多的时候使用高性能轮胎,针对不同使用场景更换不同的轮胎即可,不需更换整个车。

代码实现

场景:

某个电商网站举办一个活动,通过打折促销销售库存物品,有的商品满100减30,有的商品满200减80,有的商品直接8折出售(想起被双十一支配的恐惧么)。这样的逻辑,咋样实现的呢

  1. 普通编码
//普通编码
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. 普通的优化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)
//这样算法就被隐藏起来,并且预留了增加策略的入口,便于扩展。
  1. 策略模式的通用实现

根据上面的例子提炼一下策略模式,折扣计算方式可以被认为是策略(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

分析:

  1. 根据需求把不同转换的算法写在StrategyMap对象里。
  2. 根据Element formatter函数格式,使用StrategyContext方法返回一个formatter格式的函数给表格组件使用。

代码:

  1. 写一个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])
    }
}
  1. 对应的组件调用





  1. 效果图


    2.png

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 文件中提取出来的通用验证规则的方法名,第二个参数是报错的话表单验证的提示信息。




效果图

3.jpg

参考文献

  • https://element.eleme.cn/#/zh-CN
  • https://mp.weixin.qq.com/s/Kz0llZhayQ1PL3iaXh8eew
  • [正则在线测试工具]https://tool.oschina.net/regex/

你可能感兴趣的:(策略模式)