愿君学长松,慎勿作桃李。受屈不改心,然后知君子

《JavaScript设计模式与开发实践》-- 策略模式

详情个人博客:https://shengchangwei.github.io/js-shejimoshi-celue/

策略模式

1、定义

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

2、使用策略模式计算奖金

很多公司的年终奖是根据员工的工资基数和年底绩效情况来发放的。例如,绩效为 S 的人年终奖有 4 倍工资,绩效为 A 的人年终奖有 3 倍工资,而绩效为 B 的人年终奖是 2 倍工资。假设财务部要求我们提供一段代码,来方便他们计算员工的年终奖。

1. 最初的实现代码 --(缺点太多)

编写calculateBonus函数接收两个参数: 员工的工资数额和他的绩效考核等级。
代码如下:

 if ( performanceLevel === 'S' ){ 
 return salary * 4; 
 } 
 if ( performanceLevel === 'A' ){ 
 return salary * 3; 
 } 
 if ( performanceLevel === 'B' ){ 
 return salary * 2; 
 } 
}; 
calculateBonus( 'B', 20000 ); // 输出:40000 
calculateBonus( 'S', 6000 ); // 输出:24000

缺点:

  • calculateBonus 函数比较庞大,包含了很多 if-else 语句,这些语句需要覆盖所有的逻辑
    分支。
  • calculateBonus 函数缺乏弹性,如果增加了一种新的绩效等级 C,或者想把绩效 S 的奖金系数改为 5,那我们必须深入 calculateBonus 函数的内部实现,这是违反开放封闭原则的。
  • 算法的复用性差,如果在程序的其他地方需要重用这些计算奖金的算法呢?我们的选择只有复制和粘贴。

2. 使用组合函数重构代码 ---(改善有限)

代码如下:

var performanceS = function( salary ){ 
    return salary * 4; 
}; 
2 
var performanceA = function( salary ){ 
    return salary * 3; 
}; 
3 
var performanceB = function( salary ){ 
    return salary * 2; 
}; 
var calculateBonus = function( performanceLevel, salary ){
    if ( performanceLevel === 'S' ){ 
        return performanceS( salary ); 
    } 

    if ( performanceLevel === 'A' ){ 
        return performanceA( salary ); 
    }
    if ( performanceLevel === 'B' ){ 
        return performanceB( salary ); 
    }
}; 
calculateBonus( 'A' , 10000 ); // 输出:30000 8

缺点: 这种改善非常有限,我们依然没有解决最重要的问题:calculateBonus 函数有可能越来越庞大,而且在系统变化的时候缺乏弹性。

3. 使用策略模式重构代码(模仿传统面向对象语言中的实现)(作为了解)

// 绩效S
var performanceS = function(){};
performanceS.prototype.calculate = function(salary) {
    return salary * 4;
}
// 绩效A
var performanceA = function(){};
performanceA.prototype.calculate = function(salary) {
    return salary * 3;
}
// 绩效B
var performanceA = function(){};
performanceA.prototype.calculate = function(salary) {
    return salary * 2;
}

// 定义奖金类Bonus

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); // 把计算奖金的操作委托给对应的策略对象
}

// 使用
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

这段代码是基于传统面向对象语言模仿,自我感觉太过啰嗦,一些比较少的判断太过啰嗦

3、JavaScript 版本的策略模式 (平时自己写代码用的比较多的思想)

上一节我们让 strategy 对象从各个策略类中创建而来,这是模拟一些传统面向对象语言的实现。实际上在 JavaScript 语言中,函数也是对象,所以更简单和直接的做法是把 strategy直接定义为函数:

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 strategies[ level ]( salary ); 
};
console.log( calculateBonus( 'S', 20000 ) ); // 输出:80000 
console.log( calculateBonus( 'A', 10000 ) ); // 输出:30000

在自己平时项目开发中,遇到许多if-else判断时,首先考虑到将这些不变的条件独立封装成一个对象,然后根据判断条件从对象中获取对应的值,在这里多说几句,其实个人感觉我们在实际开发中或多或少的都用到一些设计模式,只是自己没有将一些思想提取出来,没有归类总结。而学习设计模式正是将我们实际开发中自己用到的一些思想归类,将那种模糊想要抓住的思想用一个名称去标识,下次遇到同样的业务问题可以很快的定位到自己要使用的模式,这是一种提升,废话少说,继续下文。

4、策略模式 -- 表单验证

规则:

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

4.1 第一版本(最基础的)

html

<form action="http:// xxx.com/register" id="registerForm" method="post">
   请输入用户名:<input type="text" name="userName"/ >
   请输入密码: <input type="text" name="password"/ >
   请输入手机号码:<input type="text" name="phoneNumber"/ > 
   <button>提交</button>
</form>

js

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; 
     
   } 
}

缺点:

  • registerForm.onsubmit 函数比较庞大,包含了很多 if-else 语句,这些语句需要覆盖所有的校验规则。
  • registerForm.onsubmit 函数缺乏弹性,如果增加了一种新的校验规则,或者想把密码的长度校验从 6 改成 8,我们都必须深入 registerForm.onsubmit 函数的内部实现,这是违反开放—封闭原则的。
  • 算法的复用性差,如果在程序中增加了另外一个表单,这个表单也需要进行一些类似的校验,那我们很可能将这些校验逻辑复制得漫天遍野。

4.2 策略模式重构表单校验

代码如下:

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;l
        }
    }
}

接下来我们准备实现 Validator 类。Validator 类在这里作为 Context,负责接收用户的请求并委托给 strategy 对象。在给出 Validator 类的代码之前,有必要提前了解用户是如何向 Validator类发送请求的,这有助于我们知道如何去编写 Validator 类的代码。代码如下:

var validataFunc = function() {
    var validator = new Validator(); // 创建一个对象 Validtor类在下面
    /*****************添加一些校验规则*******************/
    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类的实现

var Validator = function() {
    this.cache = [];
};
Validator.prototype.add = function(dom, rule, errorMsg) {
    var ary = rule.split(':');
    this.cache.push(function() {
        var strategy = ary.shift();
        ary.unshift(dom.value);
        ary.push(errorMsg);
        return strategies[strategy].apply(dom, ary);
    })
};
Validator.prototype.start = function() {
    for(var i = 0, validatorFunc; validatorFunc = this.cache[ i++ ];) {
        var msg = validatorFunc();
        if ( msg ){ // 如果有确切的返回值,说明校验没有通过
            return msg; 
        }
    }
}

4.3 给某个文本输入框添加多种校验规则

期待校验方式:

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

下面提供的代码可用于一个文本输入框对应多种校验规则:

<form action="http:// xxx.com/register" id="registerForm" method="post"> 
    请输入用户名:<input type="text" name="userName"/ > 
    请输入密码:<input type="text" name="password"/ > 
    请输入手机号码:<input type="text" name="phoneNumber"/ > 
    <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: '用户名长度不能小于 10 位' 
        }]); 
        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 ){ 
        alert ( errorMsg ); 
        return false; 
    }
 };

5、策略模式的优缺点

优点:

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

缺点:(了解)

  • 使用策略模式会在程序中增加许多策略类或者策略对象,但实际上这比把它们负责的逻辑堆砌在 Context 中要好。
  • 要使用策略模式,必须了解所有的 strategy,必须了解各个 strategy 之间的不同点,这样才能选择一个合适的 strategy。违反最少知识原则的。

6、小结

在平时项目开发中,遇到许多if-else判断时,首先考虑策略模式的思想,将一些不变的东西单独封装成对象或函数。

posted @ 2019-08-28 13:08  圣墨  阅读(288)  评论(0编辑  收藏  举报