我經常使用的 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,整個函數就會崩潰。同時,這樣的代碼也不容易調試。
然後,這段代碼很難應對變化。正如我在文章開頭所說的那樣,設計模式往往會在業務邏輯發生變化時表現出它的魅力。
假設我們的業務擴大了,現在還有另一個折扣促銷:黑色星期五,折扣規則如下:
-
價格低於或等於 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 不那麼臃腫。
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()
}
這是一種非常直觀的寫法,但是這種寫法有很多不好的地方:
-
耦合度太高。建築工地、船舶和遊客本來應該是分開的,但現在它們被置於相同的功能中。其中一個對象中的錯誤可能會導致其他對象無法工作。顯然,這是不合理的。
-
違反開閉原則。如果有新的訂閱者加入,那麼我們只能修改 weatherWarning 函數。
造成這種現象的原因是氣象站承擔了主動告知各單位的責任。這就要求氣象站必須瞭解每個需要了解天氣狀況的單位。
但仔細想想,其實,從邏輯上講,建築工地、船舶、遊客都應該依靠天氣預報,他們應該是積極的一方。
我們可以將依賴項更改爲如下所示:
氣象站發佈通知,然後觸發事件,建築工地、船舶和遊客訂閱該事件。
氣象站不需要關心哪些對象關注天氣預警,只需要直接觸發事件即可。然後需要了解天氣狀況的單位主動訂閱該事件。
這樣,氣象站與訂閱者解耦,訂閱者之間也解耦。如果有新的訂閱者,那麼它只需要直接訂閱事件,而不需要修改現有的代碼。
當然,爲了完成這個發佈 - 訂閱系統,我們還需要實現一個事件訂閱和分發系統。
可以這樣寫:
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