js設計模式--策略模式

前言

本系列文章主要根據《JavaScript設計模式與開發實踐》整理而來,其中會加入了一些本身的思考。但願對你們有所幫助。css

文章系列

js設計模式--單例模式html

js設計模式--策略模式es6

js設計模式--代理模式算法

概念

策略模式的定義是:定義一系列的算法,把它們一個個封裝起來,而且使它們能夠相互替換segmentfault

策略模式指的是定義一系列的算法,把它們一個個封裝起來。將不變的部分和變化的部分隔開是每一個設計模式的主題,策略模式也不例外,策略模式的目的就是將算法的使用與算法的實現分離開來設計模式

一個基於策略模式的程序至少由兩部分組成。第一個部分是一組策略類,策略類封裝了具體 的算法,並負責具體的計算過程。 第二個部分是環境類Context,Context 接受客戶的請求,隨後 把請求委託給某一個策略類。要作到這點,說明 Context中要維持對某個策略對象的引用。app

策略模式的實現並不複雜,關鍵是如何從策略模式的實現背後,找到封裝變化、委託和多態性這些思想的價值。dom

場景

從定義上看,策略模式就是用來封裝算法的。但若是把策略模式僅僅用來封裝算法,未免有一點大材小用。在實際開發中,咱們一般會把算法的含義擴散開來,使策略模式也能夠用來封裝 一系列的「業務規則」。只要這些業務規則指向的目標一致,而且能夠被替換使用,咱們就能夠 用策略模式來封裝它們。函數

優缺點

優勢

  • 策略模式利用組合、委託和多態等技術和思想,能夠有效地避免多重條件選擇語句。
  • 策略模式提供了對開放—封閉原則的完美支持,將算法封裝在獨立的strategy中,使得它們易於切換,易於理解,易於擴展。
  • 策略模式中的算法也能夠複用在系統的其餘地方,從而避免許多重複的複製粘貼工做。
  • 在策略模式中利用組合和委託來讓 Context 擁有執行算法的能力,這也是繼承的一種更輕便的替代方案。

缺點

  • 增長許多策略類或者策略對象,但實際上這比把它們負責的 邏輯堆砌在 Context 中要好。
  • 要使用策略模式,必須瞭解全部的 strategy,必須瞭解各個 strategy 之間的不一樣點, 這樣才能選擇一個合適的 strategy。

但這些缺點並不嚴重post

例子

計算獎金

粗糙的實現

var calculateBonus = function( performanceLevel, salary ){
        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

缺點:

  1. calculateBonus 函數比較龐大,包含了不少 if-else 語句
  2. calculateBonus 函數缺少彈性,若是增長了一種新的績效等級 C,或者想把績效 S 的獎金 係數改成 5,那咱們必須深刻 calculateBonus 函數的內部實現,這是違反開放封閉原則的。
  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 ); // 輸出:30000

問題依然存在:calculateBonus 函數有可能愈來愈龐大,並且在系統變化的時候缺少彈性

使用策略模式重構代碼

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

但這段代碼是基於傳統面嚮對象語言的模仿,下面咱們用JavaScript實現的策略模式。

JavaScript 版本的策略模式

在 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

es6類實現

var performanceS = function () {};
performanceS.prototype.calculate = function (salary) {
  return salary * 4;
};
var performanceA = function () {};
performanceA.prototype.calculate = function (salary) {
  return salary * 3;
};
var performanceB = function () {};
performanceB.prototype.calculate = function (salary) {
  return salary * 2;
};

//接下來定義獎金類Bonus:
class Bonus {
  constructor() {
    this.salary = null; // 原始工資
  this.strategy = null; // 績效等級對應的策略對象
  }
  setSalary(salary) {
    this.salary = salary; // 設置員工的原始工資
  }
  setStrategy(strategy) {
    this.strategy = strategy; // 設置員工績效等級對應的策略對象
  }
  getBonus() { // 取得獎金數額
    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

緩動動畫

目標:編寫一個動畫類和一些緩動算法,讓小球以各類各樣的緩動效果在頁面中運動

分析:

首先緩動算法的職責是實現小球如何運動

而後動畫類(即context)的職責是負責:

  1. 初始化動畫對象

    在運動開始以前,須要提早記錄一些有用的信息,至少包括如下信息:

    • 動畫開始時的準確時間點;
    • 動畫開始時,小球所在的原始位置;
    • 小球移動的目標位置;
    • 小球運動持續的時間。
  2. 計算小球某時刻的位置
  3. 更新小球的位置

實現:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>

<body>
  <div style="position:absolute;background:blue" id="div">我是div</div>

</body>
<script>
  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;
    }
  };

  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' );
</script>

</html>

驗證表單

簡單的實現

<html>

<body>
  <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>
  <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>

使用策略模式改進

<html>

<body>
  <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>
  <script>
    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; // 阻止表單提交
      }
    };

    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;
        }
      }
    };
  </script>
</body>

</html>

缺點:一 個文本輸入框只能對應一種校驗規則

再改進:能夠有多個校驗規則

<html>

<body>
  <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>
  <script>
    /***********************策略對象**************************/
    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 位'
      }]);
      var errorMsg = validator.start();
      return errorMsg;
    }
    registerForm.onsubmit = function () {
      var errorMsg = validataFunc();
      if (errorMsg) {
        alert(errorMsg);
        return false;
      }

    };
  </script>
</body>

</html>
相關文章
相關標籤/搜索