概念
設計模式是一套被反復使用的、多數人知曉的、經過分類編目的、代碼設計經驗的總結。使用設計模式是為了重用代碼、讓代碼更容易被他人理解、保證代碼可靠性。 毫無疑問,設計模式于己于他人于系統都是多贏的,設計模式使代碼編制真正工程化,設計模式是軟件工程的基石,如同大廈的一塊塊磚石一樣。
設計原則
-
S – Single Responsibility Principle 單一職責原則
- 一個程序只做好一件事
- 如果功能過于復雜就拆分開,每個部分保持獨立
-
O – OpenClosed Principle 開放/封閉原則
- 對擴展開放,對修改封閉
- 增加需求時,擴展新代碼,而非修改已有代碼
-
L – Liskov Substitution Principle 里氏替換原則
- 子類能覆蓋父類
- 父類能出現的地方子類就能出現
-
I – Interface Segregation Principle 接口隔離原則
- 保持接口的單一獨立
- 類似單一職責原則,這里更關注接口
-
D – Dependency Inversion Principle 依賴倒轉原則
- 面向接口編程,依賴于抽象而不依賴于具
- 使用方只關注接口而不關注具體類的實現
設計模式的類型
- 結構型模式(Structural Patterns): 通過識別系統中組件間的簡單關系來簡化系統的設計。
- 創(chuàng)建型模式(Creational Patterns): 處理對象的創(chuàng)建,根據實際情況使用合適的方式創(chuàng)建對象。常規(guī)的對象創(chuàng)建方式可能會導致設計上的問題,或增加設計的復雜度。創(chuàng)建型模式通過以某種方式控制對象的創(chuàng)建來解決問題。
- 行為型模式(Behavioral Patterns): 用于識別對象之間常見的交互模式并加以實現,如此,增加了這些交互的靈活性。
前端常見的設計模式
外觀模式(Facade Pattern)
外觀模式是最常見的設計模式之一,它為子系統中的一組接口提供一個統一的高層接口,使子系統更容易使用。簡而言之外觀設計模式就是把多個子系統中復雜邏輯進行抽象,從而提供一個更統一、更簡潔、更易用的API。很多我們常用的框架和庫基本都遵循了外觀設計模式,比如JQuery就把復雜的原生DOM操作進行了抽象和封裝,并消除了瀏覽器之間的兼容問題,從而提供了一個更高級更易用的版本。其實在平時工作中我們也會經常用到外觀模式進行開發(fā),只是我們不自知而已。
- 兼容瀏覽器事件綁定
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();
}
};
場景
- 設計初期,應該要有意識地將不同的兩個層分離,比如經典的三層結構,在數據訪問層和業(yè)務邏輯層、業(yè)務邏輯層和表示層之間建立外觀Facade
- 在開發(fā)階段,子系統往往因為不斷的重構演化而變得越來越復雜,增加外觀Facade可以提供一個簡單的接口,減少他們之間的依賴。
- 在維護一個遺留的大型系統時,可能這個系統已經很難維護了,這時候使用外觀Facade也是非常合適的,為系系統開發(fā)一個外觀Facade類,為設計粗糙和高度復雜的遺留代碼提供比較清晰的接口,讓新系統和Facade對象交互,Facade與遺留代碼交互所有的復雜工作。
優(yōu)點
- 減少系統相互依賴。
- 提高靈活性。
- 提高了安全性
缺點
- 不符合開閉原則,如果要改東西很麻煩,繼承重寫都不合適。
代理模式(Proxy Pattern)
是為一個對象提供一個代用品或占位符,以便控制對它的訪問
假設當A 在心情好的時候收到花,小明表白成功的幾率有
60%,而當A 在心情差的時候收到花,小明表白的成功率無限趨近于0。 小明跟A 剛剛認識兩天,還無法辨別A 什么時候心情好。如果不合時宜地把花送給A,花 被直接扔掉的可能性很大,這束花可是小明吃了7 天泡面換來的。 但是A 的朋友B 卻很了解A,所以小明只管把花交給B,B 會監(jiān)聽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()方法
優(yōu)點
- 代理模式能將代理對象與被調用對象分離,降低了系統的耦合度。代理模式在客戶端和目標對象之間起到一個中介作用,這樣可以起到保護目標對象的作用
- 代理對象可以擴展目標對象的功能;通過修改代理對象就可以了,符合開閉原則;
缺點
- 處理請求速度可能有差別,非直接訪問存在開銷
工廠模式(Factory Pattern)
工廠模式定義一個用于創(chuàng)建對象的接口,這個接口由子類決定實例化哪一個類。該模式使一個類的實例化延遲到了子類。而子類可以重寫接口方法以便創(chuàng)建的時候指定自己的對象類型。
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的時候就應該考慮是否用工廠模式;
- 需要依賴具體環(huán)境創(chuàng)建不同實例,這些實例都有相同的行為,這時候我們可以使用工廠模式,簡化實現的過程,同時也可以減少每種對象所需的代碼量,有利于消除對象間的耦合,提供更大的靈活性
優(yōu)點
- 創(chuàng)建對象的過程可能很復雜,但我們只需要關心創(chuàng)建結果。
- 構造函數和創(chuàng)建者分離, 符合“開閉原則”
- 一個調用者想創(chuàng)建一個對象,只要知道其名稱就可以了。
- 擴展性高,如果想增加一個產品,只要擴展一個工廠類就可以。
缺點
- 添加新產品時,需要編寫新的具體產品類,一定程度上增加了系統的復雜度
- 考慮到系統的可擴展性,需要引入抽象層,在客戶端代碼中均使用抽象層進行定義,增加了系統的抽象性和理解難度
什么時候不用
- 當被應用到錯誤的問題類型上時,這一模式會給應用程序引入大量不必要的復雜性.除非為創(chuàng)建對象提供一個接口是我們編寫的庫或者框架的一個設計上目標,否則我會建議使用明確的構造器,以避免不必要的開銷。
- 由于對象的創(chuàng)建過程被高效的抽象在一個接口后面的事實,這也會給依賴于這個過程可能會有多復雜的單元測試帶來問題。
單例模式(Singleton Pattern)
顧名思義,單例模式中Class的實例個數最多為1。當需要一個對象去貫穿整個系統執(zhí)行某些任務時,單例模式就派上了用場。而除此之外的場景盡量避免單例模式的使用,因為單例模式會引入全局狀態(tài),而一個健康的系統應該避免引入過多的全局狀態(tài)。
實現單例模式需要解決以下幾個問題:
- 如何確定Class只有一個實例?
- 如何簡便的訪問Class的唯一實例?
- Class如何控制實例化的過程?
- 如何將Class的實例個數限制為1?
我們一般通過實現以下兩點來解決上述問題:
- 隱藏Class的構造函數,避免多次實例化
- 通過暴露一個 getInstance() 方法來創(chuàng)建/獲取唯一實例
Javascript中單例模式可以通過以下方式實現:
// 單例構造器
const FooServiceSingleton = (function () {
// 隱藏的Class的構造函數
function FooService() {}
// 未初始化的單例對象
let fooService;
return {
// 創(chuàng)建/獲取單例對象的函數
getInstance: function () {
if (!fooService) {
fooService = new FooService();
}
return fooService;
}
}
})();
實現的關鍵點有:
- 使用 IIFE創(chuàng)建局部作用域并即時執(zhí)行;
-
getInstance()為一個 閉包 ,使用閉包保存局部作用域中的單例對象并返回。
我們可以驗證下單例對象是否創(chuàng)建成功:
const fooService1 = FooServiceSingleton.getInstance();
const fooService2 = FooServiceSingleton.getInstance();
console.log(fooService1 === fooService2); // true
場景例子
- 定義命名空間和實現分支型方法
- 登錄框
- vuex 和 redux中的store
優(yōu)點
- 劃分命名空間,減少全局變量
- 增強模塊性,把自己的代碼組織在一個全局變量名下,放在單一位置,便于維護
- 且只會實例化一次。簡化了代碼的調試和維護
缺點
- 由于單例模式提供的是一種單點訪問,所以它有可能導致模塊間的強耦合
- 從而不利于單元測試。無法單獨測試一個調用了來自單例的方法的類,而只能把它與那個單例作為一
- 個單元一起測試。
策略模式(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>
場景例子
- 如果在一個系統里面有許多類,它們之間的區(qū)別僅在于它們的'行為',那么使用策略模式可以動態(tài)地讓一個對象在許多行為中選擇一種行為。
- 一個系統需要動態(tài)地在幾種算法中選擇一種。
- 表單驗證
優(yōu)點
- 利用組合、委托、多態(tài)等技術和思想,可以有效的避免多重條件選擇語句
- 提供了對開放-封閉原則的完美支持,將算法封裝在獨立的strategy中,使得它們易于切換,理解,易于擴展
- 利用組合和委托來讓Context擁有執(zhí)行算法的能力,這也是繼承的一種更輕便的代替方案
缺點
- 會在程序中增加許多策略類或者策略對象
- 要使用策略模式,必須了解所有的strategy,必須了解各個strategy之間的不同點,這樣才能選擇一個合適的strategy
迭代器模式(Iterator Pattern)
如果你看到這,ES6中的迭代器 Iterator 相信你還是有點印象的,上面第60條已經做過簡單的介紹。迭代器模式簡單的說就是提供一種方法順序一個聚合對象中各個元素,而又不暴露該對象的內部表示。
迭代器模式解決了以下問題:
- 提供一致的遍歷各種數據結構的方式,而不用了解數據的內部結構
- 提供遍歷容器(集合)的能力而無需改變容器的接口
一個迭代器通常需要實現以下接口:
- 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提供了更簡單的迭代循環(huán)語法 for...of,使用該語法的前提是操作對象需要實現 可迭代協議(The iterable protocol),簡單說就是該對象有個Key為 Symbol.iterator 的方法,該方法返回一個iterator對象。
比如我們實現一個 Range 類用于在某個數字區(qū)間進行迭代:
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
觀察者模式(Observer Pattern)
觀察者模式又稱發(fā)布-訂閱模式(Publish/Subscribe Pattern),是我們經常接觸到的設計模式,日常生活中的應用也比比皆是,比如你訂閱了某個博主的頻道,當有內容更新時會收到推送;又比如JavaScript中的事件訂閱響應機制。觀察者模式的思想用一句話描述就是:被觀察對象(subject)維護一組觀察者(observer),當被觀察對象狀態(tài)改變時,通過調用觀察者的某個方法將這些變化通知到觀察者。
觀察者模式中Subject對象一般需要實現以下API:
- subscribe(): 接收一個觀察者observer對象,使其訂閱自己
- unsubscribe(): 接收一個觀察者observer對象,使其取消訂閱自己
- fire(): 觸發(fā)事件,通知到所有觀察者
用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;
})
},
// 事件觸發(fā)
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 響應式
優(yōu)點
- 支持簡單的廣播通信,自動通知所有已經訂閱過的對象
- 目標對象與觀察者之間的抽象耦合關系能單獨擴展以及重用
- 增加了靈活性
- 觀察者模式所做的工作就是在解耦,讓耦合的雙方都依賴于抽象,而不是依賴于具體。從而使得各自的變化都不會影響到另一邊的變化。
缺點
- 過度使用會導致對象與對象之間的聯系弱化,會導致程序難以跟蹤維護和理解
中介者模式(Mediator Pattern)
在中介者模式中,中介者(Mediator)包裝了一系列對象相互作用的方式,使得這些對象不必直接相互作用,而是由中介者協調它們之間的交互,從而使它們可以松散偶合。當某些對象之間的作用發(fā)生改變時,不會立即影響其他的一些對象之間的作用,保證這些作用可以彼此獨立的變化。
中介者模式和觀察者模式有一定的相似性,都是一對多的關系,也都是集中式通信,不同的是中介者模式是處理同級對象之間的交互,而觀察者模式是處理Observer和Subject之間的交互。中介者模式有些像婚戀中介,相親對象剛開始并不能直接交流,而是要通過中介去篩選匹配再決定誰和誰見面。
場景
- 例如購物車需求,存在商品選擇表單、顏色選擇表單、購買數量表單等等,都會觸發(fā)change事件,那么可以通過中介者來轉發(fā)處理這些事件,實現各個事件間的解耦,僅僅維護中介者對象即可。
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 = {
// 發(fā)送消息
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;
},
// 發(fā)送消息
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
優(yōu)點
- 使各對象之間耦合松散,而且可以獨立地改變它們之間的交互
- 中介者和對象一對多的關系取代了對象之間的網狀多對多的關系
- 如果對象之間的復雜耦合度導致維護很困難,而且耦合度隨項目變化增速很快,就需要中介者重構代碼
缺點
- 系統中會新增一個中介者對象,因為對象之間交互的復雜性,轉移成了中介者對象的復雜性,使得中介者對象經常是巨大的。中介 者對象自身往往就是一個難以維護的對象。
訪問者模式(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
場景
- 對象結構中對象對應的類很少改變,但經常需要在此對象結構上定義新的操作
- 需要對一個對象結構中的對象進行很多不同的并且不相關的操作,而需要避免讓這些操作"污染"這些對象的類,也不希望在增加新操作時修改這些類。
優(yōu)點
- 符合單一職責原則
- 優(yōu)秀的擴展性
- 靈活性
缺點
- 具體元素對訪問者公布細節(jié),違反了迪米特原則
- 違反了依賴倒置原則,依賴了具體類,沒有依賴抽象。
- 具體元素變更比較困難
相關參考資料:
JavaScript設計模式es6(23種)
《前端面試之道》
《JavaScript 設計模式》
《JavaScript 中常見設計模式整理》