【JavaScript设计模式】策略模式

2021-01-04 14:27发布

写在前面

这个系列的文章是通过对《JavaScript设计模式》一书的学习后总结而来,刚开始觉得学习的时候只需看书即可,不用再另外记录笔记了,但是后面发现书中有些内容理解起来并不是很容易,所以结合书中的描述就将自己的理解也梳理了一下并将它记录下来,希望和大家一起学习,文章中如果有我理解错的内容,请各位批评指正,大家共同进步~

这篇文章我们开始学习JavaScript的设计模式——策略模式。

 

策略模式含义

很多资料文档里面对于策略模式的定义是这样的:定义一系列算法,把它们一个个地封装起来,使得它们可以互相替换。等看完这句话的时候我觉得你应该像我一样处于比较"比较懵"的状态,下面我们来好好理解下这句话。其实这句定义里面有几个词汇比较显眼:算法、封装、替换,所以策略模式的含义可以这么理解:策略模式其实说白了就是将程序中变化的部分和不变的部分隔离开来,换句话说就是将算法的实现和算法的使用分离,然后可以实现这些算法的互相替换和扩展。

文章刚开始大家可能对策略模式的理解有些晦涩,但我们接下来通过下面的各个部分来逐步的介绍策略模式。

 

策略模式应用场景

策略模式的核心就是封装一系列的算法,使它们可以相互替换,但我们在实际业务开发中使用策略模式时就不要仅仅局限于算法哦,在实际开发过程中我们可以通过策略模式封装一系列的业务规则,只要这些业务规则的目标一致,完全是可以使用策略模式去做的,比如我们接下来要介绍的表单验证的使用场景。除了表单验证等这种使用场景之外,我们还可以用策略模式实现公司里面员工的工资计算、缓动动画等,下面我们来分别介绍一下。

一、使用策略模式实现员工薪资、奖金计算

在一个公司中如果有A、B、C三个员工等级,不同等级有不同的奖金计算方式,那我们要实现各个等级的员工奖金计算的话可能是下面这种方式:

        //奖金计算 最初的代码
        var calculate = function(level, salary) {
 
            if(level == 'A') {
                return salary * 500;
            }
 
            if(level == 'B') {
                return salary * 600;
            }
 
            if(level == 'C') {
                return salary * 700;
            }
        };
 
        console.log(calculate('B', 2000));  // 计算等级为B的员工奖金
        console.log(calculate('C', 1200));  // 计算等级为C的员工奖金

上述这段代码实现了公司里面不同等级的员工奖金计算,但是这段代码有以下几个问题:

  • calculate方法内部if语句繁多,这些语句覆盖了所有的逻辑分支;

  • calculate方法缺乏弹性,如果后期我们要增加等级D、E、F……这些的时候,是要回到calculate方法内部来改写代码,这违反开放—封闭原则;

  • 算法的复用性差,在其他地方使用这些算法的时候,我们只有选择复制粘贴。

基于以上这些问题,我们来对上述的实现进行优化,优化的时候采用策略模式进行,代码如下:

        //采用策略模式来优化
        var salaryObject = {
            "A": function(salary) {
                return salary * 500;
            },
            "B": function(salary) {
                return salary * 600;
            },
            "C": function(salary) {
                return salary * 700;
            }
        };
 
        var calculate = function(level, salary) {
            return salaryObject[level](salary);
        };
 
        console.log(calculate('B', 2000));  // 计算等级为B的员工奖金
        console.log(calculate('C', 1200));  // 计算等级为C的员工奖金

上述代码中首先是通过对象字面量的方式定义了一个奖金计算的salaryObject对象,里面封装了不同等级的奖金计算方式,紧跟着还定义了一个calculate方法来接受用户请求,但跟原始实现不同的是,这里的calculate方法仅仅接受用户请求而已,关于不同等级的奖金计算已经移交给了salaryObject对象,这样一来我们做到了算法实现和使用的分离,将不同的奖金计算方式进行了封装,使它们可以相互替换,增强了这段代码的扩展性,即使后期有更多的等级增加,只需要往salaryObject对象中增加一个计算方法即可,无需改动calculate方法了。

二、使用策略模式实现缓动动画

缓动动画其实就是通过使用策略模式简单的实现了一个div的动画,通过传入不同的缓动算法来实现不同的div动画效果,这些缓动算法都是可以被轻易替换为另一个缓动算法的,代码如下:

  var Animate = function (dom) {
    this.dom = dom; // 进行运动的dom 节点
    this.startTime = 0; // 动画开始时间
    this.startPos = 0; // 动画开始时,dom 节点的位置,即dom 的初始位置
    this.endPos = 0; // 动画结束时,dom 节点的位置,即dom 的目标位置
    this.propertyName = null; // dom 节点需要被改变的css 属性名
    this.easing = null; // 缓动算法
    this.duration = null; // 动画持续时间
  };
 
  Animate.prototype.start = function (propertyName, endPos, duration, easing) {
    this.startTime = +new Date; // 动画启动时间
    this.startPos = this.dom.getBoundingClientRect()[propertyName]; // dom 节点初始位置
    this.propertyName = propertyName; // dom 节点需要被改变的CSS 属性名
    this.endPos = endPos; // dom 节点目标位置
    this.duration = duration; // 动画持续事件
    this.easing = tween[easing]; // 缓动算法
    var self = this;
    var timeId = setInterval(function () { // 启动定时器,开始执行动画
      if (self.step() === false) { // 如果动画已结束,则清除定时器
        clearInterval(timeId);
      }
    }, 16);
  };
 
  Animate.prototype.step = function () {
    var t = +new Date; // 取得当前时间
    if (t >= this.startTime + this.duration) { // (1)
      this.update(this.endPos); // 更新小球的CSS 属性值
      return false;
    }
    var pos = this.easing(t - this.startTime, this.startPos, this.endPos - this.startPos, this.duration);
    // pos 为小球当前位置
    this.update(pos); // 更新小球的CSS 属性值
  };
 
  Animate.prototype.update = function (pos) {
    this.dom.style[this.propertyName] = pos + 'px';
  };
 
  var div = document.getElementById('div');
  var animate = new Animate(div);
  animate.start('left', 500, 1000, 'linear');
  animate.start( 'top', 1500, 500, 'strongEaseIn' );

下面是一些常见的缓动算法:

  var tween = {
    linear: function (t, b, c, d) {
      return c * t / d + b;
    },
    easeIn: function (t, b, c, d) {
      return c * (t /= d) * t + b;
    },
    strongEaseIn: function (t, b, c, d) {
      return c * (t /= d) * t * t * t * t + b;
    },
    strongEaseOut: function (t, b, c, d) {
      return c * ((t = t / d - 1) * t * t * t * t + 1) + b;
    },
    sineaseIn: function (t, b, c, d) {
      return c * (t /= d) * t * t + b;
    },
    sineaseOut: function (t, b, c, d) {
      return c * ((t = t / d - 1) * t * t + 1) + b;
    }
  };

上面又是一个策略模式的经典应用,但是目前开发过程中这类需求较少,就不再为大家展开介绍,这里面最主要的是理解策略模式的实现原理,包括JS里面的封装、委托、多态是如何在策略模式中应用的,这些才是关键。

三、使用策略模式实现表单验证

表单验证在Web开发时是一个常用的需求,我们一般使用表单验证的时候,通常采用if语句来做判断,如果某个表单控件中的值不满足判断条件,就返回错误提示消息。但是使用策略模式实现表单验证的话,会有不同的体验哦。

因为传统if语句形式的表单验证还是存在和计算奖金实现过程中一样的问题,就是表单验证函数异常庞大,违反开放—封闭原则等。我们先来看一下传统的实现方式:

下面表单验证的规则如下:

  • 用户名不为空

  • 密码长度不能少于六位

  • 手机号码必须符合样式

<form action="http://www.xbeichen.cn" 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][0-9]{9}$)/.test(registerForm.phoneNumber.value)) {
        alert('手机号码格式不正确');
        return false;
      }
}

以上就是传统方式的表单验证,可以看到验证算法通过if语句来实现,直接包含在表单提交方法里,如果后期增加规则的话,还要来改动表单提交方法中的表单验证这一段代码,扩展性极差。下面我们通过策略模式来优化一下:

		//将验证规则封装成策略对象
	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;
        }
      }
    };
 
    var validataFunc = function () {
      var validator = new Validator(); // 创建一个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(); // 如果errorMsg 有确切的返回值,说明未通过校验
      if (errorMsg) {
        alert(errorMsg);
        return false; // 阻止表单提交
      }
    };
 
	//Validator仅仅负责用户的请求,并将它委托给strategies策略对象
    var Validator = function () {
      this.cache = []; // 保存校验规则
    };
 
    Validator.prototype.add = function (dom, rule, errorMsg) {
      var ary = rule.split(':'); // 把strategy 和参数分开
      this.cache.push(function () { // 把校验的步骤用空函数包装起来,并且放入cache
        var strategy = ary.shift(); // 用户挑选的strategy
        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) { // 如果有确切的返回值,说明校验没有通过
          return msg;
        }
      }
    };

在上述优化后的代码里面,我们是将验证规则封装到了strategies策略对象中,各个验证规则的实现都是在这个策略对象中实现的;然后定义了一个Validator类,这个类是用来接收用户的请求的,然后将其委托给strategies策略对象去执行验证;那用户请求是如何发送的呢?这就是在validataFunc方法中实现的,当我们点击提交按钮时会调用validataFunc方法,在validataFunc方法中我们首先实例化了一个Validatshor类的对象,然后该对象的add方法向里面添加了一个验证规则,这里就是将用户请求发送给了Validator类,验证完成后调用start方法获取验证结果,如果有一项不符合,返回一个errorMsg的错误提示字符串,则表明此次验证失败,会阻止表单的提交事件。

以上三种就是策略模式的典型应用场景,其中第一个应用场景最为简单,理解策略模式的应用是最为合适的;第二种和第三种也是策略模式的典型应用,其实不管哪种场景,我们只要理解了算法实现和使用分离,不同封装的算法之间可以互相替换这个核心思想之后,策略模式的应用不仅仅会局限在这三种场景中。

 

策略模式实现思想

策略模式通过Context上下文对具体策略进行封装(在奖金计算实例中的Context上下文是calculate方法充当、表单验证实例中则是由Validator类来充当),供高层直接调用而不用关心策略的具体实现。然后Context本身通过不同情况实例化不同的抽象实现类(具体策略类),来执行具体策略。从而实现了具体策略的自由切换,易于新策略的扩展。

所以说一个基于策略模式的程序至少由两部分组成:第一部分是一组策略类,策略类封装了具体的算法,并负责具体的计算过程;第二部分是环境类Context,也叫做Context上下文,Context接受客户的请求,随后把请求委托给某一个策略类,要做到这点,说明Context中要维持对某个策略对象的引用。

其实策略模式的实现并不复杂,关键是如何从策略模式的实现背后,找到封装变化、委托和多态性这些思想的价值。

 

策略模式具体实现

我们还是拿奖金计算的例子来说,先看看原始代码:

        //奖金计算 最初的代码
        var calculate = function(level, salary) {
 
            if(level == 'A') {
                return salary * 500;
            }
 
            if(level == 'B') {
                return salary * 600;
            }
 
            if(level == 'C') {
                return salary * 700;
            }
        };
 
        console.log(calculate('B', 2000));  // 计算等级为B的员工奖金
        console.log(calculate('C', 1200));  // 计算等级为C的员工奖金

上述代码存在的问题我们已经说过了,所以接下来我们要进行优化这段代码,优化的时候采用策略模式来优化,代码如下:

        //采用策略模式优化 传统静态语言版
        //定义策略类
        var performenceA = function() {};
        performenceA.prototype.calculate = function(salary) {
            return salary * 500;
        };
 
        var performenceB = function() {};
        performenceB.prototype.calculate = function(salary) {
            return salary * 600;
        };
 
        var performenceC = function() {};
        performenceC.prototype.calculate = function(salary) {
            return salary * 700;
        };
 
        //定义奖金类
        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 bonusObject = new Bonus();
 
        bonusObject.setSalary(2000);
        bonusObject.setStrategy(new performenceB());
        console.log(bonusObject.getBonus());
 
        bonusObject.setSalary(1200);
        bonusObject.setStrategy(new performenceC());
        console.log(bonusObject.getBonus());

上述代码我们是通过仿照传统静态语言的形式优化了代码,结合这段代码再来理解策略模式的话其实就很好理解了:策略模式就是定义一系列的算法,把它们各自封装成策略类,算法被封装在策略类内部的方法里。在客户对Context发起请求的时候,Context总是把请求委托给这些策略对象中间的某一个进行计算。接下来我们再来看看JS版本的策略模式:

        //采用策略模式来优化 JS版
        var salaryObject = {
            "A": function(salary) {
                return salary * 500;
            },
            "B": function(salary) {
                return salary * 600;
            },
            "C": function(salary) {
                return salary * 700;
            }
        };
 
        var calculate = function(level, salary) {
            return salaryObject[level](salary);
        };
 
        console.log(calculate('B', 2000));  // 计算等级为B的员工奖金
        console.log(calculate('C', 1200));  // 计算等级为C的员工奖金

因为在JavaScript中函数也是对象,所以向上述代码一样,我们定义了一个salaryObject的对象,里面的策略类直接通过函数的形式进行了封装;同时Context也没有必要再额外定义一个Bonus类了,直接用calculate方法来接收用户请求,将其委托给策略对象去执行即可。