外貿(mào)領(lǐng)航
首頁(yè)行業(yè)資訊 > 我經(jīng)常使用的3種有用的設(shè)計(jì)模式「設(shè)計(jì)模式使用心得」

我經(jīng)常使用的3種有用的設(shè)計(jì)模式「設(shè)計(jì)模式使用心得」

來(lái)源:互聯(lián)網(wǎng) 2024-07-11 13:04:01

什么是設(shè)計(jì)模式?我們?yōu)槭裁葱枰獙W(xué)習(xí)設(shè)計(jì)模式?

網(wǎng)上已經(jīng)有很多開(kāi)發(fā)者在討論。我不知道你怎么想,但對(duì)我來(lái)說(shuō):設(shè)計(jì)模式是我個(gè)人覺(jué)得可以更好解決問(wèn)題的一種方案。

這意味著什么?如果你開(kāi)發(fā)的項(xiàng)目的功能是固定的,永遠(yuǎn)不會(huì)調(diào)整業(yè)務(wù),那么你就不需要使用設(shè)計(jì)模式等任何技巧。您只需要使用通常的方式編寫代碼并完成需求即可。

但是,我們的開(kāi)發(fā)項(xiàng)目的需求是不斷變化的,這就需要我們經(jīng)常修改我們的代碼。也就是說(shuō),我們現(xiàn)在寫代碼的時(shí)候,需要為未來(lái)業(yè)務(wù)需求可能發(fā)生的變化做好準(zhǔn)備。

這時(shí),你會(huì)發(fā)現(xiàn)使用設(shè)計(jì)模式可以讓你的代碼更具可擴(kuò)展性。

經(jīng)典的設(shè)計(jì)模式有 23 種,但并不是每一種設(shè)計(jì)模式都被頻繁使用。在這里,我介紹我最常用和最實(shí)用的 3 種設(shè)計(jì)模式。

01、策略模式

假設(shè)您目前正在從事一個(gè)電子商務(wù)商店的項(xiàng)目。每個(gè)產(chǎn)品都有一個(gè)原價(jià),我們可以稱之為 originalPrice。但并非所有產(chǎn)品都以原價(jià)出售,我們可能會(huì)推出允許以折扣價(jià)出售商品的促銷活動(dòng)。商家可以在后臺(tái)為產(chǎn)品設(shè)置不同的狀態(tài)。然后實(shí)際售價(jià)將根據(jù)產(chǎn)品狀態(tài)和原價(jià)動(dòng)態(tài)調(diào)整。

具體規(guī)則如下:

部分產(chǎn)品已預(yù)售。為鼓勵(lì)客戶預(yù)訂,我們將在原價(jià)基礎(chǔ)上享受 20% 的折扣。

部分產(chǎn)品處于正常促銷階段。如果原價(jià)低于或等于100,則以10%的折扣出售;如果原價(jià)高于 100,則減 10 美元。

有些產(chǎn)品沒(méi)有任何促銷活動(dòng)。它們屬于默認(rèn)狀態(tài),以原價(jià)出售。

如果你需要寫一個(gè)getPrice函數(shù),你應(yīng)該怎么寫呢?

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

其實(shí),面對(duì)這樣的問(wèn)題,如果不考慮任何設(shè)計(jì)模式,最直觀的寫法可能就是使用if-else通過(guò)多個(gè)判斷語(yǔ)句來(lái)計(jì)算價(jià)格。

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

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 }}

有三個(gè)條件;然后,我們寫三個(gè) if 語(yǔ)句,這是非常直觀的代碼。

但是這段代碼并不友好。

首先,它違反了單一職責(zé)原則。主函數(shù) getPrice 做了太多的事情。這個(gè)函數(shù)不易閱讀,也容易出現(xiàn)bug。如果一個(gè)條件有bug,整個(gè)函數(shù)就會(huì)崩潰。同時(shí),這樣的代碼也不容易調(diào)試。

然后,這段代碼很難應(yīng)對(duì)變化。正如我在文章開(kāi)頭所說(shuō)的那樣,設(shè)計(jì)模式往往會(huì)在業(yè)務(wù)邏輯發(fā)生變化時(shí)表現(xiàn)出它的魅力。

假設(shè)我們的業(yè)務(wù)擴(kuò)大了,現(xiàn)在還有另一個(gè)折扣促銷:黑色星期五,折扣規(guī)則如下:

價(jià)格低于或等于 100 美元的產(chǎn)品以 20% 的折扣出售。價(jià)格高于 100 美元但低于 200 美元的產(chǎn)品將減少 20 美元。價(jià)格高于或等于 200 美元的產(chǎn)品將減少 20 美元。

這時(shí)候怎么擴(kuò)展getPrice函數(shù)呢?

看起來(lái)我們必須在 getPrice 函數(shù)中添加一個(gè)條件。

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 }}

每當(dāng)我們?cè)黾踊驕p少折扣時(shí),我們都需要更改函數(shù)。這種做法違反了開(kāi)閉原則。修改已有函數(shù)很容易出現(xiàn)新的錯(cuò)誤,也會(huì)讓getPrice越來(lái)越臃腫。

那么我們?nèi)绾蝺?yōu)化這段代碼呢?

首先,我們可以拆分這個(gè)函數(shù)以使 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) }}

經(jīng)過(guò)這次修改,雖然代碼行數(shù)增加了,但是可讀性有了明顯的提升。我們的main函數(shù)顯然沒(méi)有那么臃腫,寫單元測(cè)試也比較方便。

但是上面的改動(dòng)并沒(méi)有解決根本的問(wèn)題:我們的代碼還是充滿了if-else,當(dāng)我們?cè)黾踊驕p少折扣規(guī)則的時(shí)候,我們?nèi)匀恍枰薷膅etPrice。

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

實(shí)際上,使用這些 if-else 的目的是為了對(duì)應(yīng)狀態(tài)和折扣策略。

我們可以發(fā)現(xiàn),這個(gè)邏輯本質(zhì)上是一種映射關(guān)系:產(chǎn)品狀態(tài)與折扣策略的映射關(guān)系。

我們可以使用映射而不是冗長(zhǎng)的 if-else 來(lái)存儲(chǔ)映射。比如這樣:

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

我們將狀態(tài)與折扣策略結(jié)合起來(lái)。那么計(jì)算價(jià)格會(huì)很簡(jiǎn)單:

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

這時(shí)候如果需要增減折扣策略,不需要修改getPrice函數(shù),我們只需在priceStrategies對(duì)象中增減一個(gè)映射關(guān)系即可。

之前的代碼邏輯如下:

現(xiàn)在代碼邏輯:

這樣是不是更簡(jiǎn)潔嗎?

其實(shí)這招就是策略模式,是不是很實(shí)用?我不會(huì)在這里談?wù)摬呗阅J降臒o(wú)聊定義。如果你想知道策略模式的官方定義,你可以自己谷歌一下。

如果您的函數(shù)具有以下特征:

判斷條件很多。

各個(gè)判斷條件下的代碼相互獨(dú)立

然后,你可以將每個(gè)判斷條件下的代碼封裝成一個(gè)獨(dú)立的函數(shù),接著,建立判斷條件和具體策略的映射關(guān)系,使用策略模式重構(gòu)你的代碼。

02、發(fā)布-訂閱模式

這是我們?cè)陧?xiàng)目中經(jīng)常使用的一種設(shè)計(jì)模式,也經(jīng)常出現(xiàn)在面試中。

現(xiàn)在,我們有一個(gè)天氣預(yù)報(bào)系統(tǒng):當(dāng)極端天氣發(fā)生時(shí),氣象站會(huì)發(fā)布天氣警報(bào)。建筑工地、船舶和游客將根據(jù)天氣數(shù)據(jù)調(diào)整他們的日程安排。

一旦氣象站發(fā)出天氣警報(bào),他們會(huì)做以下事情:

建筑工地:停工船舶:停泊靠岸游客:取消行程

如果,我們被要求編寫可用于通知天氣警告的代碼,你會(huì)想怎么做?

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

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

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

耦合度太高。建筑工地、船舶和游客本來(lái)應(yīng)該是分開(kāi)的,但現(xiàn)在它們被置于相同的功能中。其中一個(gè)對(duì)象中的錯(cuò)誤可能會(huì)導(dǎo)致其他對(duì)象無(wú)法工作。顯然,這是不合理的。違反開(kāi)閉原則。如果有新的訂閱者加入,那么我們只能修改weatherWarning函數(shù)。

造成這種現(xiàn)象的原因是氣象站承擔(dān)了主動(dòng)告知各單位的責(zé)任。這就要求氣象站必須了解每個(gè)需要了解天氣狀況的單位。

但仔細(xì)想想,其實(shí),從邏輯上講,建筑工地、船舶、游客都應(yīng)該依靠天氣預(yù)報(bào),他們應(yīng)該是積極的一方。

我們可以將依賴項(xiàng)更改為如下所示:

氣象站發(fā)布通知,然后觸發(fā)事件,建筑工地、船舶和游客訂閱該事件。

氣象站不需要關(guān)心哪些對(duì)象關(guān)注天氣預(yù)警,只需要直接觸發(fā)事件即可。然后需要了解天氣狀況的單位主動(dòng)訂閱該事件。

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

當(dāng)然,為了完成這個(gè)發(fā)布-訂閱系統(tǒng),我們還需要實(shí)現(xiàn)一個(gè)事件訂閱和分發(fā)系統(tǒng)。

可以這樣寫:

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); }); } };};

我們之前的代碼,重構(gòu)以后變成這樣:

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')

如果你的項(xiàng)目中存在多對(duì)一的依賴,并且每個(gè)模塊相對(duì)獨(dú)立,那么你可以考慮使用發(fā)布-訂閱模式來(lái)重構(gòu)你的代碼。

事實(shí)上,發(fā)布訂閱模式應(yīng)該是我們前端開(kāi)發(fā)者最常用的設(shè)計(jì)模式。

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

03、代理模式

現(xiàn)在我們的頁(yè)面上有一個(gè)列表:

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

我們想給頁(yè)面添加一個(gè)效果:每當(dāng)用戶點(diǎn)擊列表中的每個(gè)項(xiàng)目時(shí),都會(huì)彈出一條消息:Hi, I'm ${name}

我們將如何實(shí)現(xiàn)此功能?

大致思路是給每個(gè)li元素添加一個(gè)點(diǎn)擊事件。

<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <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>

這種方法可以滿足要求,但這樣做的缺點(diǎn)是性能開(kāi)銷,因?yàn)槊總€(gè) li 標(biāo)簽都綁定到一個(gè)事件。如果列表中有數(shù)千個(gè)元素,我們是否綁定了數(shù)千個(gè)事件?

如果我們仔細(xì)看這段代碼,可以發(fā)現(xiàn)當(dāng)前的邏輯關(guān)系如下:

每個(gè) li 都有自己的事件處理機(jī)制。但是我們發(fā)現(xiàn)不管是哪個(gè)li,其實(shí)都是ul的成員。我們可以將li的事件委托給ul,讓ul成為這些 li 的事件代理。

這樣,我們只需要為這些 li 元素綁定一個(gè)事件。

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}`) } })

這實(shí)際上是代理模式。

代理模式是本體不直接出現(xiàn),而是讓代理解決問(wèn)題。

在上述情況下,li 并沒(méi)有直接處理點(diǎn)擊事件,而是將其委托給 ul。

現(xiàn)實(shí)生活中,明星并不是直接出來(lái)談生意,而是交給他們的經(jīng)紀(jì)人,也就是明星的代理人。

代理模式的應(yīng)用非常廣泛,我們來(lái)看另一個(gè)使用它的案例。

假設(shè)我們現(xiàn)在有一個(gè)計(jì)算函數(shù),參數(shù)是字符串,計(jì)算比較耗時(shí)。同時(shí),這是一個(gè)純函數(shù)。如果參數(shù)相同,則函數(shù)的返回值將相同。

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

現(xiàn)在需要給這個(gè)函數(shù)添加一個(gè)緩存函數(shù):每次計(jì)算后,存儲(chǔ)參數(shù)和對(duì)應(yīng)的結(jié)果。在接下來(lái)的計(jì)算中,會(huì)先從緩存中查詢計(jì)算結(jié)果。

你會(huì)怎么寫代碼?

當(dāng)然,你可以直接修改這個(gè)函數(shù)的功能。但這并不好,因?yàn)榫彺娌⒉皇沁@個(gè)功能的固有特性。如果將來(lái)您不需要緩存,那么,您將不得不再次修改此功能。

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

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)

這樣,我們可以在不修改原函數(shù)技術(shù)的 情況下為其擴(kuò)展計(jì)算函數(shù)。

這就是代理模式,它允許我們?cè)诓桓淖冊(cè)紝?duì)象本身的情況下添加額外的功能。

- End -

鄭重聲明:本文版權(quán)歸原作者所有,轉(zhuǎn)載文章僅為傳播更多信息之目的,如有侵權(quán)行為,請(qǐng)第一時(shí)間聯(lián)系我們修改或刪除,多謝。

CopyRight ? 外貿(mào)領(lǐng)航 2023 All Rights Reserved.