前端常見的設計模式
今天主要介紹一下我們平常會經常用到的設計模式,設計模式總的來說有23種,而設計模式在前端中又該怎麼運用呢,接下來主要對比較前端中常見的設計模式做一個介紹
一、什麼是設計模式
一般來說,設計模式代表了最佳的實踐,通常被有經驗的面向物件的軟體開發人員所採用,在我們平時的軟體開發中,經常需要用到各種設計模式,設計模式是一套被反覆使用的、多數人知曉的、經過分類編目的、程式碼設計經驗的總結 ,使用設計模式是為了重用程式碼、讓程式碼更容易被他人理解、保證程式碼可靠性。
設計模式可以說是軟體工程的基石,合理的使用設計模式,可以使我們的程式碼真正的工程化,在專案中使用設計模式可以完美的解決很多問題,在設計模式中,大概來說總共有23種,而具體要用哪一種還需要根據情況而定,就像平時在前端開發中,我比較熟悉的就是工廠模式,原型模式和MVC這些模式啦,接下來主要對其中的一些設計模式進行一個比較詳細的介紹。
二、設計模式的分類
首先,還是需要先說一下設計模式的分類,剛才說到設計模式總的來說有23種,而這23種,又可以分為以下四大類
1、建立型模式
建立型模式提供了一種在建立物件的同時隱藏建立邏輯的方式,而不是使用 new 運算子直接例項化物件,這使得程式在判斷針對某個給定例項需要建立哪些物件時更加靈活,主要包括以下幾種:
工廠模式、抽象工廠模式、單例模式、建造者模式、原型模式
2、結構型模式
結構型模式關注類和物件的組合繼承的概念被用來組合介面和定義組合物件獲得新功能的方式,主要包括以下幾種:
介面卡模式、橋接模式、過濾器模式、組合模式、裝飾器模式、外觀模式、享元模式、代理模式
3、行為型模式
行為型模式關注物件之間的通訊,主要包括以下幾種:
責任鏈模式、命令模式、直譯器模式、迭代器模式、中介者模式、備忘錄模式、觀察者模式、狀態模式、空物件模式、策略模式、模板模式、訪問者模式
4、J2EE模式
J2EE模式關注表示層,這些模式是由 Sun Java Center 鑑定的,主要包括以下幾種:
MVC 模式、業務代表模式、組合實體模式、資料訪問物件模式、前端控制器模式、攔截過濾器模式、服務定位器模式、傳輸物件模式
三、設計模式六大原則
上面介紹了幾種不同設計的模式,而所有的設計模式都需要遵循下面的六大原則
1、開閉原則
開閉原則的意思是:對擴充套件開放,對修改關閉,在程式需要進行拓展的時候,不能去修改原有的程式碼
2、里氏代換原則
里氏代換原則中說,任何基類可以出現的地方,子類一定可以出現,里氏代換原則是對開閉原則的補充,實現開閉原則的關鍵步驟就是抽象化,而基類與子類的繼承關係就是抽象化的具體實現,所以里氏代換原則是對實現抽象化的具體步驟的規範
3、依賴倒轉原則
這個原則是開閉原則的基礎,具體內容:針對介面程式設計,依賴於抽象而不依賴於具體
4、介面隔離原則
這個原則的意思是:使用多個隔離的介面,比使用單個介面要好,它還有另外一個意思是:降低類之間的耦合度,此可見,其實設計模式就是從大型軟體架構出發、便於升級和維護的軟體設計思想,它強調降低依賴,降低耦合
5、最少知道原則
最少知道原則是指:一個實體應當儘量少地與其他實體之間發生相互作用,使得系統功能模組相對獨立
6、合成複用原則
合成複用原則是指:儘量使用合成/聚合的方式,而不是使用繼承
四、常見的設計模式
設計模式有很多種,接下來,我將介紹其中的幾種,並且介紹這些設計模式怎麼運用在前端中
1、工廠模式
工廠模式是用來建立物件的一種最常用的設計模式,我們不暴露建立物件的具體邏輯,而是將將邏輯封裝在一個函式中,那麼這個函式就可以被視為一個工廠,工廠模式根據抽象程度的不同可以分為:簡單工廠,工廠方法和抽象工廠,接下來,將對簡單工廠和工廠方法在JavaScript中的運用舉個簡單的例子
(1)簡單工廠
簡單工廠模式又叫靜態工廠模式,由一個工廠物件決定建立某一種產品物件類的例項,主要用來建立同一類物件
比如說,在實際的專案中,我們常常需要根據使用者的許可權來渲染不同的頁面,高階許可權的使用者所擁有的頁面有些是無法被低階許可權的使用者所檢視,所以我們可以在不同許可權等級使用者的建構函式中,儲存該使用者能夠看到的頁面。在根據許可權例項化使用者
let UserFactory = function (role) { function SuperAdmin() { this.name = "超級管理員", this.viewPage = ['首頁', '通訊錄', '發現頁', '應用資料', '許可權管理'] } function Admin() { this.name = "管理員", this.viewPage = ['首頁', '通訊錄', '發現頁', '應用資料'] } function NormalUser() { this.name = '普通使用者', this.viewPage = ['首頁', '通訊錄', '發現頁'] } switch (role) { case 'superAdmin': return new SuperAdmin(); break; case 'admin': return new Admin(); break; case 'user': return new NormalUser(); break; default: throw new Error('引數錯誤, 可選引數:superAdmin、admin、user'); } } //呼叫 let superAdmin = UserFactory('superAdmin'); let admin = UserFactory('admin') let normalUser = UserFactory('user')
在上面的例子中,UserFactory
就是一個簡單工廠,在該函式中有3個建構函式分別對應不同的許可權的使用者,當我們呼叫工廠函式時,只需要傳遞superAdmin
, admin
, user
這三個可選引數中的一個獲取對應的例項物件
優點:簡單工廠的優點在於,你只需要一個正確的引數,就可以獲取到你所需要的物件,而無需知道其建立的具體細節
缺點:在函式內包含了所有物件的建立邏輯(建構函式)和判斷邏輯的程式碼,每增加新的建構函式還需要修改判斷邏輯程式碼,我們的物件不是上面的3個而是30個或更多時,這個函式會成為一個龐大的超級函式,便得難以維護,簡單工廠只能作用於建立的物件數量較少,物件的建立邏輯不復雜時使用
(2)工廠方法
工廠方法模式的本意是將實際建立物件的工作推遲到子類中,這樣核心類就變成了抽象類,但是在JavaScript中很難像傳統面向物件那樣去實現建立抽象類,所以在JavaScript中我們只需要參考它的核心思想即可,我們可以將工廠方法看作是一個例項化物件的工廠類
比如說上面的例子,我們用工廠方法可以這樣寫,工廠方法我們只把它看作是一個例項化物件的工廠,它只做例項化物件這一件事情,我們採用安全模式建立物件
//安全模式建立的工廠方法函式 let UserFactory = function(role) { if(this instanceof UserFactory) { var s = new this[role](); return s; } else { return new UserFactory(role); } } //工廠方法函式的原型中設定所有物件的建構函式 UserFactory.prototype = { SuperAdmin: function() { this.name = "超級管理員", this.viewPage = ['首頁', '通訊錄', '發現頁', '應用資料', '許可權管理'] }, Admin: function() { this.name = "管理員", this.viewPage = ['首頁', '通訊錄', '發現頁', '應用資料'] }, NormalUser: function() { this.name = '普通使用者', this.viewPage = ['首頁', '通訊錄', '發現頁'] } } //呼叫 let superAdmin = UserFactory('SuperAdmin'); let admin = UserFactory('Admin') let normalUser = UserFactory('NormalUser')
在簡單工廠中,如果我們新增加一個使用者型別,需要修改兩個地方的程式碼,一個是增加新的使用者建構函式,一個是在邏輯判斷中增加對新的使用者的判斷,而在抽象工廠方法中,我們只需要在UserFactory.prototype中新增就可以啦
2、代理模式
代理模式主要是為其他物件提供一種代理以控制對這個物件的訪問,主要解決在直接訪問物件時帶來的問題,比如說:要訪問的物件在遠端的機器上,在面向物件系統中,有些物件由於某些原因(比如物件建立開銷很大,或者某些操作需要安全控制,或者需要程序外的訪問),直接訪問會給使用者或者系統結構帶來很多麻煩,我們可以在訪問此物件時加上一個對此物件的訪問層
代理模式最基本的形式是對訪問進行控制,代理物件和另一個物件(本體)實現的是同樣的介面,實際上工作還是本體在做,它才是負責執行所分派的任務的那個物件或類,代理物件所做的不外乎節制對本體的訪問,代理物件並不會在另一物件的基礎上新增方法或修改其方法,也不會簡化那個物件的介面,它實現的介面與本體完全相同,所有對它進行的方法呼叫都會被傳遞給本體
(function(){ // 示例程式碼 // 目標物件,是真正被代理的物件 function Subject(){} Subject.prototype.request = function(){}; /** * 代理物件 * @param {Object} realSubject [持有被代理的具體的目標物件] */ function Proxy(realSubject){ this.realSubject = readSubject; } Proxy.prototype.request = function(){ this.realSubject.request(); }; }());
在上面的程式碼中,Proxy可以控制對真正被代理物件的一個訪問,在代理模式中,比較常見的就是虛擬代理,虛擬代理用於控制對那種建立開銷很大的本體的訪問,它會把本體的例項化推遲到有方法被呼叫的時候,比如說,現在我們假設PublicLibrary的例項化很慢,不能在網頁載入的時候立即完成,我們可以為其建立一個虛擬代理,讓它把PublicLibrary的例項化推遲到必要的時候,比如說我們在前端中經常用到的圖片懶載入,就可以用虛擬代理
3、觀察者模式
如果大家學過一些像vue,react這些框架,相信大家對觀察者模式一定很熟悉,現在很多mvvm框架都用到了觀察者模式這個思想,觀察者模式又叫做釋出—訂閱模式,它定義物件間的一種一對多的依賴關係,當一個物件的狀態發生改變時,所有依賴於它的物件都將得到通知和更新,觀察者模式提供了一個訂閱模型,其中物件訂閱事件並在發生時得到通知,這種模式是事件驅動的程式設計基石,它有利益於良好的面向物件的設計
下面舉個例子,比如我們給頁面中的一個dom節點繫結一個事件,其實就可以看做是一種觀察者模式
document.body.addEventListener("click", function() { alert("Hello World") },false ) document.body.click() //模擬使用者點選
在上面的例子中,需要監聽使用者點選 document.body 的動作,但是我們是沒辦法預知使用者將在什麼時候點選的,因此我們訂閱了 document.body 的 click 事件,當 body 節點被點選時,body 節點便會向訂閱者釋出 "Hello World" 訊息
4、單例模式
單例模式保證一個類僅有一個例項,並提供一個訪問它的全域性訪問點,保證一個類只有一個例項,實現的方法一般是先判斷例項存在與否,如果存在直接返回,如果不存在就建立了再返回,這就確保了一個類只有一個例項物件
下面舉個例子,在js中,我們可以使用閉包來建立實現這種模式
var single = (function(){ var unique; function getInstance(){ // 如果該例項存在,則直接返回,否則就對其例項化 if( unique === undefined ){ unique = new Construct(); } return unique; } function Construct(){ // ... 生成單例的建構函式的程式碼 } return { getInstance : getInstance } })();
在上面的程式碼中,我們可以使用single.getInstance來獲取到單例,並且每次呼叫均獲取到同一個單例,在我們平時的開發中,我們也經常會用到這種模式,比如當我們單擊登入按鈕的時候,頁面中會出現一個登入框,而這個浮窗是唯一的,無論單擊多少次登入按鈕,這個浮窗只會被建立一次,因此這個登入浮窗就適合用單例模式
5、策略模式
策略模式指的是定義一些列的演算法,把他們一個個封裝起來,目的就是將演算法的使用與演算法的實現分離開來,避免多重判斷條件,更具有擴充套件性
下面也是舉個例子,現在超市有活動,vip為5折,老客戶3折,普通顧客沒折,計算最後需要支付的金額,如果不使用策略模式,我們的程式碼可能和下面一樣
function Price(personType, price) { //vip 5 折 if (personType == 'vip') { return price * 0.5; } else if (personType == 'old'){ //老客戶 3 折 return price * 0.3; } else { return price; //其他都全價 } }
在上面的程式碼中,我們需要很多個判斷,如果有很多優惠,我們又需要新增很多判斷,這裡已經違背了剛才說的設計模式的六大原則中的開閉原則了,如果使用策略模式,我們的程式碼可以這樣寫
// 對於vip客戶 function vipPrice() { this.discount = 0.5; } vipPrice.prototype.getPrice = function(price) { return price * this.discount; } // 對於老客戶 function oldPrice() { this.discount = 0.3; } oldPrice.prototype.getPrice = function(price) { return price * this.discount; } // 對於普通客戶 function Price() { this.discount = 1; } Price.prototype.getPrice = function(price) { return price ; } // 上下文,對於客戶端的使用 function Context() { this.name = ''; this.strategy = null; this.price = 0; } Context.prototype.set = function(name, strategy, price) { this.name = name; this.strategy = strategy; this.price = price; } Context.prototype.getResult = function() { console.log(this.name + ' 的結賬價為: ' + this.strategy.getPrice(this.price)); } var context = new Context(); var vip = new vipPrice(); context.set ('vip客戶', vip, 200); context.getResult();// vip客戶 的結賬價為: 100 var old = new oldPrice(); context.set ('老客戶', old, 200); context.getResult();// 老客戶 的結賬價為: 60 var Price = new Price(); context.set ('普通客戶', Price, 200); context.getResult();// 普通客戶 的結賬價為: 200
在上面的程式碼中,通過策略模式,使得客戶的折扣與演算法解藕,又使得修改跟擴充套件能獨立的進行,不影到客戶端或其他演算法的使用
當我們的程式碼中有很多個判斷分支,每一個條件分支都會引起該“類”的特定行為以不同的方式作出改變,這個時候就可以使用策略模式,可以改進我們程式碼的質量,也更好的可以進行單元測試
今天就寫到這裡了,其實還有很多設計模式,在這裡還沒有進行總結,大家有空的話也可以自己去了解