探討一些經典和現代的設計模式的 JavaScript 實現。
沒有最好的,只有最合適的
設計模式一共分為 3 大類 23 種
模式類型 | 設計模式 |
---|---|
創建型模式 | 單例模式、工廠模式、建造者模式 |
結構型模式 | 適配器模式、裝飾器模式、代理模式 |
行為型模式 | 策略模式、觀察者模式、發布訂閱模式、職責鏈模式、中介者模式 |
單例模式#
一個類只有一個實例,並提供一個訪問他的全局訪問點
-
Singleton
:特定類,這是我們需要訪問的類,訪問者要拿到的是它的實例; -
instance
:單例,是特定類的實例,特定類一般會提供getInstance
方法來獲取該單例; -
getInstance
:獲取單例的方法;
class Singleton {
let _instance = null;
static getInstance() {
if (!Singleton._instance) {
Singleton.instance = new Singleton()
}
// 如果這個唯一的實例已經存在,則直接返回
return Singleton._instance
}
}
const s1 = Singleton.getInstance()
const s2 = Singleton.getInstance()
console.log(s1 === s2) // true
實例#
Vuex 實現了一個全局的 store 用來存儲應用的所有狀態。這個 store 的實現就是單例模式的典型應用。
使用場景#
-
如果一個類實例化過程消耗資源比較多,可以使用單例避免性能浪費
-
需要公共狀態,可以使用單例保證訪問一致性。
工廠模式#
工廠模式:根據不同的參數,返回不同類的實例。將對象的創建與對象的實現分離。實現複雜,但使用簡單。直接使用工廠提供的方法即可
優點:
-
良好的封裝,訪問者無需了解創建過程,代碼結構清晰。
-
擴展性良好,通過工廠方法隔離了用戶和創建流程,符合開閉原則。
缺點:
給系統增加了抽象性,帶來了額外的系統複雜度,不能濫用
實例#
document.createElement
創建 DOM
元素。這個方法採用的就是工廠模式,方法內部很複雜,但外部使用很簡單。
使用場景#
-
對象創建比較複雜,訪問者無需了解創建過程。
-
需要處理大量具有相同 / 類似屬性的小對象。
適配器模式#
用於解決兼容問題,接口 / 方法 / 數據不兼容,將其轉換成訪問者期望的格式進行使用。
場景特點:
- 整合第三方 SDK
- 封裝舊接口
裝飾器模式#
- 動態地給某個對象添加一些額外的職責,是一種實現繼承的替代方案
- 在不改變原對象的基礎上,通過對其進行包裝擴展,使原有對象可以滿足用戶的更複雜需求,而不會影響從這個類中派生的其他對象
有點原型鏈的味道
代理模式#
為一個對象提供一個代用品或佔位符,以便控制對它的訪問
使用場景#
- ES6 的 proxy
- jQuery.proxy () 方法
裝飾者與代理模式的區別#
- 裝飾者模式: 擴展功能,原有功能不變且可直接使用
- 代理模式: 顯示原有功能,但是經過限制之後的
策略模式#
定義一系列算法,根據輸入的參數決定使用哪個算法。
實例#
場景:雙十一滿減活動。滿 200-20、滿 300-50、滿 500-100。這個需求,怎麼寫?
// if-else:臃腫,難改動
function priceCalculate(discountType,price){
if(discountType === 'discount200-20'){
return price - Math.floor(price/200) * 20;
}else if(discountType === 'discount300-50'){
return price - Math.floor(price/300) * 50;
}else if(userType === 'discount500-100'){
return price - Math.floor(price/500) * 100;
}
}
//策略模式改寫,隱藏了算法,預留了增加策略的入口,便於拓展
const priceCalculate = (function(){
const discountMap = {
'discount200-20': function(price) {
return price - Math.floor(price / 200) * 20;
},
'discount300-50': function(price) {
return price - Math.floor(price/300) * 50;
},
'discount500-100': function(price) {
return price - Math.floor(price/500) * 100;
},
};
return {
addStategy(stategyName,fn){
if(discountMap[stategyName]) return;
discountMap[stategyName] = fn;
},
priceCal(discountType,price){
return discountMap[discountType] && discountMap[discountType](price);
}
}
})()
優點:
- 策略相互獨立,可以互相切換。提高了靈活性以及復用性。
- 不需要使用
if-else
進行策略選擇,提高了維護性。 - 可擴展性好,滿足開閉原則。
缺點:
- 策略相互獨立,一些複雜的算法邏輯無法共享,造成資源浪費。
- 用戶在使用策略時,需要了解具體的策略實現。不滿足最少知識原則,增加了使用成本。
使用場景#
- 算法需要自由切換的場景。
- 多個算法只有行為上有些不同,可以考慮策略模式動態選擇算法。
- 需要多重判斷,可以考慮策略模式規避多重條件判斷。
觀察者模式#
一個對象(稱為 subject)維持一系列依賴於它的對象(稱為 observer),將有關狀態的任何變更自動通知給它們(觀察者)。
優缺點#
優點:目標變化就會通知觀察者,這是觀察者模式最大的優點。
缺點: 目標和觀察者是耦合在一起的,要實現觀察者模式,必須同時引入被觀察者和觀察者才能達到響應式的效果。
使用場景#
假設 B 站用戶就是觀察者,B 站 up 主是被觀察者,有多個的 B 站用戶關注了青春湖北這個 up 主,當這個 up 主更新視頻時就會通知這些關注的 B 站用戶。
發布訂閱模式#
基於一個主題,希望接收通知的對象(稱為 subscriber)通過自定義事件訂閱主題,被激活事件的對象(稱為 publisher)通過發布主題事件的方式被通知。
使用場景#
微信會關注很多公眾號,公眾號有新文章發布時,就會有消息及時通知我們文章更新了。
這個時候公眾號為發布者,用戶為訂閱者,用戶將訂閱公眾號的事件註冊到事件調度中心,當發布者發布新文章時,會發布事件至事件調度中心,調度中心會發消息告訴訂閱者。
Vue 雙向綁定中的發布訂閱模式#
Vue
雙向綁定通過數據劫持和發布 - 訂閱模式實現
-
通過
DefineProperty
劫持各個數據的setter
和getter
,並為每個數據添加一個訂閱者列表,這個列表將會記錄所有依賴這個數據的組件。響應式數據相當於消息的發布者。
-
每個組件都對應一個
Watcher
訂閱者,當組件渲染函數執行時,會將本組件的Watcher
加入到所依賴的響應式數據的訂閱者列表中。這個過程叫做 “依賴收集”。
-
當響應式數據發生變化時,會出
setter
,setter
負責通知數據的訂閱者列表中的Watcher
,Watcher
觸發組件重新渲染來更新視圖。視圖層相當於消息的訂閱者。
觀察者模式和發布訂閱的區別#
觀察者是經典軟件設計模式
中的一種,但發布訂閱只是軟件架構中的一種消息範式
觀察者模式 | 發布訂閱 |
---|---|
2 個角色 | 3 個角色 |
重點是被觀察者 | 重點是發布訂閱中心 |
觀察與被觀察的關係是通過被觀察者主動
建立的,被觀察者
至少要有三個方法 —— 添加觀察者、移除觀察者、通知觀察者。
發布訂閱基於一個中心來建立整個體系,其中發布者
和訂閱者
不直接進行通信,而是發布者將要發布的消息交由中心管理,訂閱者也是根據自己的情況,按需訂閱中心中的消息。
發布訂閱的實現內部利用了觀察者模式
,但由於發布訂閱中心
這一中間層的出現,對於生產方和消費方的通信管理變得更加的可管理和可拓展。