掌握前端5大常用設(shè)計(jì)模式,瞬間高大上
今天主要介紹一下我們平常會經(jīng)常用到的設(shè)計(jì)模式,設(shè)計(jì)模式總的來說有23種,而設(shè)計(jì)模式在前端中又該怎么運(yùn)用呢,接下來主要對比較前端中常見的設(shè)計(jì)模式做一個(gè)介紹。
設(shè)計(jì)模式的定義
設(shè)計(jì)模式是在面向?qū)ο筌浖O(shè)計(jì)過程中針對特定問題的簡潔而優(yōu)雅的解決方案。在不同的編程語言中,對設(shè)計(jì)模式的實(shí)現(xiàn)其實(shí)是可能會有區(qū)別的。比如java和javascript,在Java這種靜態(tài)編譯型語言中,無法動態(tài)地給已存在的對象添加職責(zé),所以一般通過包裝類的方式來實(shí)現(xiàn)裝飾者模式。但在JavaScript這種動態(tài)解釋型語言中,給對象動態(tài)添加職責(zé)是再簡單不過的事情。這就造成了JavaScript語言的裝飾者模式不再關(guān)注于給對象動態(tài)添加職責(zé),而是關(guān)注于給函數(shù)動態(tài)添加職責(zé)。本篇將介紹以下幾個(gè)比較常見的設(shè)計(jì)模式:
- 工廠模式
- 單例模式
- 代理模式
- 觀察者模式
- 策略模式
一、工廠模式
工廠模式是用來創(chuàng)建對象的一種最常用的設(shè)計(jì)模式,不暴露創(chuàng)建對象的具體邏輯,而是將將邏輯封裝在一個(gè)函數(shù)中,那么這個(gè)函數(shù)就可以被視為一個(gè)工廠,工廠模式根據(jù)抽象程度的不同可以分為:簡單工廠,工廠方法和抽象工廠,接下來,將對簡單工廠和工廠方法在JavaScript中的運(yùn)用舉個(gè)簡單的例子:
1. 簡單工廠
簡單工廠模式又叫靜態(tài)工廠模式,由一個(gè)工廠對象決定創(chuàng)建某一種產(chǎn)品對象類的實(shí)例,主要用來創(chuàng)建同一類對象
比如說,在實(shí)際的項(xiàng)目中,我們常常需要根據(jù)用戶的權(quán)限來渲染不同的頁面,高級權(quán)限的用戶所擁有的頁面有些是無法被低級權(quán)限的用戶所查看,所以我們可以在不同權(quán)限等級用戶的構(gòu)造函數(shù)中,保存該用戶能夠看到的頁面。
- let UserFactory = function (role) {
- function SuperAdmin() {
- this.name = "超級管理員",
- this.viewPage = ['首頁', '用戶管理', '訂單管理', '應(yīng)用管理', '權(quán)限管理']
- }
- function Admin() {
- this.name = "管理員",
- this.viewPage = ['首頁', '訂單管理', '應(yīng)用管理']
- }
- 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('參數(shù)錯(cuò)誤, 可選參數(shù):superAdmin、admin、user');
- }
- }
- //調(diào)用
- let superAdmin = UserFactory('superAdmin');
- let admin = UserFactory('admin')
- let normalUser = UserFactory('user')
總結(jié):在上面的例子中,UserFactory就是一個(gè)簡單工廠,在該函數(shù)中有3個(gè)構(gòu)造函數(shù)分別對應(yīng)不同的權(quán)限的用戶,當(dāng)我們調(diào)用工廠函數(shù)時(shí),只需要傳遞superAdmin, admin, user這三個(gè)可選參數(shù)中的一個(gè)獲取對應(yīng)的實(shí)例對象
- 優(yōu)點(diǎn):簡單工廠的優(yōu)點(diǎn)在于,你只需要一個(gè)正確的參數(shù),就可以獲取到你所需要的對象,而無需知道其創(chuàng)建的具體細(xì)節(jié);
- 缺點(diǎn):在函數(shù)內(nèi)包含了所有對象的創(chuàng)建邏輯(構(gòu)造函數(shù))和判斷邏輯的代碼,每增加新的構(gòu)造函數(shù)還需要修改判斷邏輯代碼,我們的對象不是上面的3個(gè)而是30個(gè)或更多時(shí),這個(gè)函數(shù)會成為一個(gè)龐大的超級函數(shù),便得難以維護(hù),簡單工廠只能作用于創(chuàng)建的對象數(shù)量較少,對象的創(chuàng)建邏輯不復(fù)雜時(shí)使用;
2. 工廠方法
工廠方法模式的本意是將實(shí)際創(chuàng)建對象的工作推遲到子類中,這樣核心類就變成了抽象類,但是在JavaScript中很難像傳統(tǒng)面向?qū)ο竽菢尤?shí)現(xiàn)創(chuàng)建抽象類,所以在JavaScript中我們只需要參考它的核心思想即可,我們可以將工廠方法看作是一個(gè)實(shí)例化對象的工廠類
比如說上面的例子,我們用工廠方法可以這樣寫,工廠方法我們只把它看作是一個(gè)實(shí)例化對象的工廠,它只做實(shí)例化對象這一件事情,我們采用安全模式創(chuàng)建對象
- //安全模式創(chuàng)建的工廠方法函數(shù)
- let UserFactory = function(role) {
- if(this instanceof UserFactory) {
- var s = new this[role]();
- return s;
- } else {
- return new UserFactory(role);
- }
- }
- //工廠方法函數(shù)的原型中設(shè)置所有對象的構(gòu)造函數(shù)
- UserFactory.prototype = {
- SuperAdmin: function() {
- this.name = "超級管理員",
- this.viewPage = ['首頁', '用戶管理', '訂單管理', '應(yīng)用管理', '權(quán)限管理']
- },
- Admin: function() {
- this.name = "管理員",
- this.viewPage = ['首頁', '訂單管理', '應(yīng)用管理']
- },
- NormalUser: function() {
- this.name = '普通用戶',
- this.viewPage = ['首頁', '訂單管理']
- }
- }
- //調(diào)用
- let superAdmin = UserFactory('SuperAdmin');
- let admin = UserFactory('Admin')
- let normalUser = UserFactory('NormalUser')
總結(jié):在簡單工廠中,如果我們新增加一個(gè)用戶類型,需要修改兩個(gè)地方的代碼,一個(gè)是增加新的用戶構(gòu)造函數(shù),一個(gè)是在邏輯判斷中增加對新的用戶的判斷,而在抽象工廠方法中,我們只需要在UserFactory.prototype中添加就可以啦。
二、單例模式
定義:是保證一個(gè)類只有一個(gè)實(shí)例,并且提供一個(gè)訪問它的全局訪問點(diǎn)。
需求:一些對象我們往往只需要一個(gè),比如線程池、全局緩存、瀏覽器中的window對象、登錄浮窗等。
實(shí)現(xiàn):用一個(gè)變量標(biāo)識當(dāng)前是否已經(jīng)為某個(gè)類創(chuàng)建過對象,如果是,則在下一次獲取這個(gè)類的實(shí)例時(shí),直接返回之前創(chuàng)建的對象。
優(yōu)點(diǎn):
- 可以用來劃分命名空間,減少全局變量的數(shù)量
- 可以被實(shí)例化,且實(shí)例化一次,再次實(shí)例化生成的也是***個(gè)實(shí)例
下面舉個(gè)例子,在js中,我們可以使用閉包來創(chuàng)建實(shí)現(xiàn)這種模式:
- var single = (function(){
- var unique;
- function getInstance(){
- // 如果該實(shí)例存在,則直接返回,否則就對其實(shí)例化
- if( unique === undefined ){
- unique = new Construct();
- }
- return unique;
- }
- function Construct(){
- // ... 生成單例的構(gòu)造函數(shù)的代碼
- }
- return {
- getInstance : getInstance
- }
- })();
總結(jié):在上面的代碼中,我們可以使用single.getInstance來獲取到單例,并且每次調(diào)用均獲取到同一個(gè)單例,在我們平時(shí)的開發(fā)中,我們也經(jīng)常會用到這種模式,比如當(dāng)我們單擊登錄按鈕的時(shí)候,頁面中會出現(xiàn)一個(gè)登錄框,而這個(gè)浮窗是唯一的,無論單擊多少次登錄按鈕,這個(gè)浮窗只會被創(chuàng)建一次,因此這個(gè)登錄浮窗就適合用單例模式。
三、代理模式
代理模式主要是為其他對象提供一種代理以控制對這個(gè)對象的訪問,主要解決在直接訪問對象時(shí)帶來的問題,比如說:要訪問的對象在遠(yuǎn)程的機(jī)器上,在面向?qū)ο笙到y(tǒng)中,有些對象由于某些原因(比如對象創(chuàng)建開銷很大,或者某些操作需要安全控制,或者需要進(jìn)程外的訪問),直接訪問會給使用者或者系統(tǒng)結(jié)構(gòu)帶來很多麻煩,我們可以在訪問此對象時(shí)加上一個(gè)對此對象的訪問層。
代理模式最基本的形式是對訪問進(jìn)行控制,代理對象和另一個(gè)對象(本體)實(shí)現(xiàn)的是同樣的接口,實(shí)際上工作還是本體在做,它才是負(fù)責(zé)執(zhí)行所分派的任務(wù)的那個(gè)對象或類,代理對象所做的不外乎節(jié)制對本體的訪問,代理對象并不會在另一對象的基礎(chǔ)上添加方法或修改其方法,也不會簡化那個(gè)對象的接口,它實(shí)現(xiàn)的接口與本體完全相同,所有對它進(jìn)行的方法調(diào)用都會被傳遞給本體。
- (function(){
- // 示例代碼
- // 目標(biāo)對象,是真正被代理的對象
- function Subject(){}
- Subject.prototype.request = function(){};
- /**
- * 代理對象
- * @param {Object} realSubject [持有被代理的具體的目標(biāo)對象]
- */
- function Proxy(realSubject){
- this.realSubject = readSubject;
- }
- Proxy.prototype.request = function(){
- this.realSubject.request();
- };
- }());
總結(jié):在上面的代碼中,Proxy可以控制對真正被代理對象的一個(gè)訪問,在代理模式中,比較常見的就是虛擬代理,虛擬代理用于控制對那種創(chuàng)建開銷很大的本體的訪問,它會把本體的實(shí)例化推遲到有方法被調(diào)用的時(shí)候,比如說,現(xiàn)在我們假設(shè)PublicLibrary的實(shí)例化很慢,不能在網(wǎng)頁加載的時(shí)候立即完成,我們可以為其創(chuàng)建一個(gè)虛擬代理,讓它把PublicLibrary的實(shí)例化推遲到必要的時(shí)候,比如說我們在前端中經(jīng)常用到的圖片懶加載,就可以用虛擬代理;
四、觀察者模式
如果大家學(xué)過一些像vue,react這些框架,相信大家對觀察者模式一定很熟悉,現(xiàn)在很多mvvm框架都用到了觀察者模式這個(gè)思想,觀察者模式又叫做發(fā)布—訂閱模式,它定義對象間的一種一對多的依賴關(guān)系,當(dāng)一個(gè)對象的狀態(tài)發(fā)生改變時(shí),所有依賴于它的對象都將得到通知和更新,觀察者模式提供了一個(gè)訂閱模型,其中對象訂閱事件并在發(fā)生時(shí)得到通知,這種模式是事件驅(qū)動的編程基石,它有利益于良好的面向?qū)ο蟮脑O(shè)計(jì)
定義:對象間的一種一對多的依賴關(guān)系。
需求:當(dāng)一個(gè)對象的狀態(tài)發(fā)生變化時(shí),所有依賴于他的對象都將得到通知。
優(yōu)點(diǎn):時(shí)間上的解耦,對象之間的解耦。
實(shí)現(xiàn):
- 指定好誰充當(dāng)發(fā)布者;
- 給發(fā)布者添加一個(gè)緩存列表,用于存放回調(diào)函數(shù)以便通知訂閱者;
- 發(fā)布消息的時(shí)候,發(fā)布者會遍歷這個(gè)緩存列表,依次觸發(fā)里面存放的訂閱者回調(diào)函數(shù)。
下面舉個(gè)例子,比如我們給頁面中的一個(gè)dom節(jié)點(diǎn)綁定一個(gè)事件,其實(shí)就可以看做是一種觀察者模式:
- document.body.addEventListener("click", function() {
- alert("Hello World")
- },false )
- document.body.click() //模擬用戶點(diǎn)擊
總結(jié):在上面的例子中,需要監(jiān)聽用戶點(diǎn)擊 document.body 的動作,但是我們是沒辦法預(yù)知用戶將在什么時(shí)候點(diǎn)擊的,因此我們訂閱了 document.body 的 click 事件,當(dāng) body 節(jié)點(diǎn)被點(diǎn)擊時(shí),body 節(jié)點(diǎn)便會向訂閱者發(fā)布 "Hello World" 消息。
五、策略模式
策略模式指的是定義一些列的算法,把他們一個(gè)個(gè)封裝起來,目的就是將算法的使用與算法的實(shí)現(xiàn)分離開來,避免多重判斷條件,更具有擴(kuò)展性。
下面也是舉個(gè)例子,現(xiàn)在超市有活動,vip為5折,老客戶3折,普通顧客沒折,計(jì)算***需要支付的金額,如果不使用策略模式,我們的代碼可能和下面一樣:
- 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; //其他都全價(jià)
- }
- }
在上面的代碼中,我們需要很多個(gè)判斷,如果有很多優(yōu)惠,我們又需要添加很多判斷,這里已經(jīng)違背了剛才說的設(shè)計(jì)模式的六大原則中的開閉原則了,如果使用策略模式,我們的代碼可以這樣寫:
- // 對于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 + ' 的結(jié)賬價(jià)為: ' + this.strategy.getPrice(this.price));
- }
- var context = new Context();
- var vip = new vipPrice();
- context.set ('vip客戶', vip, 200);
- context.getResult(); // vip客戶 的結(jié)賬價(jià)為: 100
- var old = new oldPrice();
- context.set ('老客戶', old, 200);
- context.getResult(); // 老客戶 的結(jié)賬價(jià)為: 60
- var Price = new Price();
- context.set ('普通客戶', Price, 200);
- context.getResult(); // 普通客戶 的結(jié)賬價(jià)為: 200
總結(jié):在上面的代碼中,通過策略模式,使得客戶的折扣與算法解藕,又使得修改跟擴(kuò)展能獨(dú)立的進(jìn)行,不影到客戶端或其他算法的使用。
當(dāng)我們的代碼中有很多個(gè)判斷分支,每一個(gè)條件分支都會引起該“類”的特定行為以不同的方式作出改變,這個(gè)時(shí)候就可以使用策略模式,可以改進(jìn)我們代碼的質(zhì)量,也更好的可以進(jìn)行單元測試。