阅读 1577

Javascript策略模式理解以及应用

最近一直在看Javascript设计模式,想通过写文章来增加自己对策略模式的理解,同时记录自己学习的经历。希望大家看完之后觉得有收获可以帮忙点个赞表示支持。

策略模式的定义

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

每次遇到这种设计模式的定义,第一眼的感觉总是很懵逼,不知所云。其实有一个办法:素质三连问。我们可以把定义细化,然后分析对应每个字段的含义,组合起来,就能明白定义的真正想表达的意思。

所以针对策略模式的定义我们就可以来一波素质三连问:

  1. 这里的算法是指什么
  2. 为啥需要一个个封装起来
  3. 相互替换又是指啥

在回答素质三连问之前,我们可以从生活中入手来看策略模式的应用场景。其实很多场景我们都可以使用到不同到策略来解决问题。

旅游

  1. 如果你是土豪或者时间紧,可以选择搭飞机
  2. 如果你是小资生活,也不赶时间,可以选择搭高铁
  3. 如果你是穷游,那你可以选择骑自行车等等

压缩文件

  1. zip算法
  2. gzip算法

由此可以看出,其实我们的策略就是解决我们的问题的一种方法,这种方法我们可以定义为一种算法。

策略模式的应用

讲了这么多,其实大家最关心的还是策略模式的应用场景。接下来我们用年终奖的例子为大家一步一步解决我们的素质三连问。年终奖是根据员工的工资基数以及年底绩效情况来发放的。如:绩效为S的年终奖有4倍工资、绩效为A的年终奖有3倍工资,绩效为B的年终奖只能有2倍的工资。这种逻辑我们可以用基本代码实现

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

//获得绩效为B的员工
calculateBonus('B', 20000);
//获得绩效为S的员工
calculateBonus('S', 6000);
复制代码

显而易见,这段代码虽然实现了我们想要的功能,但是很局限,有以下几种缺点

  1. 包含了过多的if-else语句,使函数过于庞大
  2. calculateBonus函数缺乏弹性,如果需要新增加不同的绩效等级,需要更改其内部实现,违反了开放-封闭原则
  3. 算法复用性差,如果其他地方需要用到这种计算规则,只能重新输出(复制、粘贴)

优化一(组合函数)

我们可以通过组合函数来重构这段代码,把各种算法(即年终奖的计算规则)封装到一个个独立的小函数中,同时给个良好的命名,那么我们就可以解决上面的缺点3算法复用的问题。代码如下

var performanceS = function (salary) {
    return salary * 4;
};
var performanceA = function (salary) {
    return salary * 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 );

复制代码

通过组合函数,我们可以看出我们的算法被封装成一个个小函数,从而可以解决函数复用的问题。但是我们的核心问题,也就是上述的缺点1、缺点2并没有解决,以此我们继续进行改进,这次通过策略模式来优化。

优化二(策略模式)

首先我们应该了解将不变的部分和变化的部分隔开是每个设计模式的主题,而策略模式的目的就是将算法的使用和算法的实现分离开来。那么在此例子中,算法的使用方式是不变的,根据某个算法计算奖金数额,但是算法的实现是可变的,如绩效S、A的实现方式。

策略模式的组成:

  1. 策略类,策略类封装了具体的算法(绩效的计算方式),并负责具体的计算过程。
  2. 环境类(Context),Context接受客户的请求,随后把请求委托给某一个策略类。
  3. 桥梁,Context中要维持对某个策略对象的引用
//策略类(S)
var performanceS = function () {}
//算法S内部具体实现
performanceS.prototype.calculate = function ( salary ) {
    return salary * 4;
}
//策略类(A)
var performanceA = function () {}
//算法A内部具体实现
performanceA.prototype.calculate = function ( salary ) {
    return salary * 3;
}
//策略类(B)
var performanceB = function () {}
//算法B内部具体实现
performanceB.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.portotype.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() ); //把请求委托给了之前保存好的策略对象
复制代码

以上的例子展示了策略模式的应用,使代码变得更加清晰,各个类职责更加鲜明,也解决了以上普通函数调用的缺点一、缺点二。那么这种类的实现其实是基于传统的面向对象语言模仿的,因此我们可以进一步对这段代码进行优化,变成JavaScript版本的策略模式

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

为什么JavaScript版本的策略模式跟传统的面向对象语言的策略模式不同呢,实际上在JavaScript语言中,函数也是对象,所以可以直接把strategy类直接定义为函数。代码如下:

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

同样,我们也可以直接用calculateBonus函数充当Context来接受用户的请求,并不需要Bonus类来表示

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

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

从以上的例子我们其实已经回答了策略模式的定义中的素质三连问,策略模式的算法是指的什么(绩效的计算方法)、为什么要封装(可复用)、相互替换又是指啥(绩效可发生变化、但是不影响函数的调用,只需改变参数)

延伸扩展

上述一直在定义策略模式中算法的概念,实际开发中,我们通常可以把算法的含义扩散开来,使得策略模式也可以用来封装一系列的'业务规则'。只要这些业务规则指向的目标一致,并且可以被替换使用,我们就可以用策略模式来封装它们 那么在我们的'业务规则'中,表单的校验就符合我们使用策略模式。

表单验证

假设我们正在编写一个注册页面,在点击按钮之前,有如下几条校验规则:

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

根据这样的要求,在我们没有引入策略模式之前,我们可以通过如下代码编写

<html>
    <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>
复制代码

这样的代码同样有着跟上述年终奖一样的缺点,函数过于庞大、缺乏弹性以及复用性差,那么我们学了策略模式,肯定需要对这种情况进行优化

优化一

  1. 明确在此场景中,算法具体是什么,很明显可以看出,这里的算法指的就是我们表单验证逻辑的业务规则。因此我们可以把这些业务规则封装成相对应的策略对象:
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;
        }
    ]
}
复制代码

实现了策略对象的算法,那么我们还需要一个环境类来负责接受用户的请求并委托给strategy对象。但是在我们实现之前,我们需要明白环境类与策略对象直接的桥梁是怎么样的,也就是用户是如何向validator类发送请求的。这样可以方便我们实现环境类,也就是这里的Validator类。 如下是我们用户向validator类发送请求的代码:

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类代码

var validator = function () {
    this.cache = [];  //保存校验规则
};
//添加检验规则函数
validator.prototype.add = function (dom, rule, errorMsg) {
    //把strategy和参数分开'minLength:6''minLength:6' -> ["minLength", "6"]
    var ary = rule.split(':'); 
    this.cache.push ( function () {
        var strategy = ary.shift(); //用户挑选的strategy ["minLength", "6"] -> 'minLength' 
        ary.unshift( dom.value ); //把input的value添加进参数列表
        ary.push( errorMsg ); //把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 ) {  //如果msg存在,则说明校验不通过
            return msg; 
        }
    }
}
复制代码

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

优化二(多个校验规则)

其实到这里为止,我们的策略模式的理解以及应用的基本概念都已经通过上述的例子阐述完毕了,但是目前我们实现的表单校验有一点小瑕疵,就是我们一个文本输入框只有对应一种校验规则。那么如果我们想要添加多种检验规则,可以通过以下方式添加:

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

那我们可以修改我们的Validator中的add方法,通过遍历的方式,把我们的多个检验规则添加到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 )
    }
};
复制代码

策略模式的优缺点

从上述的例子中,很明显能总结出策略模式的优点

  1. 采用组合、委托和多态等技术和思想、有效避免了多重条件选择语句
  2. 采用了开放-封闭原则,将算法封装在独立的strategy中,易于理解、切换、拓展
  3. 策略模式中的算法可以进行复用,从而避免很多地方的复制粘贴

同时策略模式也有其缺点,但是并不影响我们对策略模式的使用

  1. 在策略模式中,我们会增加很多策略类、策略对象
  2. 要使用策略模式,我们必须了解到所有的strategy、必须了解各个strategy之间的不同点,才能选择一个适合的strategy。

结语

大家看完之后,如果觉得有啥不对的地方,请大家提出建议。也希望这篇文章如果对你有帮助,请大家多多点赞、转发支持!

文章借鉴于:曾探老师的《JavaScript设计模式与开发实践

关注下面的标签,发现更多相似文章
评论