前端需要掌握的設(shè)計(jì)模式
王君,微醫(yī)云服務(wù)團(tuán)隊(duì)前端工程師,一個切菜比切圖還快的碼農(nóng)。
烹飪有菜譜,游戲有攻略,各個行業(yè)都存在一些快捷又高效的“套路”。而編程的“套路”就是設(shè)計(jì)模式。
提到設(shè)計(jì)模式,相信知道的同學(xué)都會脫口而出,五大基本原則(SOLID)和 23 種設(shè)計(jì)模式。SOLID 所指的五大基本原則分別是:單一功能原則、開放封閉原則、里式替換原則、接口隔離原則和依賴反轉(zhuǎn)原則。逐字逐句詮釋這五大基本原則違背了寫這篇文章的初衷,引用社區(qū)大佬的理解,SOLID 可以簡單概括為六個字,即“高內(nèi)聚,低耦合”:
高層模塊不依賴底層模塊,即為依賴反轉(zhuǎn)原則。 內(nèi)部修改關(guān)閉,外部擴(kuò)展開放,即為開放封閉原則。 聚合單一功能,即為單一功能原則。 低知識要求,對外接口簡單,即為迪米特法則。 耦合多個接口,不如獨(dú)立拆分,即為接口隔離原則。 合成復(fù)用,子類繼承可替換父類,即為里式替換原則。
23 種設(shè)計(jì)模式分為“創(chuàng)建型”、“行為型”和“結(jié)構(gòu)型”。具體類型如下圖:
設(shè)計(jì)模式說白了就是“封裝變化”。比如“創(chuàng)建型”封裝了創(chuàng)建對象的變化過程,“結(jié)構(gòu)型”將對象之間組合的變化封裝,“行為型”則是抽離對象的變化行為。接下來,本文將以“單一功能”和“開放封閉”這兩大原則為主線,分別介紹“創(chuàng)建型”、“結(jié)構(gòu)型”和“行為型”中最具代表性的幾大設(shè)計(jì)模式。
創(chuàng)建型
工廠模式
工廠模式根據(jù)抽象程度可分為三種,分別為簡單工廠、工廠方法和抽象工廠。其核心在于將創(chuàng)建對象的過程封裝其他,然后通過同一個接口創(chuàng)建新的對象。 簡單工廠模式又叫靜態(tài)工廠方法,用來創(chuàng)建某一種產(chǎn)品對象的實(shí)例,用來創(chuàng)建單一對象。
//?簡單工廠
class?Factory?{
??constructor?(username,?pwd,?role)?{
???this.username?=?username;
????this.pwd?=?pwd;
????this.role?=?role;
??}
}
class?CreateRoleFactory?{
?static?create?(username,?pwd,?role)?{
???return?new?Factory(username,?pwd,?role);
??}
}
const?admin?=?CreateRoleFactory.create('張三',?'222',?'admin');
在實(shí)際工作中,各用戶角色所具備的能力是不同的,因此簡單工廠是無法滿足的,這時候就可以考慮使用工廠方法來代替。工廠方法的本意是將實(shí)際創(chuàng)建對象的工作推遲到子類中。
class?User?{
?constructor?(name,?menuAuth)?{
???if?(new.target?===?User)?throw?new?Error('User?不能被實(shí)例化');
????this.name?=?name;
????this.menuAuth?=?menuAuth;
??}
}
class?UserFactory?extends?User?{
?constructor?(...props)?{
???super(...props);
??}
??static?create?(role)?{
???const?roleCollection?=?new?Map([
?????['admin',?()?=>?new?UserFactory('管理員',?['首頁',?'個人中心'])],
??????['user',?()?=>?new?UserFactory('普通用戶',?['首頁'])]
????])
????
????return?roleCollection.get(role)();
??}
}
const?admin?=?UserFactory.create('admin');
console.log(admin);?//?{name:?"管理員",?menuAuth:?Array(2)}
const?user?=?UserFactory.create('user');
console.log(user);?//?{name:?"普通用戶",?menuAuth:?Array(1)}
隨著業(yè)務(wù)形態(tài)的變化,一個用戶可能在多個平臺上同時存在,顯然工廠方法也不再滿足了,這時候就要用到抽象工廠。抽象工廠模式是對類的工廠抽象用來創(chuàng)建產(chǎn)品類簇,不負(fù)責(zé)創(chuàng)建某一類產(chǎn)品的實(shí)例。
class?User?{
??constructor?(hospital)?{
????if?(new.target?===?User)?throw?new?Error('抽象類不能實(shí)例化!');
????this.hospital?=?hospital;
??}
}
//?浙一
class?ZheYiUser?extends?User?{
??constructor(name,?departmentsAuth)?{
????super('zheyi_hospital');
????this.name?=?name;
????this.departmentsAuth?=?departmentsAuth;
??}
}
//?蕭山醫(yī)院
class?XiaoShanUser?extends?User?{
??constructor(name,?departmentsAuth)?{
????super('xiaoshan_hospital');
????this.name?=?name;
????this.departmentsAuth?=?departmentsAuth;
??}
}
const?getAbstractUserFactory?=?(hospital)?=>?{
??switch?(hospital)?{
????case?'zheyi_hospital':
??????return?ZheYiUser;
??????break;
????case?'xiaoshan_hospital':
??????return?XiaoShanUser;
??????break;
??}
}
const?ZheYiUserClass?=?getAbstractUserFactory('zheyi_hospital');
const?XiaoShanUserClass?=?getAbstractUserFactory('xiaoshan_hospital');
const?user1?=?new?ZheYiUserClass('王醫(yī)生',?['外科',?'骨科',?'神經(jīng)外科']);
console.log(user1);
const?user2?=?newXiaoShanUserClass('王醫(yī)生',?['外科',?'骨科']);
console.log(user2);
小結(jié): 構(gòu)造函數(shù)和創(chuàng)建對象分離,符合開放封閉原則。
使用場景: 比如根據(jù)權(quán)限生成不同用戶。
單例模式
單例模式理解起來比較簡單,就是保證一個類只能存在一個實(shí)例,并提供一個訪問它的全局接口。單例模式又分懶漢式和餓漢式兩種,其區(qū)別在于懶漢式在調(diào)用的時候創(chuàng)建實(shí)例,而餓漢式則是在初始化就創(chuàng)建好實(shí)例,具體實(shí)現(xiàn)如下:
//?懶漢式
class?Single?{
?static?getInstance?()?{
???if?(!Single.instance)?{
?????Single.instance?=?new?Single();
????}
????return?Single.instance;
??}
}
const?test1?=?Single.getInstance();
const?test2?=?Single.getInstance();
console.log(test1?===?test2);?//?true
//?餓漢式
class?Single?{
?static?instance?=?new?Single();
??static?getInstance?()?{
????return?Single.instance;
??}
}
const?test1?=?Single.getInstance();
const?test2?=?Single.getInstance();
console.log(test1?===?test2);?//?true
小結(jié): 實(shí)例如果存在,直接返回已創(chuàng)建的,符合開放封閉原則。
使用場景: Redux、Vuex 等狀態(tài)管理工具,還有我們常用的 window 對象、全局緩存等。
原型模式
對于前端來說,原型模式在常見不過了。當(dāng)新創(chuàng)建的對象和已有對象存在較大共性時,可以通過對象的復(fù)制來達(dá)到創(chuàng)建新的對象,這就是原型模式。
//?Object.create()實(shí)現(xiàn)原型模式
const?user?=?{
?name:?'zhangsan',
??age:?18
};
let?userOne?=?Object.create(user);
console.log(userOne.__proto__);?//?{name:?"zhangsan",?age:?18}
//?原型鏈繼承實(shí)現(xiàn)原型模式
class?User?{
?constructor?(name)?{
???this.name?=?name;
??}
??getName?()?{
???return?this.name;
??}
}
class?Admin?extends?User?{
?constructor?(name)?{
???super(name);
??}
??setName?(_name)?{
???return?this.name?=?_name;
??}
}
const?admin?=?new?Admin('zhangsan');
console.log(admin.getName());
console.log(admin.setName('lisi'));
小結(jié): 原型模式最簡單的實(shí)現(xiàn)方式---Object.create()。
使用場景: 新創(chuàng)建對象和已有對象無較大差別時,可以使用原型模式來減少創(chuàng)建新對象的成本。
結(jié)構(gòu)型
裝飾器模式
講裝飾器模式之前,先聊聊高階函數(shù)。高階函數(shù)就是一個函數(shù)就可以接收另一個函數(shù)作為參數(shù)。
const?add?=?(x,?y,?f)?=>?{
?return?f(x)?+?f(y);
}
const?num?=?add(2,?-2,?Math.abs);
console.log(num);?//?4
函數(shù) add 就是一個簡單的高階函數(shù),而 add 相對于 Math.abs 來說相當(dāng)于一個裝飾器,因此這個例子也可以理解為一個簡單的裝飾器模式。在 react 中,高階組件(HOC)也是裝飾器模式的一種體現(xiàn),通常用來不改變原來組件的情況下添加一些屬性,達(dá)到組件復(fù)用的功能。
import?React?from?'react';
const?BgHOC?=?WrappedComponent?=>?class?extends?React.Component?{
?render?()?{
???return?(
?????<div?style={{?background:?'blue'?}}>
???????<WrappedComponent?/>
??????div>
????);
??}
}
小結(jié): 裝飾器模式將現(xiàn)有對象和裝飾器進(jìn)行分離,兩者獨(dú)立存在,符合開放封閉原則和單一職責(zé)模式。
使用場景: es7 裝飾器、vue mixins、core-decorators 等。
適配器模式
適配器別名包裝器,其作用是解決兩個軟件實(shí)體間的接口不兼容的問題。以 axios 源碼為例:
function?getDefaultAdapter()?{
??var?adapter;
??//?判斷當(dāng)前是否是?node?環(huán)境
??if?(typeof?process?!==?'undefined'?&&?Object.prototype.toString.call(process)?===?'[object?process]')?{
????//?如果是?node?環(huán)境,調(diào)用?node?專屬的?http?適配器
????adapter?=?require('./adapters/http');
??}?else?if?(typeof?XMLHttpRequest?!==?'undefined')?{
????//?如果是瀏覽器環(huán)境,調(diào)用基于?xhr?的適配器
????adapter?=?require('./adapters/xhr');
??}
??return?adapter;
}
//?http?adapter
module.exports?=?function?httpAdapter(config)?{
??return?new?Promise(function?dispatchHttpRequest(resolvePromise,?rejectPromise)?{
????...
??}
}
//?xhr?adapter
module.exports?=?function?xhrAdapter(config)?{
??return?new?Promise(function?dispatchXhrRequest(resolve,?reject)?{
????...
??}
}
其目的就是保證 node 和瀏覽器環(huán)境的入?yún)?config 一致,出參 Promise 都是同一個。
小結(jié): 不改變原有接口的情況下,統(tǒng)一接口、統(tǒng)一入?yún)ⅰ⒔y(tǒng)一出參、統(tǒng)一規(guī)則,符合開發(fā)封閉原則。
使用場景 :擁抱變化,兼容代碼。
代理模式
代理模式就是為對象提供一個代理,用來控制對這個對象的訪問。在我們業(yè)務(wù)開發(fā)中最常見的有四種代理類型:事件代理,虛擬代理、緩存代理和保護(hù)代理。本文主要介紹虛擬代理和緩存代理兩類。 提到虛擬代理,其最具代表性的例子就是圖片預(yù)加載。預(yù)加載主要是為了避免網(wǎng)絡(luò)延遲、或者圖片太大引起頁面長時間留白的問題。通常的解決方案是先給 img 標(biāo)簽展示一個占位圖,然后創(chuàng)建一個 Image 實(shí)例,讓這個實(shí)例的 src 指向真實(shí)的目標(biāo)圖片地址,當(dāng)其真實(shí)圖片加載完成之后,再將 DOM 上的 img 標(biāo)簽的 src 屬性指向真實(shí)圖片地址。
class?ProxyImg?{
?constructor?(imgELe)?{
???this.imgELe?=?imgELe;
????this.DEFAULT_URL?=?'xxx';
??}
??setUrl?(targetUrl)?{
???this.imgEle.src?=?this.DEFAULT_URL;
????const?image?=?new?Image();
????
????image.onload?=?()?=>?{
?????this.imgEle.src?=?targetUrl;
????}
????image.src?=?targetUrl;
??}
}
緩存代理常用于一些計(jì)算量較大的場景。當(dāng)計(jì)算的值已經(jīng)被出現(xiàn)過的時候,不需要進(jìn)行第二次重復(fù)計(jì)算。以傳參求和為例:
const?countSum?=?(...arg)?=>?{
?console.log('count...');
??let?result?=?0;
??arg.forEach(v?=>?result?+=?v);
??return?result;
}
const?proxyCountSum?=?(()?=>?{
?const?cache?=?{};
??return?(...arg)?=>?{
???const?args?=?arg.join(',');
????if?(args?in?cache)?return?cache[args];
????return?cache[args]?=?countSum(...arg);
??};
})()
proxyCountSum(1,2,3,4);?//?count...??10
proxyCountSum(1,2,3,4);?//?10
小結(jié): 通過修改代理類來增加功能,符合開放封閉模式。
使用場景: 圖片預(yù)加載、緩存服務(wù)器、處理跨域以及攔截器等。
行為型
策略模式
介紹策略模式之前,簡單實(shí)現(xiàn)一個常見的促銷活動規(guī)則:
預(yù)售活動,全場 9.5 折 大促活動,全場 9 折 返場優(yōu)惠,全場 8.5 折 限時優(yōu)惠,全場 8 折
人人喊打的 if-else
const?activity?=?(type,?price)?=>?{
?if?(type?===?'pre')?{
???return?price?*?0.95;
??}?else?if?(type?===?'onSale')?{
???return?price?*?0.9;
??}?else?if?(type?===?'back')?{
???return?price?*?0.85;
??}?else?if?(type?===?'limit')?{
???return?price?*?0.8;
??}
}
以上代碼存在肉眼可見的問題:大量 if-else、可擴(kuò)展性差、違背開放封閉原則等。 我們再使用策略模式優(yōu)化:
const?activity?=?new?Map([
?['pre',?(price)?=>?price?*?0.95],
??['onSale',?(price)?=>?price?*?0.9],
??['back',?(price)?=>?price?*?0.85],
??['limit',?(price)?=>?price?*?0.8]
]);
const?getActivityPrice?=?(type,?price)?=>?activity.get(type)(price);
//?新增新手活動
activity.set('newcomer',?(price)?=>?price?*?0.7);
小結(jié): 定義一系列算法,將其一一封裝起來,并且使它們可相互替換。符合開放封閉原則。
使用場景: 表單驗(yàn)證、存在大量 if-else 場景、各種重構(gòu)等。
觀察者模式
觀察者模式又叫發(fā)布-訂閱模式,其用來定義對象之間的一對多依賴關(guān)系,以便當(dāng)一個對象更改狀態(tài)時,將通知其所有依賴關(guān)系。通過“別名”可以知道,觀察者模式具備兩個角色,即“發(fā)布者”和“訂閱者”。正如我們工作中的產(chǎn)品經(jīng)理就是一個“發(fā)布者”,而前后端、測試可以理解為“訂閱者”。以產(chǎn)品經(jīng)理建需求溝通群為例:
//?定義發(fā)布者類
class?Publisher?{
??constructor?()?{
????this.observers?=?[];
????this.prdState?=?null;
??}
??//?增加訂閱者
??add?(observer)?{
????this.observers.push(observer);
??}
??//?通知所有訂閱者
??notify?()?{
????this.observers.forEach((observer)?=>?{
??????observer.update(this);
????})
??}
??//?該方法用于獲取當(dāng)前的?prdState
??getState?()?{
????return?this.prdState;
??}
??//?該方法用于改變?prdState?的值
??setState?(state)?{
????//?prd?的值發(fā)生改變
????this.prdState?=?state;
????//?需求文檔變更,立刻通知所有開發(fā)者
????this.notify();
??}
}
//?定義訂閱者類
class?Observer?{
??constructor?()?{
??this.prdState?=?{};
??}
??update?(publisher)?{
????//?更新需求文檔
????this.prdState?=?publisher.getState();
????//?調(diào)用工作函數(shù)
????this.work();
??}
??//?work?方法,一個專門搬磚的方法
??work?()?{
????//?獲取需求文檔
????const?prd?=?this.prdState;
????console.log(prd);
??}
}
//?創(chuàng)建訂閱者:前端開發(fā)小王
const?wang?=?new?Observer();
//?創(chuàng)建訂閱者:后端開發(fā)小張
const?zhang?=?new?Observer();
//?創(chuàng)建發(fā)布者:產(chǎn)品經(jīng)理小曾
const?zeng?=?new?Publisher();
//?需求文檔
const?prd?=?{
??url:?'xxxxxxx'
};
//?小曾開始拉人入群
zeng.add(wang);
zeng.add(zhang);
//?小曾發(fā)布需求文檔并通知所有人
zeng.setState(prd);
經(jīng)常使用 Event Bus(Vue) 和 Event Emitter(node)會發(fā)現(xiàn),發(fā)布-訂閱模式和觀察者模式還是存在著細(xì)微差別,即所有事件的發(fā)布/訂閱都不能由發(fā)布者和訂閱者“私下聯(lián)系”,需要委托事件中心處理。以 Vue Event Bus 為例:
import?Vue?from?'vue';
const?EventBus?=?new?Vue();
Vue.prototype.$bus?=?EventBus;
//?訂閱事件
this.$bus.$on('testEvent',?func);
//?發(fā)布/觸發(fā)事件
this.$bus.$emit('testEvent',?params);
整個過程都是 this.$bus 這個“事件中心”在處理。
小結(jié): 為解耦而生,為事件而生,符合開放封閉原則。
使用場景: 跨層級通信、事件綁定等。
迭代器模式
迭代器模式號稱“遍歷專家”,它提供一種方法順序訪問一個聚合對象中的各個元素,且不暴露該對象的內(nèi)部表示。迭代器又分內(nèi)部迭代器(jquery.each/for...of)和外部迭代器(es6 yield)。 在 es6 之前,直接通過 forEach 遍歷 DOM NodeList 和函數(shù)的 arguments 對象,都會直接報錯,其原因都是因?yàn)樗麄兌际穷悢?shù)組對象。對此 jquery 很好的兼容了這一點(diǎn)。 在 es6 中,它約定只要數(shù)據(jù)類型具備 Symbol.iterator 屬性,就可以被 for...of 循環(huán)和迭代器的 next 方法遍歷。
(function?(a,?b,?c)?{
?const?arg?=?arguments;
??const?iterator?=?arg[Symbol.iterator]();
??
??console.log(iterator.next());?//?{value:?1,?done:?false}
??console.log(iterator.next());?//?{value:?2,?done:?false}
??console.log(iterator.next());?//?{value:?3,?done:?false}
??console.log(iterator.next());?//?{value:?undefined,?done:?true}
})(1,?2,?3)
通過 es6 內(nèi)置生成器 Generator 實(shí)現(xiàn)迭代器并沒什么難度,這里重點(diǎn)通 es5 實(shí)現(xiàn)迭代器:
function?iteratorGenerator?(list)?{
??var?index?=?0;
??//?len?記錄傳入集合的長度
??var?len?=?list.length;
??return?{
????//?自定義?next?方法
????next:?funciton?()?{
??????//?如果索引還沒有超出集合長度,done?為?false
??????var?done?=?index?>=?len;
??????//?如果?done?為?false,則可以繼續(xù)取值
??????var?value?=?!done???list[index++]?:?undefined;
??????//?將當(dāng)前值與遍歷是否完畢(done)返回
??????return?{
????????done:?done,
????????value:?value
??????};
????}
??}
}
var?iterator?=?iteratorGenerator([1,?2,?3]);
console.log(iterator.next());?//?{value:?1,?done:?false}
console.log(iterator.next());?//?{value:?2,?done:?false}
console.log(iterator.next());?//?{value:?3,?done:?false}
console.log(iterator.next());?//?{value:?undefined,?done:?true}
小結(jié): 實(shí)現(xiàn)統(tǒng)一遍歷接口,符合單一功能和開放封閉原則。
使用場景: 有遍歷的地方就有迭代器。
寫到最后
設(shè)計(jì)模式的難,在于它的抽象和分散。抽象在于每一設(shè)計(jì)模式看例子都很好理解,真正使用起來卻不知所措;分散則是出現(xiàn)一個場景發(fā)現(xiàn)好幾種設(shè)計(jì)模式都能實(shí)現(xiàn)。而解決抽象的最好辦法就是動手實(shí)踐,在業(yè)務(wù)開發(fā)中探索使用它們的可能性。本文大致介紹了前端領(lǐng)域常見的 9 種設(shè)計(jì)模式,相信大家在理解的同時也不難發(fā)現(xiàn),設(shè)計(jì)模式始終圍繞著“封裝變化”來提供代碼的可讀性、擴(kuò)展性、易維護(hù)性。所以當(dāng)我們工作生活中,始終保持“封裝變化”的思想的時候,就已經(jīng)開始體會到設(shè)計(jì)模式精髓了。
分享前端好文,點(diǎn)亮?在看
