在JavaScript中理解策略模式

設計模式是: 在面向對象軟件過程中針對特定問題的簡潔而優雅的解決方案. 通過對封裝、繼承、多態、組合等技術的反覆利用, 提煉出可重複使用面向對象的設計技巧.

JavaScript 可以模擬實現傳統面嚮對象語言的設計模式. 事實上也的確如此, 好多的代碼 demo 都是沿着這個思路分析的. 看完後心裏不免有種一萬頭🦙在奔騰, 還順便飄來了六個字, 走(qu)你(de), 設計模式.

然而僅僅是生搬硬套, 未免會失去 JavaScript 的靈活性. 不如溯本求源, 看看這些設計模式到底在傳達什麼, 然後遵循此點.

策略模式定義

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

字面意思, 就是定義封裝多種算法, 且各個算法相互獨立. 當然, 也不僅僅是算法. 只要定義一些規則, 經處理後輸出我們想要的結果就成. 在此我們稱單個封裝後的算法爲一個策略. 一系列封裝後的算法稱爲一組策略.

一個基於策略模式的程序至少由兩部分組成. 第一部分是一組策略類, 策略類封裝了具體的算法, 並負責具體的計算過程. 第二部分是環境類 Context, Context 接受客戶的請求, 隨後把請求委託給某一個策略類.

這是面向傳統面嚮對象語言中的說法. 在面向對象思想中, 通過對組合, 多態等技術的使用來實現一個策略模式. 在 JavaScript 中, 對於一個簡單的需求來說, 這麼做就有點大材小用了.

所以, 上面的那句話, 我們換種說法就是, 策略模式需要至少兩部分, 一部分是保存着一組策略. 另一部分則是如何分配這些策略, 即如何把請求委託給某個/些策略. 其實這也是策略模式的目的, 將算法的使用與算法的實現分離.

評級

快到年底了, 公司打算制定一個標準用來給員工評級發福利.

考覈項目等級
A 100>a>=90 90>a>=80 80>a>=70
B 100>b>=90 90>b>=80 80>b>=70

以A、B考覈項目來評定甲乙丙等級.
現有考覈人員:

考覈項目考覈人 person_1 person_2 person_3
A 80 93 92
B 85 70 90
const persons = [
  { A: 80, B: 85 },
  { A: 93, B: 70 },
  { A: 92, B: 90 }
]

在策略模式中一部分, 我們提到的分配策略. 要想分配策略, 首先就要知道所有的策略, 只有這樣我們才能針對性的委託給某個/些策略. 這, 也是策略模式的一個缺點.

常規操作

甲乙丙等級對 A、B 的分值要求是不一樣的. 所以我們可以這麼做:

function rating(person) {
  let a = person.A;
  let b = person.B;
  if (a >= 90 && b >= 90) {
    return '甲';
  } else if (a >= 80 && b >= 80) {
    return '乙';
  } else if (a >= 70 && b >= 70) {
    return '丙'
  } else {
    console.log('憑啥級, 還不趕緊卷鋪走人');
  }
}
persons.forEach(person => {
  person.rate = rating(person);
})
// > persons
// [ { A: 80, B: 85, rate: '乙' },
// { A: 93, B: 70, rate: '丙' },
// { A: 92, B: 90, rate: '甲' } ]

策略模式下的評級

如果換成策略模式, 第一部分就是保存一組策略. 現在我們以甲乙丙三種定級標準來制定三種策略, 用對象來存貯策略. 考慮到以後可能有 D、E、F 等考覈項目的存在, 我們稍微改一下:

const strategies = {
  '甲': (person, items) => {
    const boolean = items.every(item => {
      return person[item] >= 90;
    });
    if (boolean) return '甲';
  },
  '乙': (person, items) => {
    const boolean = items.every(item => {
      return person[item] >= 80;
    });
    if (boolean) return '乙';
  },
  '丙': (person, items) => {
    const boolean = items.every(item => {
      return person[item] >= 70;
    });
    if (boolean) return '丙';
  }
}

策略就制定好了. 對象的鍵對應着策略的名稱, 對象的值對應着策略的實現.然而, 我們發現, 任何一個策略都不能單獨完成等級的評定.

可是, 我們有說一組策略只能選擇其中一個麼? 爲了達成某個目的, 策略組封裝了一組相互獨立平等替換的策略. 一個策略不行, 那就組合唄. 這也是策略模式另一部分存在的意義, 即如何分配策略.

function rating(person, items) {
  return strategies['甲'](person, items)
    || strategies['乙'](person, items)
    || strategies['丙'](person, items)
}
persons.forEach(person => {
  person.rate = rating(person, ['A', 'B'])
})
// > persons
// [ { A: 80, B: 85, rate: '乙' },
// { A: 93, B: 70, rate: '丙' },
// { A: 92, B: 90, rate: '甲' } ]

邏輯的轉移

所有的設計模式都遵循一條原則. 即 “找出程序中變化的地方, 並將變化封裝起來”.

將不變的隔離開來, 變化的封裝起來. 策略模式中, 策略組對應着程序中不變的地方. 將策略組制定好存貯起來, 然後想着如何去分配使用策略.

當然, 如何制定策略和如何分配策略之間的關係十分緊密, 可以說兩者相互影響.

再次看看制定的策略, “找出程序中變化的地方, 並將變化封裝起來”, 我們可以再次改造一下.

const strategies = {
  '甲': 90,
  '乙': 80,
  '丙': 70,
}
function rating(person, items) {
  const level = value => {
    return (person, items) => {
      const boolean = items.every(item => {
        return person[item] >= strategies[value];
      });
      if (boolean) return value;
    }
  }
  return level('甲')(person, items)
    || level('乙')(person, items)
    || level('丙')(person, items)
}

persons.forEach(person => {
  person.rate = rating(person, ['A', 'B'])
})
// > persons
// [ { A: 80, B: 85, rate: '乙' },
// { A: 93, B: 70, rate: '丙' },
// { A: 92, B: 90, rate: '甲' } ]

在上面的這種做法中, 我們把制定策略的邏輯挪到了分配策略裏了. 所以說, 如何制定策略和如何分配策略, 依情況而定.

不過回頭在看一看這段代碼, 是不是和平時用對象映射的做法很相似.

當然, 策略模式的用法還有很多, 最常見的是規則校驗.

小結

總結一下:

  1. 策略模式至少包括兩部分, 制定策略和分配策略.
  2. 策略模式的目的在於, 將策略制定和策略分配隔離開來.
  3. 策略制定和策略分配關係密切, 相互影響.
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章