Web開發(fā)應(yīng)了解的5種設(shè)計(jì)模式
本文轉(zhuǎn)載自微信公眾號(hào)「前端進(jìn)階之路」,作者三余。轉(zhuǎn)載本文請(qǐng)聯(lián)系前端進(jìn)階之路公眾號(hào)。
什么是設(shè)計(jì)模式?
設(shè)計(jì)模式是對(duì)軟件設(shè)計(jì)開發(fā)過程中反復(fù)出現(xiàn)的某類問題的通用解決方案。設(shè)計(jì)模式更多的是指導(dǎo)思想和方法論,而不是現(xiàn)成的代碼,當(dāng)然每種設(shè)計(jì)模式都有每種語(yǔ)言中的具體實(shí)現(xiàn)方式。學(xué)習(xí)設(shè)計(jì)模式更多的是理解各種模式的內(nèi)在思想和解決的問題,畢竟這是前人無數(shù)經(jīng)驗(yàn)總結(jié)成的最佳實(shí)踐,而代碼實(shí)現(xiàn)則是對(duì)加深理解的輔助。使用設(shè)計(jì)模式是為了可重用代碼、讓代碼更容易被他人理解、保證代碼可靠性。在本文中,我將介紹其中常見的五種設(shè)計(jì)模式在JavaScript中實(shí)際使用場(chǎng)景:
單例設(shè)計(jì)模式
定義
單例模式僅允許類或?qū)ο缶哂袉蝹€(gè)實(shí)例,并且它使用全局變量來存儲(chǔ)該實(shí)例。
實(shí)現(xiàn)方法是判斷是否存在該對(duì)象的實(shí)例,如果已存在則不再創(chuàng)建
使用場(chǎng)景適用于業(yè)務(wù)場(chǎng)景中只能存在一個(gè)的實(shí)例,比如彈窗,購(gòu)物車
實(shí)現(xiàn)
單例模式分為懶漢式和餓漢式:
- 懶漢式
- let ShopCar = (function () {
- let instance;
- function init() {
- /*這里定義單例代碼*/
- return {
- buy(good) {
- this.goods.push(good);
- },
- goods: [],
- };
- }
- return {
- getInstance: function () {
- if (!instance) {
- instance = init();
- }
- return instance;
- },
- };
- })();
- let car1 = ShopCar.getInstance();
- let car2 = ShopCar.getInstance();
- car1.buy('橘子');
- car2.buy('蘋果');
- console.log(car1.goods); //[ '橘子', '蘋果' ]
- console.log(car1 === car2); // true
- 餓漢式
- var ShopCar = (function () {
- var instance = init();
- function init() {
- /*這里定義單例代碼*/ return {
- buy(good) {
- this.goods.push(good);
- },
- goods: [],
- };
- }
- return {
- getInstance: function () {
- return instance;
- },
- };
- })();
- let car1 = ShopCar.getInstance();
- let car2 = ShopCar.getInstance();
- car1.buy('橘子');
- car2.buy('蘋果'); //[ '橘子', '蘋果' ]
- console.log(car1.goods);
- console.log(car1 === car2); // true
懶漢式在類加載時(shí),不創(chuàng)建實(shí)例,因此類加載速度快,但運(yùn)行時(shí)獲取對(duì)象的速度慢;
餓漢式在類加載時(shí)就完成了初始化,所以類加載較慢,但獲取對(duì)象的速度快
策略模式
定義
策略模式定義一系列的算法,將每一個(gè)算法封裝起來,并讓他們可以相互替換。
實(shí)現(xiàn)方法定義一組可變的策略類封裝具體算法,定義一組不變的環(huán)境類將請(qǐng)求委托給某一個(gè)策略類
使用場(chǎng)景適用于業(yè)務(wù)場(chǎng)景中需要判斷多種條件,甚至包含復(fù)雜條件嵌套的,可以使用策略模式來提升代碼的可維護(hù)性和可讀性。比如支付,博客權(quán)限校驗(yàn)
實(shí)現(xiàn)
實(shí)例:
- // 定義幾個(gè)策略類
- var PaymentMethodStrategy = {
- BankAccount: function (money) {
- return money > 50 ? money * 0.7 : money;
- },
- CreditCard: function (money) {
- return money * 0.8;
- },
- Alipay: function (money) {
- return money;
- },
- };
- /*環(huán)境類*/
- var userPay = function (selectedStrategy, money) {
- return PaymentMethodStrategy[selectedStrategy](money);
- };
- console.log('銀行卡支付價(jià)格為:' + userPay('BankAccount', 100)); //70
- console.log('支付寶支付價(jià)格為:' + userPay('Alipay', 100)); //100
- console.log('信用卡支付價(jià)格為:' + userPay('CreditCard', 100)); //80
觀察者模式
定義
觀察者模式是對(duì)象的行為模式,在對(duì)象之間定義了一對(duì)多的依賴關(guān)系,就是多個(gè)觀察者和一個(gè)被觀察者之間的關(guān)系,當(dāng)被觀察者發(fā)生變化的時(shí)候,會(huì)通知所有的觀察者對(duì)象,他們做出相對(duì)應(yīng)的操作。
實(shí)現(xiàn)方法定義一組可變的策略類封裝具體算法,定義一組不變的環(huán)境類將請(qǐng)求委托給某一個(gè)策略類
使用場(chǎng)景適用于業(yè)務(wù)場(chǎng)景中當(dāng)一個(gè)對(duì)象的狀態(tài)發(fā)生變化時(shí),需要自動(dòng)通知其他關(guān)聯(lián)對(duì)象,自動(dòng)刷新對(duì)象狀態(tài),或者說執(zhí)行對(duì)應(yīng)對(duì)象的方法,比如你是一個(gè)老師,需要通知班里家長(zhǎng)的時(shí)候,你可以建一個(gè)群(列表)。每次通知事件的時(shí)候只要循環(huán)執(zhí)行這個(gè)列表就好了(群發(fā)),而不用關(guān)心這個(gè)列表里有誰。
實(shí)現(xiàn)
實(shí)例:
- // 創(chuàng)建一個(gè)群,保存通知,通知變化之后通知每個(gè)家長(zhǎng)(觸發(fā)所有觀察者對(duì)象)
- class Group {
- constructor() {
- this.message = '暫無通知';
- this.parents = [];
- }
- getMessage() {
- return this.message;
- }
- setMassage(message) {
- this.message = message;
- this.notifyAllObservers();
- }
- notifyAllObservers() {
- this.parents.forEach((parent) => {
- parent.update();
- });
- }
- attach(parent) {
- this.parents.push(parent);
- }
- }
- // 觀察者,每個(gè)家長(zhǎng)
- class Parent {
- constructor(name, group) {
- this.name = name;
- this.group = group;
- this.group.attach(this);
- }
- update() {
- console.log(`${this.name} 收到通知: ${this.group.getMessage()}`);
- }
- }
- let group = new Group();
- let t1 = new Parent('李媽媽', group);
- let t2 = new Parent('王爸爸', group);
- let t3 = new Parent('張爺爺', group);
- group.setMassage('開家長(zhǎng)會(huì)');
- group.setMassage('開運(yùn)動(dòng)會(huì)');
- /*
- 李媽媽 收到通知: 開家長(zhǎng)會(huì)
- 王爸爸 收到通知: 開家長(zhǎng)會(huì)
- 張爺爺 收到通知: 開家長(zhǎng)會(huì)
- 李媽媽 收到通知: 開運(yùn)動(dòng)會(huì)
- 王爸爸 收到通知: 開運(yùn)動(dòng)會(huì)
- 張爺爺 收到通知: 開運(yùn)動(dòng)會(huì)
- */
發(fā)布訂閱模式
定義
發(fā)布訂閱模式指的是希望接收通知的對(duì)象(Subscriber)基于一個(gè)主題通過自定義事件訂閱主題,發(fā)布事件的對(duì)象(Publisher)通過發(fā)布主題事件的方式通知各個(gè)訂閱該主題的 Subscriber 對(duì)象。
實(shí)現(xiàn)
- const pubSub = {
- list:{},
- subscribe(key,fn){ // 訂閱
- if (!this.list[key]) {
- this.list[key] = [];
- }
- this.list[key].push(fn);
- },
- publish(){ // 發(fā)布
- const arg = arguments;
- const key = Array.prototype.shift.call(arg);
- const fns = this.list[key];
- if(!fns || fns.length<=0) return false;
- for(var i=0,len=fns.length;i<len;i++){
- fns[i].apply(this, arg);
- }
- },
- unSubscribe(key) { // 取消訂閱
- delete this.list[key];
- }
- };
- // 進(jìn)行訂閱
- pubSub.subscribe('name', (name) => {
- console.log('your name is ' + name);
- });
- pubSub.subscribe('sex', (sex) => {
- console.log('your sex is ' + sex);
- });
- // 進(jìn)行發(fā)布
- pubSub.publish('name', 'ttsy1'); // your name is ttsy1
- pubSub.publish('sex', 'male'); // your sex is male
上述代碼的訂閱是基于 name 和 sex 主題來自定義事件,發(fā)布是通過 name 和 sex 主題并傳入自定義事件的參數(shù),最終觸發(fā)了特定主題的自定義事件。
可以通過 unSubscribe 方法取消特定主題的訂閱。
- pubSub.subscribe('name', (name) => {
- console.log('your name is ' + name);
- });
- pubSub.subscribe('sex', (sex) => {
- console.log('your sex is ' + sex);
- });
- pubSub.unSubscribe('name');
- pubSub.publish('name', 'ttsy1'); // 這個(gè)主題被取消訂閱了
- pubSub.publish('sex', 'male'); // your sex is male
觀察者模式 VS 發(fā)布訂閱模式:
觀察者模式與發(fā)布訂閱模式都是定義了一個(gè)一對(duì)多的依賴關(guān)系,當(dāng)有關(guān)狀態(tài)發(fā)生變更時(shí)則執(zhí)行相應(yīng)的更新。
不同的是,在觀察者模式中依賴于 Subject 對(duì)象的一系列 Observer 對(duì)象在被通知之后只能執(zhí)行同一個(gè)特定的更新方法,而在發(fā)布訂閱模式中則可以基于不同的主題去執(zhí)行不同的自定義事件。
相對(duì)而言,發(fā)布訂閱模式比觀察者模式要更加靈活多變。
裝飾器模式
定義
在不改變?cè)瓉淼慕Y(jié)構(gòu)和功能基礎(chǔ)上,動(dòng)態(tài)裝飾一些針對(duì)特別場(chǎng)景所適用的方法或?qū)傩裕刺砑右恍┬鹿δ芤栽鰪?qiáng)它的某種能力
實(shí)現(xiàn)方法定義一組可變的策略類封裝具體算法,定義一組不變的環(huán)境類將請(qǐng)求委托給某一個(gè)策略類
使用場(chǎng)景原有方法維持不變,在原有方法上再掛載其他方法來滿足現(xiàn)有需求;函數(shù)的解耦,將函數(shù)拆分成多個(gè)可復(fù)用的函數(shù),再將拆分出來的函數(shù)掛載到某個(gè)函數(shù)上,實(shí)現(xiàn)相同的效果但增強(qiáng)了復(fù)用性。比如多孔插座,機(jī)車改裝
實(shí)現(xiàn)
實(shí)例:
- const Man = function () {
- this.run = function () {
- console.log('跑步');
- };
- };
- const Decorator = function (old) {
- this.oldAbility = old.run;
- this.fly = function () {
- console.log('具備飛行能力');
- };
- this.newAbility = function () {
- this.oldAbility();
- this.fly();
- };
- };
- const man = new Man();
- const superMan = new Decorator(man);
- superMan.fly(); // 具備飛行能力
代理模式
定義
代理模式給某一個(gè)對(duì)象提供一個(gè)代理對(duì)象,并由代理對(duì)象控制對(duì)原對(duì)象的引用。通俗的來講代理模式就是我們生活中常見的中介。
實(shí)現(xiàn)方法定義一個(gè)委托者和一個(gè)代理,需要委托的事情在代理中完成
使用場(chǎng)景在某些情況下,一個(gè)客戶類不想或者不能直接引用一個(gè)委托對(duì)象,而代理類對(duì)象可以在客戶類和委托對(duì)象之間起到中介的作用。代理可以幫客戶過濾掉一些請(qǐng)求并且把一些開銷大的對(duì)象,延遲到真正需要它時(shí)才創(chuàng)建。中介購(gòu)車、代購(gòu)、課代表替老師收作業(yè)
實(shí)現(xiàn)
實(shí)例:
- class Letter {
- constructor(name) {
- this.name = name;
- }
- }
- // 暗戀人小明
- let XiaoMing = {
- name: '小明',
- sendLetter(target) {
- target.receiveLetter(this.name);
- },
- };
- // 代理小華
- let xiaoHua = {
- receiveLetter(customer) {
- // 當(dāng)然要等小紅好心情時(shí)才送情書,也在送情書也才創(chuàng)建情書
- XiaoHong.listenGoodMood(() => {
- XiaoHong.receiveLetter(new Letter(customer + '的情書'));
- });
- },
- };
- // 心儀對(duì)象小紅
- let XiaoHong = {
- name: '小紅',
- receiveLetter(letter) {
- console.log(this.name + '收到:' + letter.name);
- },
- listenGoodMood(fn) {
- setTimeout(() => {
- fn();
- }, 1000);
- },
- };
- XiaoMing.sendLetter(xiaoHua); //小紅收到:小明的情書
Proxy 是 ES6 提供的專門以代理角色出現(xiàn)的代理器,Vue 3.0 的響應(yīng)式數(shù)據(jù)部分棄用了 Object.defineProperty,使用 Proxy 來代替它。
- var proxy = new Proxy(target, handler);
現(xiàn)在用Proxy模擬一下另一種場(chǎng)景:為了保護(hù)不及格的同學(xué),課代表拿到全班成績(jī)單后只會(huì)公示及格人的成績(jī)。對(duì)考分有疑問的考生,復(fù)議后新分?jǐn)?shù)比以前大10分才有權(quán)利去更新成績(jī)
- const scoreList = { wang: 90, li: 60, wu: 100 };
- const yyProxy = new Proxy(scoreList, {
- get: function (scoreList, name) {
- if (scoreList[name] > 69) {
- console.log('輸出成績(jī)');
- return scoreList[name];
- } else {
- console.log('不及格的成績(jī)無法公示');
- }
- },
- set: function (scoreList, name, val) {
- if (val - scoreList[name] > 10) {
- console.log('修改成績(jī)');
- scoreList[name] = val;
- } else {
- console.log('無法修改成績(jī)');
- }
- },
- });
- yyProxy['wang'] = 98; //無法修改成績(jī)
- yyProxy['li'] = 80; //修改成績(jī)
總結(jié)
我曾經(jīng)以為設(shè)計(jì)模式是瘋狂的,遙遠(yuǎn)的軟件開發(fā)指南。然后我發(fā)現(xiàn)我一直在使用它們!我介紹的一些模式已在許多應(yīng)用程序中使用。但歸根結(jié)底,它們只是理論而已。作為開發(fā)人員,是否使用取決于使用后是否使代碼邏輯更易于實(shí)現(xiàn)和維護(hù)。