今天主要介紹一下我們平常會經常用到的設計模式,設計模式總的來說有23種,而設計模式在前端中又該怎麼運用呢,接下來主要對比較前端中常見的設計模式做一個介紹 一、什麼是設計模式 一般來說,設計模式代表了最佳的實踐,通常被有經驗的面向對象的軟體開發人員所採用,在我們平時的軟體開發中,經常需要用到各種設計模 ...
今天主要介紹一下我們平常會經常用到的設計模式,設計模式總的來說有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
在上面的代碼中,通過策略模式,使得客戶的折扣與演算法解藕,又使得修改跟擴展能獨立的進行,不影到客戶端或其他演算法的使用
當我們的代碼中有很多個判斷分支,每一個條件分支都會引起該“類”的特定行為以不同的方式作出改變,這個時候就可以使用策略模式,可以改進我們代碼的質量,也更好的可以進行單元測試
今天就寫到這裡了,其實還有很多設計模式,在這裡還沒有進行總結,大家有空的話也可以自己去瞭解