js設計模式之策略模式

策略模式的定義是:定義一系列的算法(這些算法目標一致),把它們一個個封裝起來,而且使它們能夠相互替換。javascript

好比要實現從上海到廣州,既能夠坐火車,又能夠坐高鐵,還能夠坐飛機。這取決與我的想法。在這裏,不一樣的到達方式就是不一樣的策略,我的想法就是條件。html

1.計算獎金

以計算獎金爲例,績效爲S的年終獎是4倍工資,績效爲A的年終獎是3倍工資,績效爲B的年終獎是2倍工資。那麼這裏獎金取決於兩個條件,績效和薪水。最初編碼實現以下:java

const calculateBonus =  (performanceLevel, salary) => {
  if(performanceLevel === 'S') {
    return salary * 4;
  }

  if(performanceLevel === 'A') {
    return salary * 3;
  }

  if(performanceLevel === 'B') {
    return salary * 2;
  }
}
複製代碼

這段代碼十分簡單,但存在顯而易見的缺點。算法

  1. if語句過多,須要涵蓋全部的條件。
  2. 彈性差,若是新增績效C,那麼須要什麼calculateBonus的內部實現去修改代碼,不符合開放封閉原則。
  3. 複用性差,計算獎金的算法不能直接複用,除非複製粘貼。

2.使用策略模式

策略模式是指定義一系列的算法,並將它們封裝起來,這很符合開閉原則。策略模式的目的就是將算法的使用和算法的實現分離出來。bash

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

如今使用策略模式來重構以上代碼,第一個版本是基於class,第二個版本是基於函數。dom

2.1基於class

class PerformanceS {
  calculate(salary) {
    return salary * 4;
  }
}

class PerformanceA {
  calculate(salary) {
    return salary * 3;
  }
}

class PerformanceB {
  calculate(salary) {
    return salary * 2;
  }
}

class Bonus {
  constructor(strategy, salary) {
    this.strategy = strategy;
    this.salary = salary;
  }

  getBonus() {
    if(!this.strategy) {
      return -1;
    }
    return this.strategy.calculate(this.salary);
  }
}

const bonus = new Bonus(new PerformanceA(), 2000);
console.log(bonus.getBonus()) // 6000
複製代碼

它沒有上述的三個缺點。在這裏,有三個策略類,分別是PerformanceS、Performance A、PerformanceB。這裏的context就是Bonus類,它接受客戶的請求(bonus.getBonus),將請求委託給策略類。它保存着策略類的引用。函數

2.2基於函數

上述中,每個策略都是class,實際上,class也是一個函數。這裏,能夠直接用函數實現。post

const strategies = {
  'S': salary => salary * 4,
  'A': salary => salary * 3,
  'B': salary => salary * 2,
}

const getBonus = (performanceLevel, salary) => strategies[performanceLevel](salary)

console.log(getBonus('A', 2000)) // 6000
複製代碼

3.多態在策略模式中的體現

經過使用策略模式重構代碼,消除了程序中大片的條件語句。全部和獎金相關的邏輯都不在Context中,而是分佈在各個策略對象中。Context並無計算獎金的能力,當它接收到客戶的請求時,它將請求委託給某個策略對象計算,計算方法被封裝在策略對象內部。當咱們發起「得到獎金」的請求時,Context將請求轉發給策略類,策略類根據客戶參數返回不一樣的內容,這正是對象多態性的體現,這也體現了策略模式的定義--「它們能夠相互替換」。動畫

4.計算小球的緩動動畫

咱們的目標是編寫一個動畫類和緩動算法,讓小球以各類各樣的緩動效果在頁面中進行移動。

很明顯,緩動算法是一個策略對象,它有幾種不一樣的策略。這些策略函數都接受四個參數:動畫開始的位置s、動畫結束的位置e、動畫已消耗的時間t、動畫總時間d。

const tween = {
  linear: (s, e, t, d) => { return e*t/d + s },
  easeIn: () => { /* some code */ },
  easeOut: () => { /* some code */ },
  easeInOut: () => { /* some code */ },
}
複製代碼

頁面上有一個div元素。

<div id='div' style='position: absolute; left: 0;'></div>
複製代碼

如今要讓這個div動起來,須要編寫一個動畫類。

const tween = {
  linear: () => { /* some code */ },
  easeIn: () => { /* some code */ },
  easeOut: () => { /* some code */ },
  easeInOut: () => { /* some code */ },
}

class Animation {
  constructor(dom) {
    this.dom = dom;
    this.startTime = 0;
    this.startPos = 0;
    this.endPos = 0;
    this.propertyName = null;
    this.easing = null;
    this.duration = null;
  }

  // 開始動畫
  start(propertyName, endPos, duration, easing) {
    this.startTime = Date.now();
    // 初始化參數,省略其餘
    const self = this;
    // 循環執行動畫,若是動畫已結束,那麼清除定時器
    let timer = setInterval(() => {
      if(self.step() === false) {
        clearInterval(timer);
      }
    }, 1000/60);
  }

  // 計算下一次循環到的時候小球位置
  step() {
    const now = Date.now();
    if(now > this.startTime + this.duration) {
      return false;
    } else {
      // 得到小球在本次循環結束時的位置並更新位置
      // const pos = this.easing();
      // this.update(pos);
    }
  }

  update(pos) {
    this.dom.style[propertyName] = pos + 'px';
  }
}
複製代碼

具體實現略去。這裏的Animation類就是環境類Context,當接收到客戶的請求(更新小球位置 self.step()),它將請求轉發給策略內(this.easing()),策略類進行計算並返回結果。

5.更廣義的「算法」

策略模式指的是定義一系列的算法,而且把他們封裝起來。上述所說的計算獎金和緩動動畫的例子都封裝了一些策略方法。

從定義上看,策略模式就是用來封裝算法的。但若是僅僅將策略模式用來封裝算法,有些大材小用。在實際開發中,策略模式也能夠用來封裝一些的「業務規則」。只要這些業務規則目標一致,而且能夠替換,那麼就能夠用策略模式來封裝它們。以使用策略模式來完成表單校驗爲例。

6.表單校驗

<form action='xxx' id='form' method='post'>
  <input type='text' name='username'>
  <input type='password' name='passsword'>
  <button>提交</button>
</form>
複製代碼

驗證規則以下:

const form = document.querySelector('form')
form.onsubmit = () => {
  if(form.username.value === '') {
    alert('用戶名不能爲空')
    return false;
  }

  if(form.password.value.length < 6) {
    alert('密碼不能少於6位')
    return false;
  }
}
複製代碼

這是一種很常見的思路,和最開始計算獎金同樣。缺點也是同樣。

6.1使用策略模式重構表單校驗

第一步須要把這些校驗邏輯封裝成策略對象。

const strategies = {
  isNonEmpty: (value, errMsg) => {
    if(value === '') {
      return errMsg
    }
  },
  minLength: (value, errMsg) => {
    if(value.length < minLength) {
      return errMsg
    }
  }
}
複製代碼

第二步對錶單進行校驗。

class Validator {
  constructor() {
    this.rules = [];
  }
  
  add(dom, rule, errMsg) {
    const arr = rule.split(':');
    this.rules.push(() => {
      const strategy = arr.shift();
      arr.unshift(dom.value);
      arr.push(errMsg);
      return strategies[strategy].apply(dom, arr);
    })
  }

  start() {
    for(let i = 0, validatorFunc; validatorFunc = this.rules[i++];) {
      let msg = validatorFunc();
      if(msg) {
        return msg;
      }
    }
  }

}

const form = document.querySelector('form')
form.onsubmit = (e) => {
  e.preventDefault();
  const validator = new Validator();
  validator.add(form.username, 'isNonEmpty', '用戶名不能爲空');
  validator.add(form.password, 'minLength:6', '密碼長度不能小於6位');
  const errMsg = validator.start();
  if(errMsg) {
    alert(errMsg);
    return false;
  }
}
複製代碼

上述例子中,校驗邏輯是策略對象,其中包含策略的實現函數。Validator類是Context,用於將客戶的請求(表單驗證)轉發到策略對象進行驗證。與計算獎金的Bonus不一樣的是,這裏並無將驗證參數經過構造函數傳入,而是經過validator.add傳入相關驗證參數,經過validator.start()進行驗證。

策略模式優缺點

  1. 策略模式利用組合、委託和多態等技術和思想,能夠有效避免多重選擇語句。
  2. 策略模式經過擴展策略類,對開放封閉原則徹底支持,使得它們易於切換和擴展。
  3. 策略模式的算法能夠用在其餘地方,避免複製。
  4. 策略模式利用組合和委託讓Context擁有執行算法的能力,這也是繼承的一種更輕便的替代方案。

前三點正是開頭實現的計算獎金函數的缺點。 策略模式有一點缺點,不過並不嚴重。

  1. 會增長策略類或者策略對象,增長了複雜度。可是與Context解耦了,這樣更便於擴展。
  2. 使用策略模式,必須瞭解全部的策略以便選擇合適的策略,這是strategies要向客戶暴露它的全部實現,不符合最少知識原則。
相關文章
相關標籤/搜索