作者:MudOnTire
https://segmentfault.com/a/1190000022396503
設計模式是對軟件設計開發過程中反復出現的某類問題的通用解決方案。設計模式更多的是指導思想和方法論,而不是現成的代碼,當然每種設計模式都有每種語言中的具體實現方式。學習設計模式更多的是理解各種模式的內在思想和解決的問題,畢竟這是前人無數經驗總結成的最佳實踐,而代碼實現則是對加深理解的輔助。
設計模式可以分為三大類:
以上定義非常的抽象和晦澀,對于我們初學者并沒有太多幫助,要了解這些設計模式真正的作用和價值還是需要通過實踐去加以理解。這三大類設計模式又可以分成更多的小類,如下圖:
下面我們選擇一些在前端開發過程中常見的模式進行一一講解。
外觀模式是最常見的設計模式之一,它為子系統中的一組接口提供一個統一的高層接口,使子系統更容易使用。簡而言之外觀設計模式就是把多個子系統中復雜邏輯進行抽象,從而提供一個更統一、更簡潔、更易用的API。很多我們常用的框架和庫基本都遵循了外觀設計模式,比如JQuery就把復雜的原生DOM操作進行了抽象和封裝,并消除了瀏覽器之間的兼容問題,從而提供了一個更高級更易用的版本。其實在平時工作中我們也會經常用到外觀模式進行開發,只是我們不自知而已。
比如,我們可以應用外觀模式封裝一個統一的DOM元素事件綁定/取消方法,用于兼容不同版本的瀏覽器和更方便的調用:
// 綁定事件
function addEvent(element, event, handler) {
if (element.addEventListener) {
element.addEventListener(event, handler, false);
} else if (element.attachEvent) {
element.attachEvent('on' + event, handler);
} else {
element['on' + event] = fn;
}
}
// 取消綁定
function removeEvent(element, event, handler) {
if (element.removeEventListener) {
element.removeEventListener(event, handler, false);
} else if (element.detachEvent) {
element.detachEvent('on' + event, handler);
} else {
element['on' + event] = null;
}
}
首先,一切皆可代理,不管是在實現世界還是計算機世界?,F實世界中買房有中介、打官司有律師、投資有經紀人,他們都是代理,由他們幫你處理由于你缺少時間或者專業技能而無法完成的事務。類比到計算機領域,代理也是一樣的作用,當訪問一個對象本身的代價太高(比如太占內存、初始化時間太長等)或者需要增加額外的邏輯又不修改對象本身時便可以使用代理。ES6中也增加了 Proxy 的功能。
歸納一下,代理模式可以解決以下的問題:
要實現代理模式需要三部分:
Real Subject
:真實對象Proxy
:代理對象Subject
接口:Real Subject 和 Proxy都需要實現的接口,這樣Proxy才能被當成Real Subject的“替身”使用比如有一個股票價格查詢接口,調用這個接口需要比較久的時間(用 setTimeout
模擬2s的調用時間):
StockPriceAPI:
function StockPriceAPI() {
// Subject Interface實現
this.getValue = function (stock, callback) {
console.log('Calling external API ... ');
setTimeout(() => {
switch (stock) {
case 'GOOGL':
callback('$1265.23');
break;
case 'AAPL':
callback('$287.05');
break;
case 'MSFT':
callback('$173.70');
break;
default:
callback('');
}
}, 2000);
}
}
我們不希望每次都去請求遠程接口,而是增加緩存機制,當有緩存的時候就直接從緩存中獲取,否則再去請求遠程接口。我們可以通過一個proxy來實現:
StockPriceAPIProxy:
function StockPriceAPIProxy() {
// 緩存對象
this.cache = {};
// 真實API對象
this.realAPI = new StockPriceAPI();
// Subject Interface實現
this.getValue = function (stock, callback) {
const cachedPrice = this.cache[stock];
if (cachedPrice) {
console.log('Got price from cache');
callback(cachedPrice);
} else {
this.realAPI.getValue(stock, (price) => {
this.cache[stock] = price;
callback(price);
});
}
}
}
注意,Proxy需要和真實對象一樣實現 getValue()
方法,getValue()
就屬于 Subject 接口。
測試一下:
const api = new StockPriceAPIProxy();
api.getValue('GOOGL', (price) => { console.log(price) });
api.getValue('AAPL', (price) => { console.log(price) });
api.getValue('MSFT', (price) => { console.log(price) });
setTimeout(() => {
api.getValue('GOOGL', (price) => { console.log(price) });
api.getValue('AAPL', (price) => { console.log(price) });
api.getValue('MSFT', (price) => { console.log(price) });
}, 3000)
輸出:
Calling external API ...
Calling external API ...
Calling external API ...
$1265.23
$287.05
$173.70
Got price from cache
$1265.23
Got price from cache
$287.05
Got price from cache
$173.70
現實生活中的工廠按照既定程序制造產品,隨著生產原料和流程不同生產出來的產品也會有區別。應用到軟件工程的領域,工廠可以看成是一個制造其他對象的對象,制造出的對象也會隨著傳入工廠對象參數的不同而有所區別。
什么場景適合應用工廠模式而不是直接 new
一個對象呢?當構造函數過多不方便管理,且需要創建的對象之間存在某些關聯(有同一個父類、實現同一個接口等)時,不妨使用工廠模式。工廠模式提供一種集中化、統一化的方式,避免了分散創建對象導致的代碼重復、靈活性差的問題。
以上圖為例,我們構造一個簡單的汽車工廠來生產汽車:
// 汽車構造函數
function SuzukiCar(color) {
this.color = color;
this.brand = 'Suzuki';
}
// 汽車構造函數
function HondaCar(color) {
this.color = color;
this.brand = 'Honda';
}
// 汽車構造函數
function BMWCar(color) {
this.color = color;
this.brand = 'BMW';
}
// 汽車品牌枚舉
const BRANDS = {
suzuki: 1,
honda: 2,
bmw: 3
}
/**
* 汽車工廠
*/
function CarFactory() {
this.create = function (brand, color) {
switch (brand) {
case BRANDS.suzuki:
return new SuzukiCar(color);
case BRANDS.honda:
return new HondaCar(color);
case BRANDS.bmw:
return new BMWCar(color);
default:
break;
}
}
}
測試一下:
const carFactory = new CarFactory();
const cars = [];
cars.push(carFactory.create(BRANDS.suzuki, 'brown'));
cars.push(carFactory.create(BRANDS.honda, 'grey'));
cars.push(carFactory.create(BRANDS.bmw, 'red'));
function say() {
console.log(`Hi, I am a ${this.color} ${this.brand} car`);
}
for (const car of cars) {
say.call(car);
}
輸出:
Hi, I am a brown Suzuki car
Hi, I am a grey Honda car
Hi, I am a red BMW car
使用工廠模式之后,不再需要重復引入一個個構造函數,只需要引入工廠對象就可以方便的創建各類對象。
顧名思義,單例模式中Class的實例個數最多為1。當需要一個對象去貫穿整個系統執行某些任務時,單例模式就派上了用場。而除此之外的場景盡量避免單例模式的使用,因為單例模式會引入全局狀態,而一個健康的系統應該避免引入過多的全局狀態。
實現單例模式需要解決以下幾個問題:
我們一般通過實現以下兩點來解決上述問題:
getInstance()
方法來創建/獲取唯一實例Javascript中單例模式可以通過以下方式實現:
// 單例構造器
const FooServiceSingleton = (function () {
// 隱藏的Class的構造函數
function FooService() {}
// 未初始化的單例對象
let fooService;
return {
// 創建/獲取單例對象的函數
getInstance: function () {
if (!fooService) {
fooService = new FooService();
}
return fooService;
}
}
})();
實現的關鍵點有:1. 使用 IIFE創建局部作用域并即時執行;2. getInstance()
為一個 閉包 ,使用閉包保存局部作用域中的單例對象并返回。
我們可以驗證下單例對象是否創建成功:
const fooService1 = FooServiceSingleton.getInstance();
const fooService2 = FooServiceSingleton.getInstance();
console.log(fooService1 === fooService2); // true
策略模式簡單描述就是:對象有某個行為,但是在不同的場景中,該行為有不同的實現算法。比如每個人都要“交個人所得稅”,但是“在美國交個人所得稅”和“在中國交個人所得稅”就有不同的算稅方法。最常見的使用策略模式的場景如登錄鑒權,鑒權算法取決于用戶的登錄方式是手機、郵箱或者第三方的微信登錄等等,而且登錄方式也只有在運行時才能獲取,獲取到登錄方式后再動態的配置鑒權策略。所有這些策略應該實現統一的接口,或者說有統一的行為模式。Node 生態里著名的鑒權庫 Passport.js API的設計就應用了策略模式。
還是以登錄鑒權的例子我們仿照 passport.js 的思路通過代碼來理解策略模式:
/**
* 登錄控制器
*/
function LoginController() {
this.strategy = undefined;
this.setStrategy = function (strategy) {
this.strategy = strategy;
this.login = this.strategy.login;
}
}
/**
* 用戶名、密碼登錄策略
*/
function LocalStragegy() {
this.login = ({ username, password }) => {
console.log(username, password);
// authenticating with username and password...
}
}
/**
* 手機號、驗證碼登錄策略
*/
function PhoneStragety() {
this.login = ({ phone, verifyCode }) => {
console.log(phone, verifyCode);
// authenticating with hone and verifyCode...
}
}
/**
* 第三方社交登錄策略
*/
function SocialStragety() {
this.login = ({ id, secret }) => {
console.log(id, secret);
// authenticating with id and secret...
}
}
const loginController = new LoginController();
// 調用用戶名、密碼登錄接口,使用LocalStrategy
app.use('/login/local', function (req, res) {
loginController.setStrategy(new LocalStragegy());
loginController.login(req.body);
});
// 調用手機、驗證碼登錄接口,使用PhoneStrategy
app.use('/login/phone', function (req, res) {
loginController.setStrategy(new PhoneStragety());
loginController.login(req.body);
});
// 調用社交登錄接口,使用SocialStrategy
app.use('/login/social', function (req, res) {
loginController.setStrategy(new SocialStragety());
loginController.login(req.body);
});
從以上示例可以得出使用策略模式有以下優勢:
方便在運行時切換算法和策略
代碼更簡潔,避免使用大量的條件判斷
關注分離,每個strategy類控制自己的算法邏輯,strategy和其使用者之間也相互獨立
ES6中的迭代器 Iterator 相信大家都不陌生,迭代器用于遍歷容器(集合)并訪問容器中的元素,而且無論容器的數據結構是什么(Array、Set、Map等),迭代器的接口都應該是一樣的,都需要遵循 迭代器協議。
迭代器模式解決了以下問題:
一個迭代器通常需要實現以下接口:
hasNext()
:判斷迭代是否結束,返回Booleannext()
:查找并返回下一個元素為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
觀察者模式又稱發布訂閱模式(Publish/Subscribe Pattern),是我們經常接觸到的設計模式,日常生活中的應用也比比皆是,比如你訂閱了某個博主的頻道,當有內容更新時會收到推送;又比如JavaScript中的事件訂閱響應機制。觀察者模式的思想用一句話描述就是:被觀察對象(subject)維護一組觀察者(observer),當被觀察對象狀態改變時,通過調用觀察者的某個方法將這些變化通知到觀察者。
比如給DOM元素綁定事件的 addEventListener()
方法:
target.addEventListener(type, listener [, options]);
Target就是被觀察對象Subject,listener就是觀察者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!
在中介者模式中,中介者(Mediator)包裝了一系列對象相互作用的方式,使得這些對象不必直接相互作用,而是由中介者協調它們之間的交互,從而使它們可以松散偶合。當某些對象之間的作用發生改變時,不會立即影響其他的一些對象之間的作用,保證這些作用可以彼此獨立的變化。
中介者模式和觀察者模式有一定的相似性,都是一對多的關系,也都是集中式通信,不同的是中介者模式是處理同級對象之間的交互,而觀察者模式是處理Observer和Subject之間的交互。中介者模式有些像婚戀中介,相親對象剛開始并不能直接交流,而是要通過中介去篩選匹配再決定誰和誰見面。中介者模式比較常見的應用比如聊天室,聊天室里面的人之間并不能直接對話,而是通過聊天室這一媒介進行轉發。一個簡易的聊天室模型可以實現如下:
聊天室成員類:
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
這只是一個最簡單的聊天室模型,真正的聊天室還可以加入更多的功能,比如敏感信息攔截、一對多聊天、廣播等。得益于中介者模式,Member不需要處理和聊天相關的復雜邏輯,而是全部交給Chatroom,有效的實現了關注分離。
訪問者模式是一種將算法與對象結構分離的設計模式,通俗點講就是:訪問者模式讓我們能夠在不改變一個對象結構的前提下能夠給該對象增加新的邏輯,新增的邏輯保存在一個獨立的訪問者對象中。訪問者模式常用于拓展一些第三方的庫和工具。
訪問者模式的實現有以下幾個要素:
Visitor Object
:訪問者對象,擁有一個 visit()
方法Receiving Object
:接收對象,擁有一個 accept()
方法visit(receivingObj)
:用于Visitor接收一個Receiving Objectaccept(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
本文僅僅初步探討了部分設計模式在前端領域的應用或者實現,旨在消除大部分同學心中對設計模式的陌生感和畏懼感?,F有的設計模式就有大約50中,常見的也有20種左右,所以設計模式是一門宏大而深奧的學問需要我們不斷的去學習和在實踐中總結。本文所涉及到的9種只占了一小部分,未涉及到的模式里面肯定也有對前端開發有價值的,希望以后有機會能一一補上。謝謝閱讀!
本文源碼輕參考:https://github.com/MudOnTire/frontend-design-patterns
最多閱讀