JavaScript 策略模式

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

策略模式计算奖金

奖金发放的场景:绩效为S的人年终奖为4倍工资,绩效为A的年终奖为3倍工资,绩效为B的年终奖为2倍工资,那我们的编程的代码。

最初代码的实现

var calculateBouns = function(performanceLevel, salary){

  if(performanceLevel === 'S'){
    return salary * 4;
  }

  if(performanceLevel === 'A'){
    return salary*3;
  }

  if(performanceLevel === 'B'){
    return salary*2;
  }
};

calculateBouns ('B', 2000); // 4000
calculateBouns ('S', 6000); // 24000

现在的这段代码十分简单,但是存在很多的显而易见的问题。

  • 如果有很多的等级的话,就会有很多的if-else的语句,这样的语句需要覆盖整个的逻辑分支。
  • 该函数缺乏弹性,如果增加一个等级C的话,或是将S的系数进行更改的话,必须要进入到函数的内部进行实现,违反了开放-封闭原则。
  • 算法的复用性很差,如果在其他的地方进行重复的逻辑,我们只能进行复制粘贴,不能直接进行代码的复用。

由于上面的代码存在很大的问题,所以我们决定对代码进行重构。

使用组合函数进行代码的重构
我们最容易想到的办法是通过组合函数进行代码的重构,,我们把各种算法封装到一个小的函数中,这些小的函数都有良好的命名,可以一目了然的知道用到了什么算法。还是上面的使用场景,进行代码的优化:

var performanceLevelS = function(){
  return salary*4;
}
var performanceLevelA = function(){
  return salary*3;
}
var performanceLevelB = function(){
  return salary*2;
}

var calculateBouns = function(performanceLevel, salary){

  if(performanceLevel === 'S'){
    return performanceLevelS;
  }

  if(performanceLevel === 'A'){
    return performanceLevelA;
  }

  if(performanceLevel === 'B'){
    return performanceLevelB;
  }
};

calculateBouns ('B', 2000); // 4000

上面的写法我们在一定的程度上解决了一定的问题,但是这样的改善是十分的有限的,当我们的逻辑很复杂的时候,calculateBouns 函数可能越来越大,而且系统的变化的时候,弹性有限。

使用策略模式重构代码
我们想到可以使用策略模式进行重构代码,策略模式的意义就是:定义一系列的算法,并将这些算法封装起来,把不变的部分和变化的部分分割开来是每一个设计模式的主题,策略模式也是不例外的,策略模式的目的就是将算法的使用和实现分割开来。
在例子中,我们注意到了,算法使用的方式是不变的,都是根据某个算法取得计算后的的奖金的金额,而且算法的实现是各异和变化的,不同的绩效对应着不同的计算公式。
一个基于策略模式的程序至少要有两个部分,第一部分是策略类:封装了具体的算法,并且负责具体计算的部分;第二部分是环境类context,用来接收客户的请求,然后将请求委托给某一个策略类。所以context中要维系对于某个策略对象的引用。
首先使用策略类模仿传统的面向对象的实现,将绩效的计算规则封装到对应的策略类里面

var performanceLevelS = function(){};

performanceLevelS.prototype.calculate = function(salary){
  return salary*4;
}

var performanceLevelA = function(){};

performanceLevelA.prototype.calculate = function(salary){
  return salary*3;
}

var performanceLevelB = function(){};

performanceLevelB.prototype.calculate = function(salary){
  return salary*2;
}

定义一下奖金类:

var Bonus = function(){
  this.strategy = null;
  this.salary = 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);
}

我们在看一下策略模式的思想:定义一系列的算法,把它们封装起来,并且使他们可以相互进行替换。我们再暂看说一下:定义一系列的算法,把它们各自封装成策略类,算法封装在策略类内部的方法里,在客户端对context发起请求,Context总是把请求委托给这些策略对象中间的某一个进行计算。
至于如何完成剩下的代码,我们首先先创建一个bonus对象,并且给bonus对象设置一些原始的数据,然后呢,把某个计算奖金的策略对象也传入到bonus对象内部并进行保存,当调用bonus.getBonus的时候,bonus对象本身并没有能力去计算,而是把请求委托给之前保存好的策略对象。

var  bonus = new Bonus();

bonus.setSalary(1000); // 设置初始工资
bonus.setStrategy (new performanceLevelA ()); //设置策略
console.log(bonus.getBonus());

针对JavaScript版本的策略模式
上面的代码我们是模仿在传统的面向对象语言来进行实现的,实际上在JavaScript语言中,函数也是对象,所以最简单最直接的方法是把strategy指定定义为函数。

var strategis = {
  "S":function(salary){
    return salary * 4;
  }
  "A":function(salary){
      return salary * 3;
    }
  "B":function(salary){
      return salary * 2;
    }
};

同样,context也没有必要必须用Bonus类来标识,我们依然使用calculateBonus函数充当context来接受用户的请求,经过改造,代码的结结构会更加简洁。

var strategis = {
  "S":function(salary){
    return salary * 4;
  }
  "A":function(salary){
      return salary * 3;
    }
  "B":function(salary){
      return salary * 2;
    }
};
var calculateBonus = function(level, salary){  

console.log(calculateBonus('S'),20000) // 80000
}

我们通过策略模式来进行代码的重构,我们消除了原程序中大片的条件分支语句。多有的有关计算的逻辑不放在context中,而是分布在策略对象中,context没有计算的能力,而是将这个职责委托给了某个策略对象。每一个策略对象负责的算法被封装在各自的对象内部,有一点向传统的多态,因此各个策略对象彼此之前是可以互换的。
表单验证
我们假设写一个注册的页面,在点击注册按钮之前,我们添加几条校验的逻辑。

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

当我们还没有引入策略模式的时候,我们的代码是下面这样的:

<form id = 'registerForm' method = 'post'>
    请输入用户名:<input type="text" name="userName"/>
    请输入密码:<input type="text" name="password" />
    请输入手机号:<input type="text" name="phoneNumber" />
    <button>提交</button>
</form>
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]{9}$)/.test(registerForm.phoneNumber.value) ){
        alert('手机号码格式不正确');
        return false;   
    }
}

这是最简单最常见的编码方式,但是它的缺点和计算奖金最开始的模板是一样的

  • registerForm.onsubmit 函数比较庞大,尤其是我们需要校验的表格比较负责的时候,包含了很多的if-else语句,这些语句需要覆盖现在所有的校验规则
  • registerForm.onsubmit 函数缺乏弹性,如果增加新的校验规则,或者是想要将6为密码修改为8位密码,我们必须深入到函数的内部进行实现,违反了开闭原则
  • 算法的复用性很差,如果还有一个表单需要校验,我们几乎要将这个逻辑完全的复制。

策略模式的表单验证

var strategies = {
    isNotEmpty: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]{9}$)/.test(value)){
            return errorMsg;
        }
    }
}

将各个算法进行简单的封装,为后续的调用提供方便,现在我们准备实现validator类作为context,负责接收用户的请求并委托给strategy对象。

var validataFunc = function(){
    var validator = new Validator();
        
    //添加一些校验规则
    validator.add(registerForm.userName, 'isNotEmpty', '用户名不为空');
    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(){ //如果errorMsg有返回值,说明没有通过校验
    if(errorMsg){
        alert(errorMsg);
        return false;//阻止表单的提交
    }
}

在代码中,我们先创建一个validator对象,然后通过validator.add方法向validator对象中添加一些校验规则。validator.add方法接受3个参数,用下面的这段来吗来进行说明。validator.add(registerForm.password, 'minLength:6', '密码长度不能小于6位');

  • registerForm.password为参加校验的input的输入框
  • minLength:6用冒号进行分隔的字符串,冒号前是算法的名字,冒号后面是我们要传入的参数,如果不需要额外的信息,就不用添加冒号,直接传入算法的名字
  • 第三个参数:当校验未通过的时候返回的错误信息

当我们向validator对象里添加完一系列的的校验规则之后,会调用validator.start()方法来启动校验,当该方法返回一个确切的errorMsg的值的时候,这个时候onsubmit方法会返回false来阻止表单的提交。让我们最后来构建Validator类的实现

var Validator = function (){
    this.cache = []; //检验校验的规则
}
Validator.prototype.add = function(dom, rule, erroMsg){
    for(var i = 0, validataFunc; validataFunc = this.cache[i++]){
        var msg = validataFunc();
        if(msg){
            return msg;
        }
    }
}

我们以后想要加入规则,只需要配置一些简单的表单验证,这些规则也可以简单的复用到程序的任何地方,甚至可以以插件的形式移植到其他的项目中去。假设我们想要修改密码长度不可以小于10位,我们可以这么进行调用validator.add(registerForm.password, 'minLength:10', '密码长度不能小于10位');
给某一个文本框添加多种校验规则
如果我们想给某一个文本框添加多种校验规则的话,比如说:我们要校验输入的用户名不为空且长度不小于10位,我们怎么进行校验呢?

validator.add(registerForm.userName, [
{
strategy:'isNotEmpty',
errorMsg:'用户名不能为空'
}, {
strategy:'minLength:10',
errorMsg:'用户名长度不能小于10位'
}
]);
//策略对象
var strategies = {
    isNotEmpty: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]{9}$)/.test(value)){
            return errorMsg;
        }
    }
}
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, validataFunc; validataFunc = this.cache[i++]){
        var msg = validataFunc();
        if(msg){
            return msg;
        }
    }
};      
//客户端进行调用的时候
var registerForm = document.getElementById('registerForm');
var validataFunc = function(){
    var validator = new Validator();
            
    validator.add(registerForm.userName,[{
        strategy: 'isNotEmpty',
        errorMsg: '用户名不能为空'
    },
    {
        strategy: 'minLength:10',
        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;
    }
}

策略模式的优点:

  • 策略模式利用组合、委托和多态,可以避免多重条件选择
  • 可以完美的支持开放-关闭原则,将算法独立的封装在strategy中,是算法之间易于切换、易于理解、易于扩展。
  • 策略模式的算法也可以复用到系统的其他地方,从而避免许多的重复粘贴
  • 策略模式利用组合和委托Context拥有执行算法的能力,这也是继承的一种更加轻便的替代方案。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345

推荐阅读更多精彩内容