• JavaScript策略模式


    1 什么是策略模式

    策略模式(Strategy Pattern)是一种行为型设计模式,它定义了一系列算法,将每个算法都封装起来,并且使它们可以相互替换。

    策略模式让算法独立于使用它的客户而独立变化。

    2 实现一个基础的策略模式

    下面以计算奖金的例子来介绍策略模式。比如说,绩效为S的人年终奖有4倍工资,绩效为A的人年终奖有3倍工资,绩效为B的人年终奖为2倍工资。

    根据上述条件,我们有如下的实现,编写一个名为calcBouns的函数来计算年终奖,很显然该函数需要接受两个参数:这个人的绩效等级以及他的工资数。

    var calcBonus = function (level, salary) {
      if (level === "S") {
        return salary * 4;
      }
      if (level === "A") {
        return salary * 3;
      }
      if (level === "B") {
        return salary * 2;
      }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    这段代码虽然简单,但是存在着一些缺点:
    1、calcBonus函数包含了很多if-else语句,这些语句需要覆盖所有的逻辑分支
    2、缺乏弹性,如果增加了一种新的绩效等级C,或者想把绩效S的奖金系数改为5,那我们必须深入函数的内部实现
    3、算法的复用性差


    了解了以上代码的缺点之后,我们需要对这些代码进行重构。首先使用组合函数重构代码,我们将各种条件封装到每个函数中,使其可以被很好的复用,示例如下:

    var levelS = function (salary) {
      return salary * 4;
    };
    
    var levelA = function (salary) {
      return salary * 3;
    };
    
    var levelB = function (salary) {
      return salary * 2;
    };
    
    var calcBonus = function (level, salary) {
      if (level === "S") {
        return levelS(salary);
      }
      if (level === "A") {
        return levelA(salary);
      }
      if (level === "B") {
        return levelB(salary);
      }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    上面的代码将复用性得到了改善,但是calcBonus函数有可能会变得越来越庞大的问题没有解决,这时有更好的办法,就是使用策略模式来重构代码。


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

    1. 一组策略类,策略类封装了具体的算法,并负责具体的计算过程
    2. 环境类ContextContext接受客户的请求,随后把请求委托给某一个策略类。要做到这点,说明 Context中要维持对某个策略对象的引用

    首先把每种绩效的计算规则都封装在对应的策略类里面:

    var levelS = function () {};
    levelS.prototype.calculate = function (salary) {
      return salary * 4;
    };
    
    var levelA = function () {};
    levelA.prototype.calculate = function (salary) {
      return salary * 3;
    };
    
    var levelB = function () {};
    levelB.prototype.calculate = function (salary) {
      return salary * 2;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    然后定义一个奖金类Bouns

    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); // 把计算奖金的操作委托给对应的策略对象
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    现在我们来完成这个例子中剩下的代码。先创建一个bonus对象,并且给bonus对象设置员工的原始工资数额,接下来把某个计算奖金的策略对象也传入bonus对象内部保存起来。当调bonus.getBonus()来计算奖金的时候,bonus对象本身并没有能力进行计算,而是把请求委托给了之前保存好的策略对象:

    var bonus = new Bonus(); // 创建Bonus实例对象
    bonus.setSalary(1000); // 设置员工的初始工资
    
    bonus.setStrategy(new levelS()); // 设置策略对象
    console.log(bonus.getBonus()); // 输出:4000
    
    bonus.setStrategy(new levelA()); // 设置策略对象
    console.log(bonus.getBonus()); // 输出:3000
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    可以看到通过策略模式重构之后,代码变得更加清晰,各个类的职责更加鲜明。

    3 Javascript中策略模式

    JavaScript语言中,函数也是对象,所以更简单和直接的做法是把计算规则直接定义为对象:

    var level= {
      S: function (salary) {
        return salary * 4;
      },
      A: function (salary) {
        return salary * 3;
      },
      B: function (salary) {
        return salary * 2;
      },
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    依然用calcBonus函数充当环境类Context来接受用户的请求,代码结构更加简洁:

    var calceBonus = function (level, salary) {
      return level[level](salary);
    };
    
    • 1
    • 2
    • 3

    4 使用策略模式实现缓动动画

    JavaScript中实现动画效果,可以通过连续改变元素的某个CSS属性,比如lefttopbackground-position来实现动画效果。

    例如,编写一个动画类和缓动算法,让正方形在页面中运动起来。下面是一些常见的缓动算法,这些算法都接受4个参数,这4个参数的含义分别是:动画已消耗的时间、原始位置、目标位置、动画持续的总时间,返回的值是动画元素应该处在的当前位置。

    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;
      },
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    现在来分析实现这个程序的思路。在运动开始之前,我们了解以下信息:

    • 动画开始时,正方形所在的原始位置
    • 正方形移动的目标位置
    • 动画开始时的准确时间点
    • 正方形运动持续的时间

    随后,我们会用setInterval创建一个定时器,定时器每隔19ms循环一次。在定时器的每一帧里,我们会把动画已消耗的时间、正方形原始位置、正方形目标位置和动画持续的总时间等信息传入缓动算法。该算法会通过这几个参数,计算出正方形当前应该所在的位置。最后再更新该div对应的CSS属性,正方形就能够顺利地运动起来了。

    现在我们开始编写完整的代码,首先在页面中放置一个div

    <div class="div1">橘猫吃不胖div>
    
    • 1
    .div1 {
      position: absolute;
      width: 100px;
      height: 100px;
      background-color: orange;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    接下来定义Animate类,Animate的构造函数接受一个参数:即将运动起来的dom节点。Animate类的代码如下:

    var Animate = function (dom) {
      this.dom = dom; // 进行运动的dom节点
      this.startTime = 0; // 动画开始时间
      this.startPos = 0; // 动画开始时,dom的初始位置
      this.endPos = 0; // 动画结束时,dom的目标位置
      this.propertyName = null; // dom节点需要被改变的css属性名
      this.easing = null; // 缓动算法
      this.duration = null; // 动画持续时间
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    接下来定义Animate.prototype.start方法,它负责启动这个动画,在动画被启动的瞬间,要记录一些信息,供缓动算法在以后计算小球当前位置的时候使用。在记录完这些信息之后,此方法还要负责启动定时器。代码如下:

    // propertyName:要改变的CSS属性名,比如'left'、'top',分别表示左右移动和上下移动
    // endPos:正方形运动的目标位置
    // duration:动画持续时间。
    // easing:缓动算法。
    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);
        }
      }, 19);
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    然后是定义Animate.prototype.step方法,该方法代表正方形运动的每一帧要做的事情。在此处,这个方法负责计算小球的当前位置,和调用更新CSS属性值的方法Animate.prototype.update

    // 定义正方形每一帧要做的事情
    Animate.prototype.step = function () {
      var time = +new Date(); // 取得当前时间
      // 如果当前时间>动画开始时间+动画持续时间,说明动画已经结束了
      // 此时修正正方形的位置就可以了
      if (time >= this.startTime + this.duration) {
        this.update(this.endPos); // 更新正方形的CSS属性值
        return false; // 清除定时器标识
      }
      // 如果动画没有结束,根据缓动动画计算正方形的位置
      var pos = this.easing(
        time - this.startTime,
        this.startPos,
        this.endPos - this.startPos,
        this.duration
      );
      // pos为正方形当前位置
      this.update(pos); // 更新正方形的CSS属性值
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    更新CSS属性的方法:

    // 更新正方形CSS属性
    Animate.prototype.update = function (pos) {
      this.dom.style[this.propertyName] = pos + "px";
    };
    
    • 1
    • 2
    • 3
    • 4

    到这里缓动动画的实现就结束了,我们在页面中实验一下:

    var div = document.querySelector(".div1");
    var animate = new Animate(div);
    // 2s内向右移动500px
    animate.start("left", 500, 2000, "strongEaseOut");
    
    • 1
    • 2
    • 3
    • 4

    在这里插入图片描述

    5 使用策略模式实现表单校验

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

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

    首先是一版没有引入策略模式的代码:

    <form id="registerForm">
      请输入用户名: 请输入密码: 请输入手机号码:
      <button>提交button>
    form>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    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;
      }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    这段代码的缺点跟计算奖金的最初版本一模一样,接下来我们使用策略模式来重构代码。首先我们要把这些校验逻辑都封装成策略对象:

    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;
        }
      },
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    接下来我们实现环境类Context,也就是Validator类,它负责接收用户的请求并委托给strategy对象。

    var Validator = function () {
      this.cache = []; // 保存校验规则
    };
    
    // 通过add添加校验方法
    // dom;参与校验的表单dom,即表单字段
    // rule:当前表单字段的校验规则
    // errorMsg:校验未通过时返回的错误信息
    Validator.prototype.add = function (dom, rule, errorMsg) {
      // rule接收xxxx:xx的形式的字符串
      var ary = rule.split(":"); // 把strategy和参数分开
      this.cache.push(function () {
        // 把校验的步骤用空函数包装起来,并且放入cache
        var strategy = ary.shift(); // 用户挑选的strategy
        ary.unshift(dom.value); // 把表单项的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;
        }
      }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    到这里就使用策略模式改写完成了,接下来使用一下,给提交按钮绑定submit事件,其中包含校验表单的逻辑:

    // 提交表单时校验每个字段
    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; // 阻止表单提交
      }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    在这里插入图片描述

  • 相关阅读:
    hive中的join操作及其数据倾斜
    存在主义和阳明心学
    深入剖析Linux线程特定数据
    怎么写出美观,可读性高的代码?
    (五)Alian 的 Spring Cloud DB Starter(自己写个starter)
    hadoop 3.x大数据集群搭建系列4-安装Spark
    jmeter压力测试报告
    go-zero&go web集成gorm实战
    【学习笔记】开源计算机视觉库OPENCV学习方案
    就知道你爱学习,来吧,今天的干货,对与循环的使用
  • 原文地址:https://blog.csdn.net/m0_46612221/article/details/132956296