js設計模式--策略模式

凹瓶發表於2019-01-02

前言

本系列文章主要根據《JavaScript設計模式與開發實踐》整理而來,其中會加入了一些自己的思考。希望對大家有所幫助。

文章系列

js設計模式--單例模式

js設計模式--策略模式

js設計模式--代理模式

概念

策略模式的定義是:定義一系列的演算法,把它們一個個封裝起來,並且使它們可以相互替換

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

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

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

場景

從定義上看,策略模式就是用來封裝演算法的。但如果把策略模式僅僅用來封裝演算法,未免有一點大材小用。在實際開發中,我們通常會把演算法的含義擴散開來,使策略模式也可以用來封裝 一系列的“業務規則”。只要這些業務規則指向的目標一致,並且可以被替換使用,我們就可以 用策略模式來封裝它們。

優缺點

優點

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

缺點

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

但這些缺點並不嚴重

例子

計算獎金

粗糙的實現

	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>
複製程式碼

相關文章