策略模式的定义是: 定义一系列的算法(这些算法目标一致), 把它们一个个封装起来, 并且使它们可以相互替换.
比如要实现从上海到广州, 既可以坐火车, 又可以坐高铁, 还可以坐飞机. 这取决与个人想法. 在这里, 不同的到达方式就是不同的策略, 个人想法就是条件.
1. 计算奖金
以计算奖金为例, 绩效为 S 的年终奖是 4 倍工资, 绩效为 A 的年终奖是 3 倍工资, 绩效为 B 的年终奖是 2 倍工资. 那么这里奖金取决于两个条件, 绩效和薪水. 最初编码实现如下:
- const calculateBonus = (performanceLevel, salary) => {
- if(performanceLevel === 'S') {
- return salary * 4;
- }
- if(performanceLevel === 'A') {
- return salary * 3;
- }
- if(performanceLevel === 'B') {
- return salary * 2;
- }
- }
这段代码十分简单, 但存在显而易见的缺点.
if 语句过多, 需要涵盖所有的条件.
弹性差, 如果新增绩效 C, 那么需要什么 calculateBonus 的内部实现去修改代码, 不符合开放封闭原则.
复用性差, 计算奖金的算法不能直接复用, 除非复制粘贴.
2. 使用策略模式
策略模式是指定义一系列的算法, 并将它们封装起来, 这很符合开闭原则. 策略模式的目的就是将算法的使用和算法的实现分离出来.
一个基于策略模式的程序至少由两部分组成. 第一个部分是策略类, 它封装了具体的算法, 并负责计算的具体过程. 第二个部分是环境类 Context,Context 接受客户的请求, 随后将请求委托给某一个策略类. 要做到这点, Context 中需要维持对某个策略对象的引用.
现在使用策略模式来重构以上代码, 第一个版本是基于 class, 第二个版本是基于函数.
2.1 基于 class
- class PerformanceS {
- calculate(salary) {
- return salary * 4;
- }
- }
- class PerformanceA {
- calculate(salary) {
- return salary * 3;
- }
- }
- class PerformanceB {
- calculate(salary) {
- return salary * 2;
- }
- }
- class Bonus {
- constructor(strategy, salary) {
- this.strategy = strategy;
- this.salary = salary;
- }
- getBonus() {
- if(!this.strategy) {
- return -1;
- }
- return this.strategy.calculate(this.salary);
- }
- }
- const bonus = new Bonus(new PerformanceA(), 2000);
- console.log(bonus.getBonus()) // 6000
它没有上述的三个缺点. 在这里, 有三个策略类, 分别是 PerformanceS,Performance A,PerformanceB. 这里的 context 就是 Bonus 类, 它接受客户的请求(bonus.getBonus), 将请求委托给策略类. 它保存着策略类的引用.
2.2 基于函数
上述中, 每一个策略都是 class, 实际上, class 也是一个函数. 这里, 可以直接用函数实现.
- const strategies = {
- 'S': salary => salary * 4,
- 'A': salary => salary * 3,
- 'B': salary => salary * 2,
- }
- const getBonus = (performanceLevel, salary) => strategies[performanceLevel](salary)
- console.log(getBonus('A', 2000)) // 6000
3. 多态在策略模式中的体现
通过使用策略模式重构代码, 消除了程序中大片的条件语句. 所有和奖金相关的逻辑都不在 Context 中, 而是分布在各个策略对象中. Context 并没有计算奖金的能力, 当它接收到客户的请求时, 它将请求委托给某个策略对象计算, 计算方法被封装在策略对象内部. 当我们发起 "获得奖金" 的请求时, Context 将请求转发给策略类, 策略类根据客户参数返回不同的内容, 这正是对象多态性的体现, 这也体现了策略模式的定义 --"它们可以相互替换".
4. 计算小球的缓动动画
我们的目标是编写一个动画类和缓动算法, 让小球以各种各样的缓动效果在页面中进行移动.
很明显, 缓动算法是一个策略对象, 它有几种不同的策略. 这些策略函数都接受四个参数: 动画开始的位置 s, 动画结束的位置 e, 动画已消耗的时间 t, 动画总时间 d.
- const tween = {
- linear: (s, e, t, d) => { return e*t/d + s },
- easeIn: () => { /* some code */ },
- easeOut: () => { /* some code */ },
- easeInOut: () => { /* some code */ },
- }
页面上有一个 div 元素.
<div id='div' style='position: absolute; left: 0;'></div>
现在要让这个 div 动起来, 需要编写一个动画类.
- const tween = {
- linear: () => { /* some code */ },
- easeIn: () => { /* some code */ },
- easeOut: () => { /* some code */ },
- easeInOut: () => { /* some code */ },
- }
- class Animation {
- constructor(dom) {
- this.dom = dom;
- this.startTime = 0;
- this.startPos = 0;
- this.endPos = 0;
- this.propertyName = null;
- this.easing = null;
- this.duration = null;
- }
- // 开始动画
- start(propertyName, endPos, duration, easing) {
- this.startTime = Date.now();
- // 初始化参数, 省略其他
- const self = this;
- // 循环执行动画, 如果动画已结束, 那么清除定时器
- let timer = setInterval(() => {
- if(self.step() === false) {
- clearInterval(timer);
- }
- }, 1000/60);
- }
- // 计算下一次循环到的时候小球位置
- step() {
- const now = Date.now();
- if(now> this.startTime + this.duration) {
- return false;
- } else {
- // 获得小球在本次循环结束时的位置并更新位置
- // const pos = this.easing();
- // this.update(pos);
- }
- }
- update(pos) {
- this.dom.style[propertyName] = pos + 'px';
- }
- }
具体实现略去. 这里的 Animation 类就是环境类 Context, 当接收到客户的请求(更新小球位置 self.step()), 它将请求转发给策略内(this.easing()), 策略类进行计算并返回结果.
5. 更广义的 "算法"
策略模式指的是定义一系列的算法, 并且把他们封装起来. 上述所说的计算奖金和缓动动画的例子都封装了一些策略方法.
从定义上看, 策略模式就是用来封装算法的. 但如果仅仅将策略模式用来封装算法, 有些大材小用. 在实际开发中, 策略模式也可以用来封装一些的 "业务规则". 只要这些业务规则目标一致, 并且可以替换, 那么就可以用策略模式来封装它们. 以使用策略模式来完成表单校验为例.
6. 表单校验
- <form action='xxx' id='form' method='post'>
- <input type='text' name='username'>
- <input type='password' name='passsword'>
- <button > 提交</button>
- </form>
验证规则如下:
- const form = document.querySelector('form')
- form.onsubmit = () => {
- if(form.username.value === '') {
- alert('用户名不能为空')
- return false;
- }
- if(form.password.value.length <6) {
- alert('密码不能少于 6 位')
- return false;
- }
- }
这是一种很常见的思路, 和最开始计算奖金一样. 缺点也是一样.
6.1 使用策略模式重构表单校验
第一步需要把这些校验逻辑封装成策略对象.
- const strategies = {
- isNonEmpty: (value, errMsg) => {
- if(value === '') {
- return errMsg
- }
- },
- minLength: (value, errMsg) => {
- if(value.length <minLength) {
- return errMsg
- }
- }
- }
第二步对表单进行校验.
- class Validator {
- constructor() {
- this.rules = [];
- }
- add(dom, rule, errMsg) {
- const arr = rule.split(':');
- this.rules.push(() => {
- const strategy = arr.shift();
- arr.unshift(dom.value);
- arr.push(errMsg);
- return strategies[strategy].apply(dom, arr);
- })
- }
- start() {
- for(let i = 0, validatorFunc; validatorFunc = this.rules[i++];) {
- let msg = validatorFunc();
- if(msg) {
- return msg;
- }
- }
- }
- }
- const form = document.querySelector('form')
- form.onsubmit = (e) => {
- e.preventDefault();
- const validator = new Validator();
- validator.add(form.username, 'isNonEmpty', '用户名不能为空');
- validator.add(form.password, 'minLength:6', '密码长度不能小于 6 位');
- const errMsg = validator.start();
- if(errMsg) {
- alert(errMsg);
- return false;
- }
- }
上述例子中, 校验逻辑是策略对象, 其中包含策略的实现函数. Validator 类是 Context, 用于将客户的请求 (表单验证) 转发到策略对象进行验证. 与计算奖金的 Bonus 不同的是, 这里并没有将验证参数通过构造函数传入, 而是通过 validator.add 传入相关验证参数, 通过 validator.start()进行验证.
策略模式优缺点
策略模式利用组合, 委托和多态等技术和思想, 可以有效避免多重选择语句.
策略模式通过扩展策略类, 对开放封闭原则完全支持, 使得它们易于切换和扩展.
策略模式的算法可以用在其他地方, 避免复制.
策略模式利用组合和委托让 Context 拥有执行算法的能力, 这也是继承的一种更轻便的替代方案.
前三点正是开头实现的计算奖金函数的缺点. 策略模式有一点缺点, 不过并不严重.
会增加策略类或者策略对象, 增加了复杂度. 但是与 Context 解耦了, 这样更便于扩展.
使用策略模式, 必须了解所有的策略以便选择合适的策略, 这是 strategies 要向客户暴露它的所有实现, 不符合最少知识原则.
来源: https://juejin.im/post/5c31bc4c51882526300b6a2a