JavaScript设计模式与开发实践-设计模式(策略模式)

150 阅读7分钟
在程序设计中,我们常常遇到类似的情况,要实现一个功能有多种方案可以选择。

比如我们要去某个地方旅游,可以根据具体的实际情况来选择出行路线。
1. 如果没有时间但是不在乎钱,可以选择坐飞机。
2. 如果没有钱,可以选择坐大巴或者火车。
3. 如果再穷一点,可以选择自行车。

比如一个压缩文件的程序,既可以选择zip算法,也可以选择gzip算法。
...

策略模式的定义定义一系列的算法,把他们一个个封装起来,并且使它们可以互相替换。

1. 使用策略模式计算奖金
1.1 最初的实现代码
        var calculateBonus = function (performanceLevel, salary) {
            if (performanceLevel === 'S') {
                return salary * 4
            }
            if (performanceLevel === 'A') {
                return salary * 3
            }
            if (performanceLevel === 'B') {
                return salary * 2
            }
        }
        console.log(calculateBonus('B', 20000)) // 40000
        console.log(calculateBonus('S', 6000)) // 24000
    可以发现,上段代码十分简单,但是存在着显而易见的缺点。
    1. calculateBonus函数比较庞大,包含了很多if-else语言,这些语句需要覆盖所有的逻辑分支。
    2. calculateBonus函数缺乏弹性,如果增加了一种新的绩效等级C, 或者想把绩效S的奖金系数改为5,
       那么我们必须深入calculateBonus函数的内部实现,这是违反开发-封闭原则的。
    3. 算法的复用性差,如果再程序的其他地方需要谨慎重用这些计算奖金的算法呢?我们的选择只有复制黏贴。
1.2 使用组合函数重构代码

我们能不能把一种计算方式封装到一个个的小函数里面,然后这些小函数有着良好的命名,这样可以一目了然的制度他对应着哪种算法,他们也可以被复用在其他程序的地方。

            var performanceS = function (salary) {
                return salary * 4
            }

            var performanceA = function (salary) {
                return salary * 3
            }

            var performanceB = function (salary) {
                return salary * 2
            }

            var calculateBonus = function (performance, salary) {
                if (performance === 'S') {
                    return performanceS(salary)
                }
                if (performance === 'A') {
                    return performanceA(salary)
                }
                if (performance === 'B') {
                    return performanceB(salary)
                }
            }
            
            console.log(calculateBonus('A', 10000)) // 30000

看上面的代码,目前虽然程序得到了一定的改善,但是这种改善非常有限,我们依然没有解决最重要的问题。calculateBonus函数有可能越来越庞大,而且在系统变化的时候缺乏弹性。

1.3 使用策略模式重构代码

使用策略模式来重构代码。策略模式指的是定义一些列的算法,把他们一个个封装起来。将不变的部分和变化的部分隔开是每个设计模式的主题,策略模式也不例外,策略模式的目的就算将算法的使用与算法的实现分离开来。 在这个例子里,算法的使用方式是不变的,都是根据某个算法取得计算后的奖金数额。而算法的实现是各异和变化的,每种绩效对应着不同的计算规则。
一个基于策略模式的程序至少由两个部分组成。第一个部分是一组策略类,策略类封装了具体的算法,并负责具体的计算过程。第二部分是环境类Context,Context接受客户的请求,随后把请求委托给某一策略类。要做到这点。说明Content中要维持对某个策略对象的引用。

(1).先把每种绩效的计算规则都封装在对应的策略类里面

            var performanceS = function() {}
            performanceS.prototype.calculate = function (salary) {
                return salary * 4
            }

            var performanceA = function () {}
            performanceA.prototype.calculate = function (salary) {
                return salary * 3
            }

            var performanceB = function() {}
            performanceB.prototype.calculate = function (salary) {
                return salary * 2
            }

(2).接下来定义奖金类 Bonus

           var Bonus = function() {
                this.salary = null // 原始工资
                this.strategy = null // 绩效等级对应的策略对象

            }

            Bonus.prototype.setSalary = function(salary) {
                this.salary = salary // 设置员工的原始工资
            }
            Bonus.prototype.setStrategy = function(startegy) {
                this.strategy = startegy // 设置员工绩效等级对应的策略对象
                console.log("整个策略对象:", this.strategy)
            }
            Bonus.prototype.getBonus = function() { // 取得奖金数额
                console.log("调用策略对象上的方法并传入原始工资")
                return this.strategy.calculate(this.salary) // 把计算奖金的操作委托给对方的策略对象
            }

(3).使用Bonus类

策略模式思想:定义一系列的算法,把他们一个个封装起来,并且使它们可以互相替换。
就是定义一系列的算法,把他们各种封装成策略类,算法被封装在策略类内部的方法里。
在客户对Context发起请求的时候,Context总是把请求委托给这些策略对象中间的某一个进行计算。

以下代码过程:先创建一个bonus对象,并且给bonus对象设置一些原始的数据,比如员工的原始工资数额。接下来把某个计算奖金的策略也传入bonus对象内部保存起来。当调用bonus.getBonus()来计算奖金的时候,bonus对象本身并没有能力进行计算。而是把请求委托给了之前保存好的策略对象:

            var bonus = new Bonus()
            bonus.setSalary(10000)
            bonus.setStrategy(new performanceS()) // 设置策略对象
            console.log(bonus.getBonus()) // 40000

            bonus.setStrategy(new performanceA) // 设置策略对象
            console.log(bonus.getBonus()) // 30000
2. JavaScript版本的策略模式
           var strategies = {
                "S": function(salary) {
                    return salary * 4
                },
                "A": function(salary) {
                    return salary * 3
                },
                "B": function (salary) {
                    return salary * 2
                }
            }

            var calculateBonus = function (level, salary) {
                // return Object.hasOwn(strategies, level) ? strategies[level](salary) : null
                return strategies[level](salary)
            }

            console.log(calculateBonus('S', 2000)) // 8000
            console.log(calculateBonus('A', 2000)) // 6000
3. 多态在策略模式中的体现

通过使用策略模式重构代码,我们消除了原程序中大片的条件分支语句。所有跟计算奖金有关的逻辑不再放在Context中,而是分布在各个策略对象中。Context并没有计算奖金的能力,而是把这个职责委托给了某个策略对象。每个策略对象赋值的算法已被各自封装在对象内部。当我们对这些策略对象发出了"计算奖金"的请求时,它会返回各自的不同计算结果,这正是对象多态性的提醒,也是"他们可以相互替换"的目的。替换Context中当前保存的策略对象,并能执行不同的算法来得到我们想要的结果。

4. 表单校验
    <form action="http://xxx.com/register" id="registerForm" method="post">
        请输入用户名:<input type="text" name="userName" /><br/>
        请输入密码:<input type="text" name="password" /><br/>
        请输入手机号码:<input type="text" name="phoneNumber" /><br/>
        <button>提交</button>
    </form>
             // 策略对象
            var 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
                    }
                }
            }
            // Validator类
            var Validator = function () {
                this.cache = []
            }

            Validator.prototype.add = function (dom, rules) {

                var self = this

                for(var i = 0, rule; rule = rules[i++];) {
                    (function (rule) {
                        var strategyAry = rule.strategy.split(":")
                        var errorMsg = rule.errorMsg

                        self.cache.push(function() {
                            var strategy = strategyAry.shift()
                            strategyAry.unshift(dom.value)
                            strategyAry.push(errorMsg)
                            return strategies[strategy].apply(dom, strategyAry)
                        })
                    })(rule)
                }
            }

            Validator.prototype.start = function () {
                for(var i = 0, validatorFunc; validatorFunc = this.cache[i++];) {
                    var errorMsg = validatorFunc()
                    if (errorMsg) {
                        return errorMsg
                    }
                }
            }
            // 客户调用代码
            var registerForm = document.getElementById('registerForm')
            var validataFunc = function () {
                var validator = new Validator()

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

                validator.add(registerForm.password, [
                    {strategy: 'minLength:6', errorMsg: '密码产犊不能小于6位'}
                ])

                validator.add(registerForm.phoneNumber, [
                    {strategy: 'isMobile', errorMsg: '手机号码格式不正确'}
                ])

                var errorMsg = validator.start()
                return errorMsg
            }

            registerForm.onsubmit = function() {
                var errorMsg = validataFunc()

                if (errorMsg) {
                    console.log(errorMsg)
                    return false
                }
                console.log("验证正确")
            }
5. 策略模式的优缺点

缺点:

  1. 使用策略模式会在程序中增加许多策略类或者策略对象,但实际上这比他们负责的逻辑堆砌在Context中要好。
  2. 要使用策略模式,必须了解所有的strategy,必须了解各个strategy,必须了解各个strategy之间的不同点。这样才能选择一个合适的strategy。比如,我要选择一种合适的旅游出行路线,必须先了解选择飞机,火车,自行车等方案的细节。此时strategy要向客户暴露它的所有实现,这是违反最少知识原则的。