結構型:策略模式

2023-03-27 12:01:27

定義

  定義一系列的演演算法,將他們一個個封裝起來,使他們直接可以相互替換。
 
  1. 演演算法:就是寫的邏輯可以是你任何一個功能函數的邏輯
  2. 封裝:就是把某一功能點對應的邏輯給抽出來
  3. 可替換:建立在封裝的基礎上,這些獨立的演演算法可以很方便的替換
通俗的理解就是,把你的演演算法(邏輯)封裝到不同的策略中,在不同的策略中是互相獨立的,這樣我們封裝的每一個演演算法是可以很方便的複用。

策略模式主要解決在有多種情況下,使用 if...else 所帶來的複雜和難以維護

它的優點是演演算法可以自由切換,同時可以避免多重if...else判斷,且具有良好的擴充套件性。

看一個真實場景--最簡單的策略模式

我們有一個根據不同的型別返回不同價格的一個方法

function getPrice (type) {
  if (type === 1) {
    // code 或許每個分支要處理很多邏輯
  }
  if (type === 2) {
    // code
  }
  if (type === 3) {
    // code
  }
  if (type === 4) {
    // code
  }
  if (type === 5) {
    // code
  }
  if (type === 6) {
    // code
  }
  if (type === 7) {
    // code
  }
}

程式碼上看確實沒有什麼問題,但是如果需要增加一種新的型別,我們就會一個if判斷,導致這個方法可能會過於龐大,後期不太好維護。

其次程式碼每次都是從上往下走,可能存在前面某個判斷出現問題(如某個 && 的判斷變數null 之類),導致後面的程式碼沒有走,所以這種影響還是挺大的。

用了這麼多 if-else,我們的目的是什麼?是不是就是為了把傳進來的引數的值-對應的處理常式,這個對映關係給明確下來?在 JS 中我們可以通過物件對映的形式來做,如下程式碼

/*
1、把 if else 的程式碼快優化為一個一個的對映
2、把if else 裡面的邏輯抽離成一個獨立的函數,這樣方便其他模組或者分支使用
*/
function getPrice (type) {
  const actionMap = {
    '1': action1,
    '2': action2,
    '3': action3,
    '4': action4,
    '5': action5,
    '6': action6,
    '7': action7,
  }
  const params = {}
 return  actionMap[type](params)
}

這種程式碼結構變得易讀、易維護。

這就是最簡單的策略模式

模擬表單校驗邏輯

如果不把邏輯封裝起來,那麼我們在判斷的時候會寫很多的if else,如寫一個很簡單的表單的校驗

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

<head>
  <meta charset="UTF-8">
  <meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport">
  <meta content="yes" name="apple-mobile-web-app-capable">
  <meta content="black" name="apple-mobile-web-app-status-bar-style">
  <meta content="telephone=no,email=no" name="format-detection">
  <meta name="App-Config" content="fullscreen=yes,useHistoryState=yes,transition=yes">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title></title>
  <link href="css/style.css" rel="stylesheet">
</head>

<body>
  <div>
    <form action="" id="form">
      姓名:<input type="text" id="username"><br>
      密碼:<input type="password" id="password1"><br>
      確認密碼:<input type="password" id="password2"><br>
      手機號:<input type="text" id="phone"><br>
      <input type="submit" value="提交">
    </form>
  </div>

  <script>
    function getValue (id) {
  return document.getElementById(id).value;
}
var formData = document.getElementById('form')
formData.onsubmit = function () {
  var name = getValue('username');
  var pwd1 = getValue('password1');
  var pwd2 = getValue('password2');
  var tel = getValue('phone');
  if (name.replace(/(^\s*)|(\s*$)/g, "") === "") {
    alert('使用者名稱不能為空')
    return false
  }
  if (pwd1.replace(/(^\s*)|(\s*$)/g, "")  === "") {
    alert('密碼不能為空')
    return false
  }
  if (pwd2.replace(/(^\s*)|(\s*$)/g, "")  === "") {
    alert('確認密碼不能為空')
    return false
  }
  if (pwd2 !== pwd1) {
    alert('確認密碼與原密碼不相同!')
    return false
  }
  if (tel.replace(/(^\s*)|(\s*$)/g, "") === "") {
    alert('手機號碼不能為空')
    return false
  }
  if (!/^1[3,4,5,7,8,9][0-9]\d{8}$/.test(tel)) {
    alert('手機號碼格式不正確')
    return false
  }
  alert('註冊成功')
}
  </script>
</body>

</html>

只是4個欄位,我們用了 6個if判斷來做相關的邏輯校驗。

仔細觀察發現很多校驗的邏輯是一致的,所以我們可以把他封裝起來,用策略模式修改成如下

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

<head>
  <meta charset="UTF-8">
  <meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport">
  <meta content="yes" name="apple-mobile-web-app-capable">
  <meta content="black" name="apple-mobile-web-app-status-bar-style">
  <meta content="telephone=no,email=no" name="format-detection">
  <meta name="App-Config" content="fullscreen=yes,useHistoryState=yes,transition=yes">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title></title>
  <link href="css/style.css" rel="stylesheet">
</head>

<body>
  <div>
    <form action="" id="form">
      姓名:<input type="text" id="username"><br>
      密碼:<input type="password" id="password1"><br>
      確認密碼:<input type="password" id="password2"><br>
      手機號:<input type="text" id="phone"><br>
      <input type="submit" value="提交">
    </form>
  </div>

  <script>
    let formData = document.getElementById('form')
    function getValue(id) {
      return document.getElementById(id).value;
    }
    function Validate() { }
    Validate.prototype.rules = {
      // 是否手機號
      isMobile: function (str) {
        let rule = /^1[3,4,5,7,8,9][0-9]\d{8}$/;
        return rule.test(str);
      },
      // 是否必填
      isRequired: function (str) {
        // 除去首尾空格
        let value = str.replace(/(^\s*)|(\s*$)/g, "");
        return value !== "";
      },
      // 最小長度
      minLength: function (str, length) {
        let strLength = str.length;
        return strLength >= length;
      },
      // 是否相等
      isEqual: function (...args) {
        let equal = args.every(function (value) {
          return value === args[0];
        })
        return equal;
      }
    }

    Validate.prototype.test = function (rules) {
      let _this = this;
      let valid; // 儲存校驗結果
      for (let key in rules) { // 遍歷校驗規則物件
        for (let i = 0; i < rules[key].length; i++) { // 遍歷每一個欄位的校驗規則
          let ruleName = rules[key][i].rule; // 獲取每一個校驗規則的規則名
          let value = rules[key][i].value; // 獲取每一個校驗規則的校驗值
          if (!Array.isArray(value)) { // 統一校驗值為陣列型別
            value = new Array(value)
          }
          let result = _this.rules[ruleName].apply(this, value); // 呼叫校驗規則方法進行校驗
          if (!result) { // 如果校驗不通過,就獲取校驗結果資訊,並立即跳出迴圈不再執行,節約消耗
            valid = {
              errValue: key,
              errMsg: rules[key][i].message
            }
            break;
          }
        }
        if (valid) { // 如果有了校驗結果,代表存在不通過的欄位,則立即停止迴圈,節約消耗
          break;
        }
      }
      return valid; // 把校驗結果反悔出去
    }

    formData.onsubmit = function () {
      event.preventDefault()
      let validator = new Validate();
      let result = validator.test({
        'username': [{ rule: 'isRequired', value: this.username.value, message: '使用者名稱不能為空!' }],
        'password1': [
          { rule: 'isRequired', value: this.password1.value, message: '密碼不能為空!' },
          { rule: 'minLength', value: [this.password1.value, 6], message: '密碼長度不能小於6個字元!' }
        ],
        'password2': [
          { rule: 'isRequired', value: this.password2.value, message: '確認密碼不能為空!' },
          { rule: 'minLength', value: [this.password2.value, 6], message: '確認密碼長度不能小於6個字元!' },
          { rule: 'isEqual', value: [this.password2.value, this.password1.value], message: '確認密碼與原密碼不相同!' }
        ],
        'isMobile': [
          { rule: 'isRequired', value: this.phone.value, message: '手機號不能為空!' },
          { rule: 'isMobile', value: this.phone.value, message: '手機號格式不正確!' }
        ]
      })

      if (result) {
        console.log(result);
      } else {
        console.log('校驗通過');
      }
    }
  </script>
</body>

</html>

下次我們增加其他的欄位也只是增加規則而已,而不會去修改判斷的業務邏輯。

小結

  1. 將一個個演演算法封裝起來,提高程式碼複用率,減少程式碼冗餘
  2. 策略模式可看作為if/else判斷的另一種表現形式,在達到相同目的的同時,極大的減少了程式碼量以及程式碼維護成本

  3. 策略模式有效避免多重條件選擇語句,將演演算法封裝在策略中