javascript 设计模式之策略模式

文章系列

javascript 设计模式之单例模式javascript

javascript 设计模式之适配器模式html

javascript 设计模式之装饰者模式java

javascript设计模式之代理模式git

javascript 适配、代理、装饰者模式的比较github

javascript 设计模式之状态模式web

javascript 设计模式之迭代器模式算法

javascript 设计模式之策略模式设计模式

javascript 设计模式之观察者模式markdown

javascript 设计模式之发布订阅者模式app

概念

策略模式:定义一系列的算法(这些算法目标一致),把它们一个个封装起来,而且使它们能够相互替换。

本文代码

从公司绩效谈起

每家公司年终奖的发放都会根据该年度员工表现给予必定的奖惩,固然 A 公司也不例外, A 公司的年终绩效制度以下:

  1. 等级为 S 的,年终奖为工资的 4倍
  2. 等级为 A 的,年终奖为工资的 3 倍
  3. 等级为 B 的,年终奖为工资的 2 倍
  4. 等级为 C 的,年终奖为工资的 0.3 倍

代码实现以下:

var calculateBonus = function (performanceLevel, salary) {
    if (performanceLevel == 'S') {
        return salary * 4
    }
    if (performanceLevel == 'A') {
        return salary * 3
    }
    if (performanceLevel == 'B') {
        return salary * 2
    }
    if (performanceLevel == 'C') {
        return salary * 0.3
    }
}
console.info(calculateBonus('S', 20000)) // 80000
console.info(calculateBonus('C', 15000)) // 4500
复制代码

功能上能够正常使用,但存在以下几个缺点:

  1. 违反单一功能原则,在一个函数里处理了四个绩效逻辑,绝对的胖逻辑,这种的铁定要拆
  2. 违反开放封闭原则,若是要再加个绩效为 D 的逻辑,还得往 calculateBonus 函数里添加一段逻辑,这样提测的时候,还要求测试人员重头将全部绩效等级测遍。
  3. 算法复用性差,若是其余地方须要用到这种计算规则,只能从新输出(复制、粘贴)

优化一(组合函数)

让咱们先来解决第三点算法复用性差问题。 把各类算法(即年终奖的计算规则)封装到一个个独立的小函数中。 代码改为以下:

var performanceS = function (salary) {
    return salary * 4
}
var performanceA = function (salary) {
    return salary * 3
}
var performanceB = function (salary) {
    return salary * 2
}
var performanceC = function (salary) {
    return salary * 0.3
}
var calculateBonus = function (performanceLevel, salary) {
    if (performanceLevel == 'S') {
        return performanceS(salary)
    }
    if (performanceLevel == 'A') {
        return performanceA(salary)
    }
    if (performanceLevel == 'B') {
        return performanceB(salary)
    }
    if (performanceLevel == 'C') {
        return performanceC(salary)
    }
}
console.info(calculateBonus('S', 20000)) // 80000
console.info(calculateBonus('C', 15000)) // 4500
复制代码

采用组合函数,将每一个绩效算法抽离成单独的函数,是解决了复用问题,若是有别的地方要计算 S 等级的薪资,直接调用 performanceS 函数便可。 但上面一、2两个问题仍然存在,咱们继续优化,引出主角策略模式

优化二(策略模式)

设计模式中很重要的一点就是将不变的部分和变化的部分分离出来,而策略模式的目的就是将算法的使用和算法的实现分离开来。

在这个例子中,算法的使用方式是不变的,都是根据等级和薪资计算年终奖;算法的实现是变化的,好比 S 有 S 等级的计算方式, A 有 A 等级的计算方式。

策略模式的组成:

  1. 一组策略类,策略类封装了具体的算法,并负责具体的计算过程。
  2. 环境类Context,Context接收客户的请求,随后把请求委托给某一个策略类。

定义策略类:

//策略类(S)
class performanceS {
    calculate(salary) {
        return salary * 4;
    }
}

//策略类(A)
class performanceA {
    calculate(salary) {
        return salary * 3;
    }
}

//策略类(B)
class performanceB {
    calculate(salary) {
        return salary * 2;
    }
}

//策略类(C)
class performanceC {
    calculate(salary) {
        return salary * 0.3;
    }
}
复制代码

定义环境类:

// 环境类
class Bonus {
    constructor() {
        this.salary = null;     //原始工资
        this.strategy = null;  //绩效公司对应的策略对象
    }
    setSalary(salary) {
        this.salary = salary;  //设置原始工资
    }
    setStrategy(strategy) {
        this.strategy = strategy; //设置员工绩效等级对应的策略对象
    }
    getBonus() {//取得奖金数额
        //维持对策略对象的引用
        //委托给对应的策略对象
        return this.strategy.calculate( this.salary );  
    }
}
复制代码

验证:

const bonus = new Bonus()
bonus.setSalary( 20000 );
bonus.setStrategy( new performanceS() ); //设置策略对象

console.info(bonus.getBonus()) // 80000
复制代码

上述展现了策略模式的应用,代码比较清晰,解决了上述的几大问题,要再增长个绩效 D 的逻辑,不会动到 Bonus 类,只要再定义个 performanceD 策略类便可,在 Bonus 类里作的事情也很单一,负责接收策略类实例,并调用。

上述展现的策略模式是基于传统的面向对象语言,能够进一步对这段代码进行优化,变成JavaScript版本的策略模式。

优化三(JavaScript版本策略模式)

在 JS 中,函数也是对象,因此能够将策略类直接定义为函数,并以对象映射形式展现。

//策略对象
var strategies = {
  //一系列算法
  "S": function ( salary ) {
    return salary * 4;
  },
  "A": function ( salary ) {
    return salary * 3;
  },
  "B": function ( salary ) {
    return salary * 2;
  },
  "C": function (salary) {
    return salary * 0.3;
  },
};
复制代码

同时,也能够将环境类定义为函数,改为以下:

var calculateBonus = function ( level, salary) {
  return strategies[ level ]( salary );    
};
复制代码

验证下:

console.log( calculateBonus('S', 20000)); // 80000
复制代码

再来看下策略模式的定义

定义一系列的算法(这些算法目标一致),把它们一个个封装起来,而且使它们能够相互替换。

  • 算法:绩效的计算方法
  • 封装:计算方法被封装在策略对象内部,达到可复用
  • 相互替换:要更改某个绩效时,只要改变参数,不影响函数的调用

更广义的"算法"

策略模式指的是定义一系列的算法,而且把它们封装起来,可是策略模式不只仅只封装算法,咱们还能够用来封装一系列的业务规则,只要这些业务规则目标一致,咱们就可使用策略模式来封装它们;好比表单验证

表单验证

需求:

  1. 用户名不能为空
  2. 密码长度不能少于6位
  3. 手机号码必须符合格式

简单实现

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>表单验证</title>
</head>
<body>
  <form action='xxx.com' 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>
复制代码

跟上述绩效奖金,存在同样的问题,函数过于庞大、缺少弹性以及复用性差,下面采用策略模式优化

优化一(策略模式)

采用策略模式,首先要定义策略类,那策略类要先找到算法具体是指什么:表单验证逻辑的业务规则

因此定义策略类以下:

// 定义策略类
const strategies = {
    isNonEmpty: function ( value, errorMsg) {
        if ( value === '') {
            return errorMsg;
        }
    },
    minLength: function ( value, length, errorMsg ) {
        if ( value.length < length ) {
            return errorMsg
        }
    },
    isMobile: function ( value, errorMsg) {
        if ( !/(^1[3|5|8][0-9]{9}$)/.test( value )) {
            return errorMsg;
        }
    }
}
复制代码

接着要定义环境类,在定义环境类以前,先看下客户通常是怎么使用的?

var validataFunc = function () {
    //建立一个validator对象
    var validator = new Validator();
    //添加校验规则
    validator.add( registerForm.userName, 'isNonEmpty', '用户名不能为空');
    validator.add( registerForm.password, 'minLength:6', '密码长度不能少于6位');
    validator.add( registerForm.phoneNumber, 'isMobile', '手机号码格式不正确');
    var errorMsg = validator.start();
    //返回校验结果
    return errorMsg;
}
var registerForm = document.getElementById('registerForm');
registerForm.onsubmit = function () {
    var errorMsg = validataFunc();   //若是存在,则说明未经过校验
    if ( errorMsg ) {
        alert( errorMsg );
        return false; //阻止表单提交
    }
}
复制代码

从上述代码中,能够明确在环境类 Validator 中要有 add 方法,经过 add 方法来添加校验规则。

同时有 start 方法,经过 start 方法开始校验,若是有错误,那么就返回错误信息( errorMsg )

有了策略对象以及策略对象与环境类( Validator )的桥梁,咱们即可以写出 Validator 类代码

class Validator {
    constructor() {
        this.cache = [];  //保存校验规则 
    }
    //添加检验规则函数
    add(dom,rule,errorMsg){
        //把strategy和参数分开'minLength:6' 如'minLength:6' -> ["minLength", "6"]
        let ary = rule.split(':'); 
        this.cache.push ( function () {
            //用户挑选的strategy ["minLength", "6"] -> 'minLength'
            let strategy = ary.shift();  
            //把input的value添加进参数列表
            ary.unshift( dom.value ); 
            //把errorMsg添加进参数列表
            ary.push( errorMsg ); 
            //委托策略对象调用
            return strategies[ strategy ].apply( dom, ary ); 
        })
    }
    start(){
        for ( var i = 0,validatorFunc; validatorFunc = this.cache[i++];) {
            var msg = validatorFunc(); //开始校验,并取得校验后的返回信息
            if ( msg ) {  //若是msg存在,则说明校验不经过
                return msg; 
            }
        }
    }
}
复制代码

在上述中,经过对业务规则这种算法的抽象,经过策略模式来完成表单检验,在修改某个校验规则的时候,咱们只有修改少许代码便可。好比想把用户名的输入改为不能少于4个字符,只须要把minLength:6改成minLength:4便可

优化二(多个校验规则)

目前实现的表单校验有一点小问题:一个文本输入框只能对应一种校验规则。 若是想要添加多种检验规则,能够经过如下方式添加:

validator.add( registerForm.userName, [{
    strategy: 'isNonEmpty',
    errorMsg: '用户名不能为空'
},{
    strategy: 'minLength:10',
    errorMsg: '用户名长度不能小于10位'
}])
复制代码

要修改 Validator 中的 add 方法,经过遍历的方式,把多个检验规则添加到cache中。

add (dom, rules) {
    let self = this;
    for (let i = 0,rule; rule = rules[i++];) {
        (function ( rule ) {
            let strategyAry = rule.strategy.split( ':' );
            let errorMsg = rule.errorMsg;
            self.cache.push( function () {
                let strategy = strategyAry.shift();
                strategyAry.unshift( dom.value );
                strategyAry.push( errorMsg );
                return strategies[ strategy ].apply( dom, strategyAry )
            })
        })(rule)
    }
}
复制代码

总结

优势:

  • 策略之间相互独立,但策略能够自由切换,这个策略模式的特色给策略模式带来不少灵活性,也提升了策略的复用率;
  • 若是不采用策略模式,那么在选策略时通常会采用多重的条件判断,采用策略模式能够避免多重条件判断,增长可维护性;
  • 可扩展性好,策略能够很方便的进行扩展;

缺点:

  • 在策略模式中,咱们会增长不少策略类、策略对象
  • 要使用策略模式,咱们必须了解到全部的 strategy 、必须了解各个 strategy 之间的不一样点,才能选择一个适合的 strategy 。好比,咱们要选择一种合适的旅游出行路线,必须先了解选择飞机、火车、自行车等方案的细节。此时 strategy 要向客户暴露它的全部实现,这是违反最少知识原则的。

使用场景:

  1. 多个算法只在行为上稍有不一样的场景,这时可使用策略模式来动态选择算法;
  2. 算法须要自由切换的场景;
  3. 有时须要多重条件判断,那么可使用策略模式来规避多重条件判断的状况;

参考连接

JavaScript设计模式与开发实践

结语

你的点赞是对我最大的确定,若是以为有帮助,请留下你的赞扬,谢谢!!!