JavaScript 設計模式之策略模式

語言: CN / TW / HK

持續創作,加速成長!這是我參與「掘金日新計劃 · 6 月更文挑戰」的第 26 天,點選檢視活動詳情

什麼是設計模式?為什麼需要學習設計模式?

學習設計模式的目的是:為了程式碼可重用性、讓程式碼更容易被他人理解、保證程式碼可靠性。 設計模式使程式碼編寫真正工程化;設計模式是軟體工程的基石脈絡,如同大廈的結構一樣。

經典的設計模式有 23 種,但並不是每一種設計模式都被頻繁使用。在這裡,介紹最常用和最實用的幾種設計模式,本文先來介紹策略模式(Strategy Pattern)。

策略模式是一種行為設計模式,定義一系列演算法,將每一個演算法封裝起來,並讓它們可以相互替換。策略模式讓演算法獨立於使用它的客戶而變化,也稱為政策模式(Policy)。

假如正在開發一個線上商城的專案,每個產品都有原價,稱之為 originalPrice。但實際上並非所有產品都以原價出售,可能會推出允許以折扣價出售商品的促銷活動。

商家可以在後臺為產品設定不同的狀態,然後實際售價將根據產品狀態和原價動態調整。

具體規則如下:

  • 部分產品已預售:為鼓勵客戶預訂,將在原價基礎上享受 20% 的折扣。
  • 部分產品處於正常促銷階段:如果原價低於或等於100,則以10%的折扣出售;如果原價高於 100,則減 10 元。
  • 有些產品沒有任何促銷活動:它們屬於 default 狀態,並以原價出售。

這時需要寫一個獲取商品價格的函式 getPrice ,應該怎麼寫呢?

function getPrice(originalPrice, status) { // ... // 返回價格; }

事實上,面對這樣的問題,如果不考慮任何設計模式,最直觀的寫法可能 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 語句,這是非常直觀的程式碼,但是這段程式碼組織上不好。

首先,它違反了單一職責原則(Single responsibility principle,規定每個類或者函式都應該有一個單一的功能,並且該功能應該由這個類或者函式完全封裝起來)。函式 getPrice 做了太多的事情,這個函式不易閱讀,也容易出現 bug 。如果一個條件出現 bug ,整個函式就會崩潰。同時,這樣的程式碼也不容易除錯。

並且這段程式碼很難應對變化的需求,這時就需要考慮設計模式,其往往會在業務邏輯發生變化時展現出它的魅力。

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

  • 價格低於或等於 100 元的產品以 20% 的折扣出售。
  • 價格高於 100 元但低於 200 元的產品將減少 20 元。
  • 價格高於或等於 200 元的產品將減少 20 元。

這個時候該怎麼擴充套件 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 以減少臃腫。

``` / * 預售商品價格規則 * @param {} origialPrice * @returns / function preSalePrice(origialPrice) { return originalPrice * 0.8; } / * 促銷商品價格規則 * @param {} origialPrice * @returns / function promotionPrice(origialPrice) { if (origialPrice <= 100) { return origialPrice * 0.9; } else { return originalPrice - 20; } } / * 黑色星期五促銷規則 * @param {} origialPrice * @returns / function blackFridayPrice(origialPrice) { if (origialPrice >= 100 && originalPrice < 200) { return origialPrice - 20; } else if (originalPrice >= 200) { return originalPrice - 50; } else { return originalPrice * 0.8; } } / * 預設商品價格 * @param {} origialPrice * @returns / 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);
}

} ```

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

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

其實使用這些 if-else 的目的就是為了對應狀態和折扣策略。

促銷規則.png

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

可以使用對映而不是冗長的 if-else 來儲存對映,按照這個思路可以構造一個價格策略的對映關係(策略名稱與其處理函式之間的對映),如下:

const priceStrategies = { "pre-sale": preSalePrice, promotion: promotionPrice, "black-friday": blackFridayPrice, default: defaultPrice, };

將狀態與折扣策略結合起來,價格函式就可以優化成如下:

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

這時候如果需要加減折扣策略,不需要修改函式,只需要修改價格策略對映關係 priceStrategies

之前的程式碼邏輯如下:

程式碼邏輯1.png

優化後的程式碼邏輯如下:

程式碼邏輯2.png

以上的優化策略就是使用了設計模式之策略模式,在實際的專案開發過程中還是比較實用。

在什麼情況下可以考慮使用策略模式呢?如果函式具有以下特徵:

  • 判斷條件很多
  • 各個判斷條件下的程式碼相互獨立

然後可以將每個判斷條件下的程式碼封裝成一個獨立的函式,然後建立判斷條件和具體策略的對映關係。