阅读 53

javascript设计模式 之 2 策略模式

1 策略模式的定义

定义一系列的算法,把它们一个个封装起来,并且使用它们可以相互替换。 举个例子:在程序设计中,我们需要实现某一个功能其实有多种方案可以选择,例如压缩文件的程序,我们可以选择zip的算法,也可以选择gzip的算法。这些算法灵活多样,而且可以随意相互替换。

2 使用策略模式计算奖金

在单位年终的时候,会对职员进行年终评价,不同等级获得不一样的奖金。A级获得4倍工资,B级获得3倍工资,C级获得2倍工资,D级只有一倍工资。我们需要设计一个计算年终的函数。

function calculate = function(salary, level) {
    switch(level) {
        case 'A':
            return salary * 4;
        case 'B':
            return salary * 3;
        case 'C':
            return salary * 2;
        default:  
            return salary;
    }
}
复制代码

上面是一般我们会选择的计算方式,这样设计有缺点:

  • calculate的函数比较大,Swicth-case中需要覆盖所有的逻辑分支
  • calculate的函数缺乏弹性,如果需要新增一种等级D,那么就需要修改calculate函数内部的实现。
  • 算法的复用性差,如果程序的其他地方需要重用这些计算奖金的算法,我们只能选择粘贴复制

2.1 使用策略模式重构代码

策略模式是定义一系列的算法,把它们一个个封装起来。将不变的部分变化的部分隔开是每个设计模式的主题,策略模式也不例外,策略模式的目的就是将算法的使用算法的实现分离开来。例子中,可以按一下方式理解:

  • 算法的实现是变化的,每种绩效对应的不同的计算规则
  • 算法的使用方式是不变的,都是根据某个算法取得计算后的奖金数额

策略模式的程序至少由两部分组成:

  • 策略类: 封装具体的算法,并负责具体的计算过程
  • 环境类Context: 接收用户请求,随后将请求委托给策略类

2.1.1 模仿面向对象的方式实现

  • performanceA, performanceB, performanceC, performanceOther :都是可变的策略类,封装计算规则。
  • Bonus: 环境类部分,接收用户请求,委托给策略类进行计算
// 策略类
var performanceA = function() {}
performanceA.calculate = function(salary) {
    return salary * 4;
}

var performanceB = function() {}
performanceB.calculate = function(salary) {
    return salary * 3;
}

var performanceC = function() {}
performanceC.calculate = function(salary) {
    return salary * 2;
}

var performanceOther = function() {}
performanceOther.calculate = function(salary) {
    return salary;
}

// 环境类Context
var Bonus = function() {
    this.salary = null; // 定义工资
    this.strategy = null; // 定义使用的策略类
}
Bonus.prototype.setSalary = function(salary) {
    this.salary = salary;
}
Bonus.prototype.setStrategy = function(strategy) {
    this.strategy = strategy;
}
Bonus.prototype.getBonus = function() {
    return this.strategy.calculate(this.salary);
}


复制代码

2.1.2 javascript版本的策略模式

上面strategy对象是从各个策略类中创建出来的,那是模拟传统的面向对象语言实现的。在javascript中,函数也就是对象,所以能够更加简单和直接的把strategy直接定义为函数。

var strategies = {
    'A': function(salary) {
        return 4 * salary;
    },
    'B': function(salary) {
        return 3 * salary;
    },
    'C': function(salary) {
        return 2 * salary;
    }
}

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

2.1.3 多态在策略模式中的体现

通过上面的重构,我们消除了switch分支的条件语句。把计算奖金的逻辑不再放入到context中,而是分布在各个策略对象中。

  • 每个策略对象负责的算法被各自封在了对象内部
  • Context没有计算奖金的能力,通过职责委托给了某个策略对象
    当对这些策略发起请求时计算奖金时,会根据各自不同的计算返回不同的结果,而这也是对象多态的体现。也是它们能够相互替换的目的。替换context中的当前保存的策略,遍能够知晓不同的算法来得到我们想要的结果。

3 使用策略模式进行表单校验

我们在编写注册界面的时候,点击注册按钮前需要对表单进行校验工作:

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

3.1 普通实现

首先我们不使用策略模式进行实现。该实现方式与计算奖金的实现问题一模一样。

  • registerForm.onSubmit函数很庞大,包含了if-else, 包含了所有的校验规则
  • registerForm.onSubmit函数缺乏弹性,如果想新增一个校验规则,或则修改规则,那么就需要深入到该函数的内部实现。违背了开放-封闭原则
  • 算法复用性差。如果项目的其他位置也需要相同的校验,需要拷贝复制
<html>
    <body>
    <form name="registerForm">
        请输入用户名: <input type="text" name="userName"/ >
        请输入密码: <input type="text" name="password"/ >
        请输入手机号码: <input type="text" name="phoneNumber"/ >
        <button>提交</button>
    </form>
    <script>
        var registerForm = document.forms['registerForm'];
        registerForm.onsubmit = function(event){
            if ( registerForm.userName.value === '' ){
                alert ( '用户名不能为空' );
                event.preventDefault();
            }
            if ( registerForm.password.value.length < 6 ){
                alert ( '密码长度不能少于 6 位' );
                 event.preventDefault();
            }
            if ( !/(^1[3|5|8][0-9]{9}$)/.test( registerForm.phoneNumber.value ) ){
                alert ( '手机号码格式不正确' );
                 event.preventDefault();
            }
        }
    </script>
    </body>
</html>
复制代码

3.2 使用策略模式重构

我们需要遵循的规则,依然是这两条:

  • 提取所有的可变原则,将校验规则封装起来作为策略类
  • 提取context内容,接收用户请求,通过委托给策略类进行计算 下面,我们实现的内容需求:
  • 调用validate.add()方法:添加校验规则(参数1:需要校验的字符串, 参数2:校验的规则数组,参数4:可选的正则)
  • 调用valiadte.start()方法:开始校验
<html>
    <body>
    <form name="registerForm">
        请输入用户名: <input type="text" name="userName"/ >
        请输入密码: <input type="text" name="password"/ >
        请输入手机号码: <input type="text" name="phoneNumber"/ >
        <button>提交</button>
    </form>
    <script>
    // 策略类
    var strategies = {
        isNotEmpty: function(str, errorMsg) {
            if (str === '') {
                return errorMsg;
            }
        },
        minLength: function(str, errorMsg, length) {
            if (str.length < length) {
                return errorMsg;
            }
        },
        isRegExp: function(str, errorMsg, regExp) {
            if (!regExp.test(str)) {
                return errorMsg;
            }
        }
    }

    // context类: 负责接收用户传入的请求,并委托给策略类。不可变
    var Validate =  function() {
        var cache = [];
        return {
            add: function(str, rules, regExp) {
                rules.map(function(rule) {
					var key = Object.keys(rule)[0];
                    var errorMsg = rule[key];
                    var ary = key.split(':');
					console.log(key, ary);
                    cache.push(function() {
                        // 加入有:分割,第一个则是策略
                        var strategy = ary.shift();
                        return strategies[strategy].call(null, str, errorMsg, regExp || ary.shift());
                    });
                }); 
               
            },
            start: function() {
				var msg = '';
				for (var i = 0; i < cache.length ; i++) {
					 msg = cache[i]();
					if (msg) {
                        alert(msg);
						break;
					}
				}
				return msg;
            }
        }
    };
    
    function validateRegister(registerForm) {
        var validate = Validate();
        validate.add(registerForm.userName.value, [{'isNotEmpty': '用户名不能为空'},{'minLength:3':'密码长度不能少于 3 位'}]);
        validate.add(registerForm.password.value, [{'minLength:6':'密码长度不能少于 6 位'}]);
        validate.add(registerForm.phoneNumber.value, [{'isRegExp':'手机号码格式不正确'}], /(^1[3|5|8][0-9]{9}$)/);
        var returnMsg = validate.start();
        return returnMsg ? false : true;
    }
    registerForm.onsubmit = function(event){
        var isPass = validateRegister(registerForm);
        if (!isPass) {
			event.preventDefault();
             console.log('no validate');
        } else {
           console.log('pass');
		   registerForm.submit();
        }
    }
    </script>
    </body>
</html>
复制代码

4 策略模式的优缺点

策略模式有点:

  • 利用组合,委托和多态等技术和思想,可以有效避免多重条件选择语句
  • 提供了对外开放-封闭的原则的完美支持。将算法封装在独立的strategy内,使得它们容易切换,易于理解和拓展
  • 策略模式中的算法可以提供给其他地方,避免了重复粘贴复制
  • 利用组合与委托让context拥有执行算法的能力。这也是继承的一种更轻便的替代方案

缺点:

  • 使用策略模式会让程序增加许多策略类或者策略对象。
  • 使用策略模式,必须要了解所有的strategy之前的不同点,这样才能选择一个适合的strategy。