前端常見的9種設計模式
本文目錄:
- 1.概念
- 2.設計原則
- 3.設計模式的型別
- 前端常見設計模式1:外觀模式(Facade Pattern)
- 前端常見設計模式2:代理模式(Proxy Pattern)
- 前端常見設計模式3:工廠模式(Factory Pattern)
- 前端常見設計模式4:單例模式(Singleton Pattern)
- 前端常見設計模式5:策略模式(Strategy Pattern)
- 前端常見設計模式6:迭代器模式(Iterator Pattern)
- 前端常見設計模式7:觀察者模式(Observer Pattern)
- 前端常見設計模式8:中介者模式(Mediator Pattern)
- 前端常見設計模式9:訪問者模式(Visitor Pattern)
1. 概念
設計模式是一套被反覆使用的、多數人知曉的、經過分類編目的、程式碼設計經驗的總結。使用設計模式是為了重用程式碼、讓程式碼更容易被他人理解、保證程式碼可靠性。毫無疑問,設計模式於己於他人於系統都是多贏的,設計模式使程式碼編制真正工程化,設計模式是軟體工程的基石,如同大廈的一塊塊磚石一樣。
2. 設計原則
- S – Single Responsibility Principle 單一職責原則
一個程式只做好一件事
如果功能過於複雜就拆分開,每個部分保持獨立 - O – OpenClosed Principle 開放/封閉原則
對擴充套件開放,對修改封閉
增加需求時,擴充套件新程式碼,而非修改已有程式碼 - L – Liskov Substitution Principle 里氏替換原則
子類能覆蓋父類
父類能出現的地方子類就能出現 - I – Interface Segregation Principle 介面隔離原則
保持介面的單一獨立
類似單一職責原則,這裡更關注介面 - D – Dependency Inversion Principle 依賴倒轉原則
面向介面程式設計,依賴於抽象而不依賴於具
使用方只關注介面而不關注具體類的實現
3. 設計模式的型別
- 1.結構型模式(Structural Patterns): 通過識別系統中元件間的簡單關係來簡化系統的設計。
- 2.建立型模式(Creational Patterns): 處理物件的建立,根據實際情況使用合適的方式建立物件。常規的物件建立方式可能會導致設計上的問題,或增加設計的複雜度。建立型模式通過以某種方式控制物件的建立來解決問題。
- 3.行為型模式(Behavioral Patterns): 用於識別物件之間常見的互動模式並加以實現,如此,增加了這些互動的靈活性。
4.前端常見設計模式1:外觀模式(Facade Pattern)
外觀模式是最常見的設計模式之一,它為子系統中的一組介面提供一個統一的高層介面,使子系統更容易使用。簡而言之外觀設計模式就是把多個子系統中複雜邏輯進行抽象,從而提供一個更統一、更簡潔、更易用的API。很多我們常用的框架和庫基本都遵循了外觀設計模式,比如JQuery就把複雜的原生DOM操作進行了抽象和封裝,並消除了瀏覽器之間的相容問題,從而提供了一個更高階更易用的版本。其實在平時工作中我們也會經常用到外觀模式進行開發,只是我們不自知而已。
相容瀏覽器事件繫結
let addMyEvent = function (el, ev, fn) {
if (el.addEventListener) {
el.addEventListener(ev, fn, false)
} else if (el.attachEvent) {
el.attachEvent('on' + ev, fn)
} else {
el['on' + ev] = fn
}
};
封裝介面
let myEvent = {
// ...
stop: e => {
e.stopPropagation();
e.preventDefault();
}
};
場景
設計初期,應該要有意識地將不同的兩個層分離,比如經典的三層結構,在資料訪問層和業務邏輯層、業務邏輯層和表示層之間建立外觀Facade
在開發階段,子系統往往因為不斷的重構演化而變得越來越複雜,增加外觀Facade可以提供一個簡單的介面,減少他們之間的依賴。
在維護一個遺留的大型系統時,可能這個系統已經很難維護了,這時候使用外觀Facade也是非常合適的,為繫系統開發一個外觀Facade類,為設計粗糙和高度複雜的遺留程式碼提供比較清晰的介面,讓新系統和Facade物件互動,Facade與遺留程式碼互動所有的複雜工作。
優點
- 減少系統相互依賴。
- 提高靈活性。
- 提高了安全性
缺點
不符合開閉原則,如果要改東西很麻煩,繼承重寫都不合適。
前端常見設計模式2:代理模式(Proxy Pattern)
是為一個物件提供一個代用品或佔位符,以便控制對它的訪問
假設當A 在心情好的時候收到花,小明表白成功的機率有 60%,而當A 在心情差的時候收到花,小明表白的成功率無限趨近於0。小明跟A 剛剛認識兩天,還無法辨別A 什麼時候心情好。如果不合時宜地把花送給A,花 被直接扔掉的可能性很大,這束花可是小明吃了7 天泡麵換來的。但是A 的朋友B 卻很瞭解A,所以小明只管把花交給B,B 會監聽A 的心情變化,然後選 擇A 心情好的時候把花轉交給A,程式碼如下:
let Flower = function() {}
let xiaoming = {
sendFlower: function(target) {
let flower = new Flower()
target.receiveFlower(flower)
}
}
let B = {
receiveFlower: function(flower) {
A.listenGoodMood(function() {
A.receiveFlower(flower)
})
}
}
let A = {
receiveFlower: function(flower) {
console.log('收到花'+ flower)
},
listenGoodMood: function(fn) {
setTimeout(function() {
fn()
}, 1000)
}
}
xiaoming.sendFlower(B)
場景
HTML元 素事件代理
<ul id="ul">
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<script>
let ul = document.querySelector('#ul');
ul.addEventListener('click', event => {
console.log(event.target);
});
</script>
ES6 的 proxy 阮一峰Proxy
jQuery.proxy()方法
優點
- 代理模式能將代理物件與被呼叫物件分離,降低了系統的耦合度。代理模式在客戶端和目標物件之間起到一箇中介作用,這樣可以起到保護目標物件的作用
- 代理物件可以擴充套件目標物件的功能;通過修改代理物件就可以了,符合開閉原則;
缺點
處理請求速度可能有差別,非直接訪問存在開銷
前端常見設計模式3:工廠模式(Factory Pattern)
工廠模式定義一個用於建立物件的介面,這個介面由子類決定例項化哪一個類。該模式使一個類的例項化延遲到了子類。而子類可以重寫介面方法以便建立的時候指定自己的物件型別。
class Product {
constructor(name) {
this.name = name
}
init() {
console.log('init')
}
fun() {
console.log('fun')
}
}
class Factory {
create(name) {
return new Product(name)
}
}
// use
let factory = new Factory()
let p = factory.create('p1')
p.init()
p.fun()
場景
- 如果你不想讓某個子系統與較大的那個物件之間形成強耦合,而是想執行時從許多子系統中進行挑選的話,那麼工廠模式是一個理想的選擇
- 將new操作簡單封裝,遇到new的時候就應該考慮是否用工廠模式;
- 需要依賴具體環境建立不同例項,這些例項都有相同的行為,這時候我們可以使用工廠模式,簡化實現的過程,同時也可以減少每種物件所需的程式碼量,有利於消除物件間的耦合,提供更大的靈活性
優點
- 建立物件的過程可能很複雜,但我們只需要關心建立結果。
- 建構函式和建立者分離, 符合“開閉原則”
- 一個呼叫者想建立一個物件,只要知道其名稱就可以了。
- 擴充套件性高,如果想增加一個產品,只要擴充套件一個工廠類就可以。
缺點
- 新增新產品時,需要編寫新的具體產品類,一定程度上增加了系統的複雜度
- 考慮到系統的可擴充套件性,需要引入抽象層,在客戶端程式碼中均使用抽象層進行定義,增加了系統的抽象性和理解難度
什麼時候不用
- 當被應用到錯誤的問題型別上時,這一模式會給應用程式引入大量不必要的複雜性.除非為建立物件提供一個介面是我們編寫的庫或者框架的一個設計上目標,否則我會建議使用明確的構造器,以避免不必要的開銷。
- 由於物件的建立過程被高效的抽象在一個介面後面的事實,這也會給依賴於這個過程可能會有多複雜的單元測試帶來問題。
前端常見設計模式4:單例模式(Singleton Pattern)
顧名思義,單例模式中Class的例項個數最多為1。當需要一個物件去貫穿整個系統執行某些任務時,單例模式就派上了用場。而除此之外的場景儘量避免單例模式的使用,因為單例模式會引入全域性狀態,而一個健康的系統應該避免引入過多的全域性狀態。
實現單例模式需要解決以下幾個問題:
- 如何確定Class只有一個例項?
- 如何簡便的訪問Class的唯一例項?
- Class如何控制例項化的過程?
- 如何將Class的例項個數限制為1?
我們一般通過實現以下兩點來解決上述問題:
1.隱藏Class的建構函式,避免多次例項化
2.通過暴露一個 getInstance() 方法來建立/獲取唯一例項
Javascript中單例模式可以通過以下方式實現:
// 單例構造器
const FooServiceSingleton = (function () {
// 隱藏的Class的建構函式
function FooService() {}
// 未初始化的單例物件
let fooService;
return {
// 建立/獲取單例物件的函式
getInstance: function () {
if (!fooService) {
fooService = new FooService();
}
return fooService;
}
}
})();
實現的關鍵點有:
- 使用 IIFE建立區域性作用域並即時執行;
- getInstance()為一個 閉包 ,使用閉包儲存區域性作用域中的單例物件並返回。
我們可以驗證下單例物件是否建立成功:
const fooService1 = FooServiceSingleton.getInstance();
const fooService2 = FooServiceSingleton.getInstance();
console.log(fooService1 === fooService2); // true
場景
- 定義名稱空間和實現分支型方法
- 登入框
- vuex 和 redux中的store
優點
- 劃分名稱空間,減少全域性變數
- 增強模組性,把自己的程式碼組織在一個全域性變數名下,放在單一位置,便於維護
- 且只會例項化一次。簡化了程式碼的除錯和維護
缺點
由於單例模式提供的是一種單點訪問,所以它有可能導致模組間的強耦合
從而不利於單元測試。無法單獨測試一個呼叫了來自單例的方法的類,而只能把它與那個單例作為一個單元一起測試。
前端常見設計模式5:策略模式(Strategy Pattern)
策略模式簡單描述就是:物件有某個行為,但是在不同的場景中,該行為有不同的實現演算法。把它們一個個封裝起來,並且使它們可以互相替換
<html>
<head>
<title>策略模式-校驗表單</title>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
</head>
<body>
<form id = "registerForm" method="post" action="http://xxxx.com/api/register">
使用者名稱:<input type="text" name="userName">
密碼:<input type="text" name="password">
手機號碼:<input type="text" name="phoneNumber">
<button type="submit">提交</button>
</form>
<script type="text/javascript">
// 策略物件
const strategies = {
isNoEmpty: function (value, errorMsg) {
if (value === '') {
return errorMsg;
}
},
isNoSpace: function (value, errorMsg) {
if (value.trim() === '') {
return errorMsg;
}
},
minLength: function (value, length, errorMsg) {
if (value.trim().length < length) {
return errorMsg;
}
},
maxLength: function (value, length, errorMsg) {
if (value.length > length) {
return errorMsg;
}
},
isMobile: function (value, errorMsg) {
if (!/^(13[0-9]|14[5|7]|15[0|1|2|3|5|6|7|8|9]|17[7]|18[0|1|2|3|5|6|7|8|9])\d{8}$/.test(value)) {
return errorMsg;
}
}
}
// 驗證類
class Validator {
constructor() {
this.cache = []
}
add(dom, rules) {
for(let i = 0, rule; rule = rules[i++];) {
let strategyAry = rule.strategy.split(':')
let errorMsg = rule.errorMsg
this.cache.push(() => {
let strategy = strategyAry.shift()
strategyAry.unshift(dom.value)
strategyAry.push(errorMsg)
return strategies[strategy].apply(dom, strategyAry)
})
}
}
start() {
for(let i = 0, validatorFunc; validatorFunc = this.cache[i++];) {
let errorMsg = validatorFunc()
if (errorMsg) {
return errorMsg
}
}
}
}
// 呼叫程式碼
let registerForm = document.getElementById('registerForm')
let validataFunc = function() {
let validator = new Validator()
validator.add(registerForm.userName, [{
strategy: 'isNoEmpty',
errorMsg: '使用者名稱不可為空'
}, {
strategy: 'isNoSpace',
errorMsg: '不允許以空白字元命名'
}, {
strategy: 'minLength:2',
errorMsg: '使用者名稱長度不能小於2位'
}])
validator.add(registerForm.password, [ {
strategy: 'minLength:6',
errorMsg: '密碼長度不能小於6位'
}])
validator.add(registerForm.phoneNumber, [{
strategy: 'isMobile',
errorMsg: '請輸入正確的手機號碼格式'
}])
return validator.start()
}
registerForm.onsubmit = function() {
let errorMsg = validataFunc()
if (errorMsg) {
alert(errorMsg)
return false
}
}
</script>
</body>
</html>
場景
- 如果在一個系統裡面有許多類,它們之間的區別僅在於它們的'行為',那麼-
使用策略模式可以動態地讓一個物件在許多行為中選擇一種行為。 - 一個系統需要動態地在幾種演算法中選擇一種。
- 表單驗證
優點
- 利用組合、委託、多型等技術和思想,可以有效的避免多重條件選擇語句
- 提供了對開放-封閉原則的完美支援,將演算法封裝在獨立的strategy中,使得它們易於切換,理解,易於擴充套件
- 利用組合和委託來讓Context擁有執行演算法的能力,這也是繼承的一種更輕便的代替方案
缺點
- 會在程式中增加許多策略類或者策略物件
- 要使用策略模式,必須瞭解所有的strategy,必須瞭解各個strategy之間的不同點,這樣才能選擇一個合適的strategy
前端常見設計模式6:迭代器模式(Iterator Pattern)
迭代器模式簡單的說就是提供一種方法順序一個聚合物件中各個元素,而又不暴露該物件的內部表示。
迭代器模式解決了以下問題:
- 提供一致的遍歷各種資料結構的方式,而不用瞭解資料的內部結構
- 提供遍歷容器(集合)的能力而無需改變容器的介面
一個迭代器通常需要實現以下介面:
- hasNext():判斷迭代是否結束,返回Boolean
- next():查詢並返回下一個元素
為Javascript的陣列實現一個迭代器可以這麼寫:
const item = [1, 'red', false, 3.14];
function Iterator(items) {
this.items = items;
this.index = 0;
}
Iterator.prototype = {
hasNext: function () {
return this.index < this.items.length;
},
next: function () {
return this.items[this.index++];
}
}
驗證一下迭代器是否工作:
const iterator = new Iterator(item);
while(iterator.hasNext()){
console.log(iterator.next());
}
//輸出:1, red, false, 3.14
ES6提供了更簡單的迭代迴圈語法 for...of,使用該語法的前提是操作物件需要實現 可迭代協議(The iterable protocol),簡單說就是該物件有個Key為 Symbol.iterator 的方法,該方法返回一個iterator物件。
比如我們實現一個 Range 類用於在某個數字區間進行迭代:
function Range(start, end) {
return {
[Symbol.iterator]: function () {
return {
next() {
if (start < end) {
return { value: start++, done: false };
}
return { done: true, value: end };
}
}
}
}
}
驗證一下:
for (num of Range(1, 5)) {
console.log(num);
}
// 輸出:1, 2, 3, 4
前端常見設計模式7:觀察者模式(Observer Pattern)
觀察者模式又稱釋出-訂閱模式(Publish/Subscribe Pattern),是我們經常接觸到的設計模式,日常生活中的應用也比比皆是,比如你訂閱了某個博主的頻道,當有內容更新時會收到推送;又比如JavaScript中的事件訂閱響應機制。觀察者模式的思想用一句話描述就是:被觀察物件(subject)維護一組觀察者(observer),當被觀察物件狀態改變時,通過呼叫觀察者的某個方法將這些變化通知到觀察者。
觀察者模式中Subject物件一般需要實現以下API:
- subscribe(): 接收一個觀察者observer物件,使其訂閱自己
- unsubscribe(): 接收一個觀察者observer物件,使其取消訂閱自己
- fire(): 觸發事件,通知到所有觀察者
用JavaScript手動實現觀察者模式:
// 被觀察者
function Subject() {
this.observers = [];
}
Subject.prototype = {
// 訂閱
subscribe: function (observer) {
this.observers.push(observer);
},
// 取消訂閱
unsubscribe: function (observerToRemove) {
this.observers = this.observers.filter(observer => {
return observer !== observerToRemove;
})
},
// 事件觸發
fire: function () {
this.observers.forEach(observer => {
observer.call();
});
}
}
驗證一下訂閱是否成功:
const subject = new Subject();
function observer1() {
console.log('Observer 1 Firing!');
}
function observer2() {
console.log('Observer 2 Firing!');
}
subject.subscribe(observer1);
subject.subscribe(observer2);
subject.fire();
//輸出:
Observer 1 Firing!
Observer 2 Firing!
驗證一下取消訂閱是否成功:
subject.unsubscribe(observer2);
subject.fire();
//輸出:
Observer 1 Firing!
場景
DOM事件
document.body.addEventListener('click', function() {
console.log('hello world!');
});
document.body.click()
vue 響應式的實現
優點
支援簡單的廣播通訊,自動通知所有已經訂閱過的物件
目標物件與觀察者之間的抽象耦合關係能單獨擴充套件以及重用
增加了靈活性
觀察者模式所做的工作就是在解耦,讓耦合的雙方都依賴於抽象,而不是依賴於具體。從而使得各自的變化都不會影響到另一邊的變化。
缺點
過度使用會導致物件與物件之間的聯絡弱化,會導致程式難以跟蹤維護和理解
前端常見設計模式8:中介者模式(Mediator Pattern)
在中介者模式中,中介者(Mediator)包裝了一系列物件相互作用的方式,使得這些物件不必直接相互作用,而是由中介者協調它們之間的互動,從而使它們可以鬆散偶合。當某些物件之間的作用發生改變時,不會立即影響其他的一些物件之間的作用,保證這些作用可以彼此獨立的變化。
中介者模式和觀察者模式有一定的相似性,都是一對多的關係,也都是集中式通訊,不同的是中介者模式是處理同級物件之間的互動,而觀察者模式是處理Observer和Subject之間的互動。中介者模式有些像婚戀中介,相親物件剛開始並不能直接交流,而是要通過中介去篩選匹配再決定誰和誰見面。
場景
例如購物車需求,存在商品選擇表單、顏色選擇表單、購買數量表單等等,都會觸發change事件,那麼可以通過中介者來轉發處理這些事件,實現各個事件間的解耦,僅僅維護中介者物件即可。
var goods = { //手機庫存
'red|32G': 3,
'red|64G': 1,
'blue|32G': 7,
'blue|32G': 6,
};
//中介者
var mediator = (function() {
var colorSelect = document.getElementById('colorSelect');
var memorySelect = document.getElementById('memorySelect');
var numSelect = document.getElementById('numSelect');
return {
changed: function(obj) {
switch(obj){
case colorSelect:
//TODO
break;
case memorySelect:
//TODO
break;
case numSelect:
//TODO
break;
}
}
}
})();
colorSelect.onchange = function() {
mediator.changed(this);
};
memorySelect.onchange = function() {
mediator.changed(this);
};
numSelect.onchange = function() {
mediator.changed(this);
};
聊天室裡
聊天室成員類:
function Member(name) {
this.name = name;
this.chatroom = null;
}
Member.prototype = {
// 傳送訊息
send: function (message, toMember) {
this.chatroom.send(message, this, toMember);
},
// 接收訊息
receive: function (message, fromMember) {
console.log(`${fromMember.name} to ${this.name}: ${message}`);
}
}
聊天室類:
function Chatroom() {
this.members = {};
}
Chatroom.prototype = {
// 增加成員
addMember: function (member) {
this.members[member.name] = member;
member.chatroom = this;
},
// 傳送訊息
send: function (message, fromMember, toMember) {
toMember.receive(message, fromMember);
}
}
測試一下:
const chatroom = new Chatroom();
const bruce = new Member('bruce');
const frank = new Member('frank');
chatroom.addMember(bruce);
chatroom.addMember(frank);
bruce.send('Hey frank', frank);
//輸出:bruce to frank: hello frank
優點
- 使各物件之間耦合鬆散,而且可以獨立地改變它們之間的互動
- 中介者和物件一對多的關係取代了物件之間的網狀多對多的關係
- 如果物件之間的複雜耦合度導致維護很困難,而且耦合度隨專案變化增速很快,就需要中介者重構程式碼
缺點
系統中會新增一箇中介者物件,因為物件之間互動的複雜性,轉移成了中介者物件的複雜性,使得中介者物件經常是巨大的。中介 者物件自身往往就是一個難以維護的物件。
前端常見設計模式9:訪問者模式(Visitor Pattern)
訪問者模式 是一種將演算法與物件結構分離的設計模式,通俗點講就是:訪問者模式讓我們能夠在不改變一個物件結構的前提下能夠給該物件增加新的邏輯,新增的邏輯儲存在一個獨立的訪問者物件中。訪問者模式常用於拓展一些第三方的庫和工具。
// 訪問者
class Visitor {
constructor() {}
visitConcreteElement(ConcreteElement) {
ConcreteElement.operation()
}
}
// 元素類
class ConcreteElement{
constructor() {
}
operation() {
console.log("ConcreteElement.operation invoked");
}
accept(visitor) {
visitor.visitConcreteElement(this)
}
}
// client
let visitor = new Visitor()
let element = new ConcreteElement()
elementA.accept(visitor)
訪問者模式的實現有以下幾個要素:
- Visitor Object:訪問者物件,擁有一個visit()方法
- Receiving Object:接收物件,擁有一個accept() 方法
- visit(receivingObj):用於Visitor接收一個Receiving Object
- accept(visitor):用於Receving Object接收一個Visitor,並通過呼叫Visitor的 visit() 為其提供獲取Receiving Object資料的能力
簡單的程式碼實現如下:
Receiving Object:
function Employee(name, salary) {
this.name = name;
this.salary = salary;
}
Employee.prototype = {
getSalary: function () {
return this.salary;
},
setSalary: function (salary) {
this.salary = salary;
},
accept: function (visitor) {
visitor.visit(this);
}
}
Visitor Object:
function Visitor() { }
Visitor.prototype = {
visit: function (employee) {
employee.setSalary(employee.getSalary() * 2);
}
}
驗證一下:
const employee = new Employee('bruce', 1000);
const visitor = new Visitor();
employee.accept(visitor);
console.log(employee.getSalary());//輸出:2000
場景
- 物件結構中物件對應的類很少改變,但經常需要在此物件結構上定義新的操作
- 需要對一個物件結構中的物件進行很多不同的並且不相關的操作,而需要避免讓這些操作"汙染"這些物件的類,也不希望在增加新操作時修改這些類。
優點
- 符合單一職責原則
- 優秀的擴充套件性
- 靈活性
缺點
- 具體元素對訪問者公佈細節,違反了迪米特原則
- 違反了依賴倒置原則,依賴了具體類,沒有依賴抽象。
- 具體元素變更比較困難