設計模式專欄 - 行爲型設計模式之策略模式(Strategy Pattern)(表單校驗案例, 策略模式的優缺點)

設計模式專欄 - 行爲型設計模式之策略模式(Strategy Pattern)

目錄:

  • 初識策略模式

  • 策略模式案例

  • 策略模式優缺點

初識策略模式

官方定義: 定義一系列的算法並將它們一個一個的封裝起來, 並使得他們可以相互替換

個人定義:當多行代碼只區別於行爲不一樣而目的一樣的時候(什麼叫行爲不一樣目的一樣, 我要喝水, 我可以拿杯子喝, 也可以拿水壺喝, 這就叫行爲不一樣但是目的一樣), 我們應該將這些行爲統一放在一個地方, 當遇到需要使用不同行爲的達到同一目的的需求時, 根據不同需求來從這塊地方選擇不同的行爲配合使用

我們來看一個最基本也是最常見的例子:

我們要實現一個選擇旗艦手機的例子, 用戶選擇品牌(如Apple, HUAWEI, SAMSUNG, XiaoMi, OPPO), 選擇完品牌以後我們必須給他們推送目前該品牌的旗艦機器,【比如用戶選了Apple, 我們要返回給他11ProMax, 用戶選擇了三星, 我們要返回給他SAMSUNG S20 Ultra】 我相信一般的朋友可能會寫出如下的代碼

function chooseBrand(userChoose) {
    if(userChoose === 'Apple') return 'iPhone 11 Pro Max';
    else if(userChoose === 'SAMSUNG') return 'SAMSUNG S20 Ultra';
    else if(userChoose === 'HUAWEI') return 'HUAWEI Mate 30 Pro';
    else if(userChoose === 'XiaoMi') return 'XiaoMi MI10 Pro';
    else if(userChoose === 'OPPO') return 'OPPO FindX2 Pro';
    else return "Sorry, we can't find the brand that u input";
}

chooseBrand('Apple'); // 返回11promax

沒錯, 上方的代碼確實可以在目前解決我們的基本需求, 但是上方代碼會有一些問題

  1. 如果某天我們需要加入vivo手機, 或者加入OnePlus手機, 我們一定要把這個函數打開, 然後進行再一次elseif的書寫, 這樣違反了開閉原則, 而違反開閉原則帶來的危害可以查看筆者關於開閉原則的博客, 違反開閉原則導致代碼會變得越來越不穩定, 而開發者隨着代碼量的增加會變得越來越難於修改

  2. 多重的if elseif個不停會顯得代碼非常的low, 給人觀感不佳, 估計如果書寫了這種代碼 公司還留得住你的話, 那麼你們公司可能確實不咋地

既然問題都出來了, 我們勢必是需要解決的, 而策略模式可以說是這類問題的剋星, 這裏如果你基礎好的話, 你會馬上看懂這塊的代碼, 如果你覺得看起來比較吃力也沒關係, 混個臉熟, 我稍後會詳細的介紹策略模式

在這個需求中我們的目的只有一個, 那就是根據用戶選擇返回不同的手機


// 每個品牌都對應一個方法
chooseBrand.brandMap = new Map([
        ['Apple', () => 'iPhone 11 Pro Max'],
        ['SAMSUNG', () => 'SAMSUNG S20 Ultra'],
        ['HUAWEI', () => 'HUAWEI Mate 30 Pro'],
        ['XiaoMi', () => 'XiaoMi Mi 10 Pro'],
        ['OPPO', () => 'OPPO FindX2 Pro']
    ])

// chooseBrand方法上有一個extends方法, 如果我們需要添加新的品牌, 調用該方法可以幫助我們進行添加
chooseBrand.extends = function(brandName, brandHandler) {
    if(chooseBrand.brandMap.get(brandName)) return 'the brandName is exsit';
    chooseBrand.brandMap.set(brandName, brandHandler);
    return 'successfull';
}

function chooseBrand(choose) {
    const brandMap = chooseBrand.brandMap;
    if(!brandMap.get(choose)) return "Sorry, we can't find the brand that u input";
    return brandMap.get(choose)();
}

console.log(chooseBrand('Apple')); // iPhone 11 Pro Max
console.log(chooseBrand.extends('OnePlus', () => 'One Plus 7t Pro')); //successfull
console.log(chooseBrand('OnePlus')); // One Plus 7t Pro
console.log(chooseBrand.extends('Apple', () => '11 Pro Max')); //the brandName is exsit

你會發現我們的代碼量增多了, 但是首先我們消除了過多的if else帶來的low感, 其次每當我們需要添加一個新的手機品牌的時候我們不需要打開任何方法進行修改, 而是我們直接執行extends方法進行擴展, 這樣符合開閉原則, 代碼出錯的概率也大大降低, 同時結構看起來也非常的清晰, 這就是一個簡單的策略模式案例

OK, 再回到我們一開始總結的定義**策略模式就是將同一目的的不同行爲封裝進一個數據結構中(可以是對象, 也可以是Map結構或者其他任何你覺得合理的數據結構), 當我們需要執行不同行爲的時候, 從該數據結構中取出相對應的行爲進行處理, 將執行過程和目的分別開來**, 這樣做的好處不言而喻, 首先讀代碼就變得更加容易閱讀, 比如上方實例, 只要一看就知道所有的執行規則都處在brandMap中, 其次對於擴展來說是極好的符合開閉原則, 非常的nice和實用

小提示

你可以認爲策略模式實際上是一種多態機制, 不同的指令發送給同一方法得到得反饋截然不同, 而實際上, 策略模式也極大得借鑑了多態得思想, 如果你具備比較好得多態得概念, 相信對這個策略模式你會更加容易理解

策略模式案例

如果看完定義模塊你對策略模式還是不是很清晰, 那麼在這一章裏我會着重的多舉出幾個案例, 引起你的共鳴(在代碼中不在處理擴展和容錯機制, 擴展則是如定義中的案例一樣添加extends方法, 容錯則是進行對象是否有該屬性等判定)

  • 案例1: 處理個人所得稅

    我們有一個需求, 處理個人所得稅, 當用戶選擇地區爲中國則按照中國個人所得稅進行計算, 如果選擇美國則按照美國個人所得稅計算, 在這裏, 你可以先停止往下閱讀, 如果是你你會怎麼寫呢

    例如: 用戶輸入US和實際的金錢, 我們計算方式爲金錢 * 40%, 如果輸入CN, 計算方式則爲金錢 * 10%

    使用策略模式的代碼風格如下

    const {caculateMoney} = (function() {
        const countryMap = {
            US(salary) {
                return salary * 0.4; 
            },
            CN(salary) {
                return salary * 0.1;
            }
        }
        
        function caculateMoney(country, salary) {
           return countryMap[country](salary);
        }
        return {caculateMoney};
    }())
    
    console.log(caculateMoney('US',  100));  // 40
    
  • 案例2: 表單校驗

    我們在日常開發中, 幾乎任何一個項目都會跟表單打交道, 而處理表單校驗也是我們的必備工作之一, 那麼我們來看看這個需求

    目前用戶表單中有用戶名和密碼兩個輸入框, 我們需要校驗的用戶名和密碼都不能爲空, 同時用戶名最多爲8位, 密碼最少爲6位,必須考慮到擴展性,未來可能會加入email或者phone的驗證, 如果一般的Coder, 我相信一定會這麼寫(因爲這個案例比較具有代表性, 所以我這裏會對比Coder(小白)和高級Programmer的寫法讓你理解更加的深刻)

    Coder(小白版本)

      var userName = document.querySelector('.user-name'); // 用戶名輸入框
      var password = document.querySelector('.pass-word'); // 密碼框
       
       function validateForm() {
           if(userName == '') return '用戶名不能爲空';
           else if(userName.value.length > 8) return '用戶名最大爲8位';
    
           if(password == '') return '密碼不能爲空';
           else if(password.value.length < 6) return '密碼最小必須爲6位';
       }
    
       validateForm(); // 開啓校驗
    
    

    上面這份代碼有無似曾相識的感覺, 每個人在一開始的時候一定書寫過這樣的代碼, 這也是小白的標配, 小白不可怕, 只要勇於學習新的知識, 都會成爲大神, 只是時間問題(從來沒有看不起小白, 只看不起不懂得進步和努力得人), 那麼我們可以來看看如果是一名高級Programmer, 在考慮到擴展的問題,他會怎樣書寫代碼實現同樣的功能呢

    Programmer(高級工程師版本)

      const userName = document.querySelector('.user-name'); // 用戶名輸入框
          const password = document.querySelector('.pass-word'); // 密碼框
          const email = document.querySelector('.email'); // 郵箱
          const btn = document.querySelector('button');
          
          // 在原型上定義一個rulesStrategy對象, 該對象中儲藏了所有的校驗規則
          Validate.prototype.rulesStrategy = {
              notNull(value, msg) {
                  console.log(value, msg);
                  if(value == '') return msg;
                  
                  else {
                      console.log('sdasd')
                      return true;
                  } 
              },
              maxLength(value, msg, config) {
                  let {length} = config;
                  console.log(length);
                  if(value.length > length ) return msg;
                  else return true;
              },
              minLength(value, msg, {length} = config) {
                  if(value.length < length) return msg;
                  else return true;
              }
    
          }
    
          // 有了規則對象, 我們業務流程執行應該如下:
          // 1. 用戶使用new關鍵字獲得validate實例
          // 2. 調用實例的addRules方法來爲當前需要進行驗證的表單進行驗證, addRules接受幾個參數
          //      - _validateValue: 被校驗的value
          //      - _validateDom: 跟被校驗的value相關的dom
          //      - _strategyArr: 使用的校驗規則 Arr的每一項爲一個對象{strategyName: 'notNull', errMsg: '該input不允許爲空', validateConfig: {maxLength: 7}} 
          // 3. 可以多次調用addRules給不同的dom添加規則
          // 每一條校驗規則將被放進cacheRules中
          // 4. 調用實例上的start方法將會將cacheRules中的所有方法全部執行, 並獲取驗證結果
    
          Validate.prototype.addRules = function(_validateDom, _strategyArr) {
              // _validateDom: 被校驗的dom元素, _strategyArr: 用來校驗dom元素的規則數組(比如非空, 最大值)
              // 遍歷_strategyArr規則數組, 取出每一條規則
              _strategyArr.forEach(strategy => {
                  // strategy: 每一條規則爲一個數組, 我們可以從數組中拿出點東西
                  // strategyName: 該條規則名稱, errMsg: 如果校驗不通過返回的錯誤信息, validateConfig: 其他的校驗配置信息        
                  this.cacheRules.push(() => {
                      let [ strategyName, errMsg, validateConfig ] = strategy; // 解構出來這三哥們 
                      // 將strategyName匹配rulesStrategy中進行匹配有沒有對應規則的執行方法, 有的話拿到result結果
                      // 無的話報錯
                      // 策略模式的核心!!!!!!!!!!!!
                      let result = this.rulesStrategy[strategyName](_validateDom.value, errMsg, validateConfig)
                      // 這個是我們的初始對象, 他表示最後被返回對象的樣子
                      let initValidateResult = {dom: _validateDom, msg: 'ok', isValidate: true};
                      // lastValidateResult是我們最後要返回出去的對象, 因爲我門要儘量不要改變原對象
                      let lastValidateResult = initValidateResult; // 這裏按道理來說應該使用深度克隆deepClone包裹一下, 但是我懶得寫了
                      if(result !== true) {
                         // 如果result直接給我們返回true的話, 代表驗證通過
                         // 如果不是true的話, 我們需要更改一下lastValidateResult的值
                         lastValidateResult = Object.assign({}, initValidateResult, {isValidate: false, msg: errMsg})
                      }
                      // 如果
                      return lastValidateResult;
                  })
              })
              console.log(this.cacheRules);
              
          }
    
          // start方法用來開啓全局校驗
          Validate.prototype.start = function() {
              let resultArr = new Map();
              // 遍歷所有的cacheRules數組, 拿到每一個方法
              this.cacheRules.forEach(func => {
                  // 由於每個方法執行都會返回一個lastValidateResult對象, 所以我們解構他
                  let {dom, isValidate, msg} = func(); 
                  // dom: 被校驗dom對象, isValidate: 是否校驗通過, msg: 校驗詳細信息
                  let passValidate = resultArr.get(dom); 
                  // 我們要先看Map中有無該dom作爲key值, 如果有的話 我們需要判斷它上一條是否通過
                  // 如果上一條都沒通過, 這一條也不用看了
                  if(passValidate && passValidate.isValidate === false) return;
                  resultArr.set(dom, {dom, isValidate, msg});
              })
              return resultArr;
          }
    
    
          // 如果我們想要擴展校驗規則, 可以使用原型上的extends方法
          // 該方法接收兩個參數, rulesName: 規則名, rulesHandle: 規則執行方法
          Validate.prototype.extends = function(rulesName, rulesHandle) {
              this.rulesStrategy[rulesName] = rulesHandle;
          }
          function Validate() {
              this.cacheRules = []; // 校驗規則數組
          }
    
    
          const validate = new Validate();
    
          console.log(userName.value);
          // 添加校驗規則
          validate.addRules(userName, [['notNull', `用戶名不能爲空`], ['maxLength', `最大長度爲6位`, {length:  6}]]);
          validate.addRules(password, [['notNull', `密碼不能爲空`], ['minLength', `最小長度爲8位`, {length:  8}]]);
          // 擴展校驗規則
          validate.extends('isEmail', function(value, errMsg) {
              if(value.includes('@')) return true;
              else return errMsg;
          })
          validate.addRules(email, [['notNull', '郵箱不能爲空'], ['isEmail', '郵箱格式不正確']])
          
          btn.onclick = function() {
              const result = validate.start();
              console.log(result); // 這個result肯定能夠穩穩的拿到每一個表單的校驗結果並通過Map形式一一展現
          }
    
    
    
    

    通過上面的代碼, 不管是校驗用戶名或者密碼我們都能夠做到, 也不用管用戶名的規則多刁鑽我們都可以進行判定, 甚至來說如果我們需要新驗證一個郵箱, 我們也可以穩穩的去通過extends方法擴展郵箱的規則, 這就是策略模式的深入

    小提示
    上方的代碼如果你的基礎不太好, 可能看起來會比較吃力, 甚至來說很多基礎好的可能也覺得比較新奇和難懂, 但是筆者把註釋都已經寫好了, 你可以複製到你的編輯器中查看效果, 對比兩種代碼風格有怎麼樣的不同, 看起來吃力表示你在走出舒適區, 也是學習更進一步的明顯特性

    策略模式優缺點

    看了上面的案例, 我們也明白了一些策略模式的應用增加了一些對策略模式的基本理解, 關於我們爲什麼要用策略模式這個問題我相信大家心裏都有了一個朦朧的概念, 筆者這裏來給大家總結一下用策略模式的優缺點

    • 策略模式的優點

      1. 利用不同的策略也就是不同的書寫代碼方式, 我們得到了策略模式的最直觀的優點, 消除了過多的if else, 顯得代碼不那麼low和低級

      2. 策略模式對於設計原則 - 開閉原則進行了最完美的支持, 我們從表單的案例中就可以明白, 如果我們需要新增其他的需求, 不必扒開源碼去修改, 而是使用extends進行擴展, 這樣讓我們的代碼更加的穩固, 出錯率更低

      3. 當我們使用了策略模式以後, 拿表單案例來說, 這個校驗操作已經變得可以複用, 我們可以把他抽離成一個單獨的js文件, 在任何需要校驗表單的地方都拿出它來, 這讓我們的代碼變得更加清爽, 不用像小白一樣copy and paste

    • 策略模式的優點很明顯, 缺點也不是沒有

      1. 最明顯的, 就是代碼量明顯增加, 對代碼的理解能力要求直線上升, 要使用的很好必須經過很久的練習和對策略模式有深刻的理解

      2. 同時策略模式是違反最小知道原則的(這一點暫時混個臉熟, 後面會說最小值的原則)

    小提示

    其實我們會發現, 我們在設計模式的使用過程中, 設計模式帶來的好處不言而喻, 而他也並不是一勞永逸的, 會帶來一些副作用, 甚至爲了實現一種原則去違背另一種原則, 所以無論是在代碼中, 或者是生活工作中, 我們永遠不是追求極端的一面, 而是在某些事物之間追求平衡, 我們不追求代碼極致的內聚, 所以某些時候我們必須要自己創造一些耦合, 以現在我們的知識來說, 設計模式不必處處都強行去使用, 因爲在某種程度上它會限制我們, 比如我們的需求已經確定了永遠都只會校驗賬號密碼, 規則也永遠確定, 那麼我們其實壓根不必要使用策略模式, 我們需要一種權衡, 不要唯模式至上

    OK, 策略模式至此完結, 希望我的這篇文章能夠給你帶來幫助

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章