我經常使用的 3 種有用的設計模式

什麼是設計模式?我們爲什麼需要學習設計模式?

網上已經有很多開發者在討論。我不知道你怎麼想,但對我來說:設計模式是我個人覺得可以更好解決問題的一種方案。

這意味着什麼?如果你開發的項目的功能是固定的,永遠不會調整業務,那麼你就不需要使用設計模式等任何技巧。您只需要使用通常的方式編寫代碼並完成需求即可。

但是,我們的開發項目的需求是不斷變化的,這就需要我們經常修改我們的代碼。也就是說,我們現在寫代碼的時候,需要爲未來業務需求可能發生的變化做好準備。

這時,你會發現使用設計模式可以讓你的代碼更具可擴展性。

經典的設計模式有 23 種,但並不是每一種設計模式都被頻繁使用。在這裏,我介紹我最常用和最實用的 3 種設計模式。

01、策略模式

假設您目前正在從事一個電子商務商店的項目。每個產品都有一個原價,我們可以稱之爲 originalPrice。但並非所有產品都以原價出售,我們可能會推出允許以折扣價出售商品的促銷活動。商家可以在後臺爲產品設置不同的狀態。然後實際售價將根據產品狀態和原價動態調整。

具體規則如下:

部分產品已預售。爲鼓勵客戶預訂,我們將在原價基礎上享受 20% 的折扣。

部分產品處於正常促銷階段。如果原價低於或等於 100,則以 10% 的折扣出售;如果原價高於 100,則減 10 美元。

有些產品沒有任何促銷活動。它們屬於默認狀態,以原價出售。

如果你需要寫一個 getPrice 函數,你應該怎麼寫呢?

function getPrice(originalPrice, status){  // ...
  return price 
}

其實,面對這樣的問題,如果不考慮任何設計模式,最直觀的寫法可能就是使用 if-else 通過多個判斷語句來計算價格。

有三種狀態,所以我們可以快速編寫如下代碼:

function getPrice(originalPrice, status) {  if (status === 'pre-sale') {    return originalPrice * 0.8
  }  if (status === 'promotion') {    if (origialPrice <= 100) {      return origialPrice * 0.9
    } else {      return originalPrice - 20
    }
  }  if (status === 'default') {    return originalPrice
  }
}

有三個條件;然後,我們寫三個 if 語句,這是非常直觀的代碼。

但是這段代碼並不友好。

首先,它違反了單一職責原則。主函數 getPrice 做了太多的事情。這個函數不易閱讀,也容易出現 bug。如果一個條件有 bug,整個函數就會崩潰。同時,這樣的代碼也不容易調試。

然後,這段代碼很難應對變化。正如我在文章開頭所說的那樣,設計模式往往會在業務邏輯發生變化時表現出它的魅力。

假設我們的業務擴大了,現在還有另一個折扣促銷:黑色星期五,折扣規則如下:

這時候怎麼擴展 getPrice 函數呢?

看起來我們必須在 getPrice 函數中添加一個條件。

function getPrice(originalPrice, status) {  if (status === 'pre-sale') {    return originalPrice * 0.8
  }  if (status === 'promotion') {    if (origialPrice <= 100) {      return origialPrice * 0.9
    } else {      return originalPrice - 20
    }
  }  if (status === 'black-friday') {    if (origialPrice >= 100 && originalPrice < 200) {      return origialPrice - 20
    } else if (originalPrice >= 200) {      return originalPrice - 50
    } else {      return originalPrice * 0.8
    }
  }  if(status === 'default'){    return originalPrice
  }
}

每當我們增加或減少折扣時,我們都需要更改函數。這種做法違反了開閉原則。修改已有函數很容易出現新的錯誤,也會讓 getPrice 越來越臃腫。

那麼我們如何優化這段代碼呢?

首先,我們可以拆分這個函數以使 getPrice 不那麼臃腫。

function preSalePrice(origialPrice) {  return originalPrice * 0.8}function promotionPrice(origialPrice) {  if (origialPrice <= 100) {    return origialPrice * 0.9
  } else {    return originalPrice - 20
  }
}function blackFridayPrice(origialPrice) {  if (origialPrice >= 100 && originalPrice < 200) {    return origialPrice - 20
  } else if (originalPrice >= 200) {    return originalPrice - 50
  } else {    return originalPrice * 0.8
  }
}function defaultPrice(origialPrice) {  return origialPrice
}function getPrice(originalPrice, status) {  if (status === 'pre-sale') {    return preSalePrice(originalPrice)
  }  if (status === 'promotion') {    return promotionPrice(originalPrice)
  }  if (status === 'black-friday') {    return blackFridayPrice(originalPrice)
  }  if(status === 'default'){    return defaultPrice(originalPrice)
  }
}

經過這次修改,雖然代碼行數增加了,但是可讀性有了明顯的提升。我們的 main 函數顯然沒有那麼臃腫,寫單元測試也比較方便。

但是上面的改動並沒有解決根本的問題:我們的代碼還是充滿了 if-else,當我們增加或減少折扣規則的時候,我們仍然需要修改 getPrice。

想一想,我們之前用了這麼多 if-else,目的是什麼?

實際上,使用這些 if-else 的目的是爲了對應狀態和折扣策略。

我們可以發現,這個邏輯本質上是一種映射關係:產品狀態與折扣策略的映射關係。

我們可以使用映射而不是冗長的 if-else 來存儲映射。比如這樣:

let priceStrategies = {  'pre-sale': preSalePrice,  'promotion': promotionPrice,  'black-friday': blackFridayPrice,  'default': defaultPrice
}

我們將狀態與折扣策略結合起來。那麼計算價格會很簡單:

function getPrice(originalPrice, status) {
  return priceStrategies[status](originalPrice)
}

這時候如果需要增減折扣策略,不需要修改 getPrice 函數,我們只需在 priceStrategies 對象中增減一個映射關係即可。

之前的代碼邏輯如下:

現在代碼邏輯:

這樣是不是更簡潔嗎?

其實這招就是策略模式,是不是很實用?我不會在這裏談論策略模式的無聊定義。如果你想知道策略模式的官方定義,你可以自己谷歌一下。

如果您的函數具有以下特徵:

判斷條件很多。

各個判斷條件下的代碼相互獨立

然後,你可以將每個判斷條件下的代碼封裝成一個獨立的函數,接着,建立判斷條件和具體策略的映射關係,使用策略模式重構你的代碼。

02、發佈 - 訂閱模式

這是我們在項目中經常使用的一種設計模式,也經常出現在面試中。

現在,我們有一個天氣預報系統:當極端天氣發生時,氣象站會發布天氣警報。建築工地、船舶和遊客將根據天氣數據調整他們的日程安排。

一旦氣象站發出天氣警報,他們會做以下事情:

如果,我們被要求編寫可用於通知天氣警告的代碼,你會想怎麼做?

編寫天氣警告函數的常用方法可能是這樣的:

function weatherWarning(){  buildingsite.stopwork()  ships.mooring()  tourists.canceltrip()
}

這是一種非常直觀的寫法,但是這種寫法有很多不好的地方:

造成這種現象的原因是氣象站承擔了主動告知各單位的責任。這就要求氣象站必須瞭解每個需要了解天氣狀況的單位。

但仔細想想,其實,從邏輯上講,建築工地、船舶、遊客都應該依靠天氣預報,他們應該是積極的一方。

我們可以將依賴項更改爲如下所示:

氣象站發佈通知,然後觸發事件,建築工地、船舶和遊客訂閱該事件。

氣象站不需要關心哪些對象關注天氣預警,只需要直接觸發事件即可。然後需要了解天氣狀況的單位主動訂閱該事件。

這樣,氣象站與訂閱者解耦,訂閱者之間也解耦。如果有新的訂閱者,那麼它只需要直接訂閱事件,而不需要修改現有的代碼。

當然,爲了完成這個發佈 - 訂閱系統,我們還需要實現一個事件訂閱和分發系統。

可以這樣寫:

const EventEmit = function() {  this.events = {};  this.on = function(name, cb) {    if (this.events[name]) {      this.events[name].push(cb);
    } else {      this.events[name] = [cb];
    }
  };  this.trigger = function(name, ...arg) {    if (this.events[name]) {      this.events[name].forEach(eventListener => {
        eventListener(...arg);
      });
    }
  };
};

我們之前的代碼,重構以後變成這樣:

let weatherEvent = new EventEmit()

weatherEvent.on('warning', function () {  // buildingsite.stopwork()
  console.log('buildingsite.stopwork()')
})

weatherEvent.on('warning', function () {  // ships.mooring()
  console.log('ships.mooring()')
})

weatherEvent.on('warning', function () {  // tourists.canceltrip()
  console.log('tourists.canceltrip()')
})

weatherEvent.trigger('warning')

如果你的項目中存在多對一的依賴,並且每個模塊相對獨立,那麼你可以考慮使用發佈 - 訂閱模式來重構你的代碼。

事實上,發佈訂閱模式應該是我們前端開發者最常用的設計模式。

element.addEventListener('click', function(){  //...})// this is also publish-subscribe pattern

03、代理模式

現在我們的頁面上有一個列表:

<ul id="container">
    <li>Jon</li>
    <li>Jack</li>
    <li>bytefish</li>
    <li>Rock Lee</li>
    <li>Bob</li>
  </ul>

我們想給頁面添加一個效果:每當用戶點擊列表中的每個項目時,都會彈出一條消息:Hi, I'm ${name}

我們將如何實現此功能?

大致思路是給每個 li 元素添加一個點擊事件。

<!DOCTYPE html><html lang="en"><head>
  <meta charset="UTF-8">
  <meta >
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Proxy Pattern</title></head><body>
  <ul id="container">
    <li>Jon</li>
    <li>Jack</li>
    <li>bytefish</li>
    <li>Rock Lee</li>
    <li>Bob</li>
  </ul>

  <script>
    let container = document.getElementById('container')    Array.prototype.forEach.call(container.children, node => {
      node.addEventListener('click', function(e){
        e.preventDefault()
        alert(`Hi, I'm ${e.target.innerText}`)
      })
    })</script></body></html>

這種方法可以滿足要求,但這樣做的缺點是性能開銷,因爲每個 li 標籤都綁定到一個事件。如果列表中有數千個元素,我們是否綁定了數千個事件?

如果我們仔細看這段代碼,可以發現當前的邏輯關係如下:

每個 li 都有自己的事件處理機制。但是我們發現不管是哪個 li,其實都是 ul 的成員。我們可以將 li 的事件委託給 ul,讓 ul 成爲這些 li 的事件代理。

這樣,我們只需要爲這些 li 元素綁定一個事件。

  let container = document.getElementById('container')

    container.addEventListener('click', function (e) {      console.log(e)      if (e.target.nodeName === 'LI') {
        e.preventDefault()
        alert(`Hi, I'm ${e.target.innerText}`)
      }
    })

這實際上是代理模式。

代理模式是本體不直接出現,而是讓代理解決問題。

在上述情況下,li 並沒有直接處理點擊事件,而是將其委託給 ul。

現實生活中,明星並不是直接出來談生意,而是交給他們的經紀人,也就是明星的代理人。

代理模式的應用非常廣泛,我們來看另一個使用它的案例。

假設我們現在有一個計算函數,參數是字符串,計算比較耗時。同時,這是一個純函數。如果參數相同,則函數的返回值將相同。

function compute(str) {    
    // Suppose the calculation in the funtion is very time consuming        
    console.log('2000s have passed')    return 'a result'}

現在需要給這個函數添加一個緩存函數:每次計算後,存儲參數和對應的結果。在接下來的計算中,會先從緩存中查詢計算結果。

你會怎麼寫代碼?

當然,你可以直接修改這個函數的功能。但這並不好,因爲緩存並不是這個功能的固有特性。如果將來您不需要緩存,那麼,您將不得不再次修改此功能。

更好的解決方案是使用代理模式。

const proxyCompute = (function (fn){  // Create an object to store the results returned after each function execution.
  const cache = Object.create(null);  // Returns the wrapped function
  return function  (str) {    // If the cache is not hit, the function will be executed
    if ( !cache[str] ) {        let result = fn(str);        // Store the result of the function execution in the cache
        cache[str] = result;
    }    return cache[str]
  }
})(compute)

這樣,我們可以在不修改原函數技術的 情況下爲其擴展計算函數。

這就是代理模式,它允許我們在不改變原始對象本身的情況下添加額外的功能。

來源:

https://www.toutiao.com/article/7111509295270773280/?log_from=9bc1b65b6f4b7_1657503462898

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/o4QygqCrUWk5YvnR9-rUeg