淺析什么是設計模式(套路)、為什么需要設計模式(最優解決方案)、前端常見設計模式(策略模式、發布訂閱模式、裝飾器模式、適配器模式、職責鏈模式、代理模式)


一、什么是設計模式

  官方解釋一點就是:模式是一種可復用的解決方案,用於解決軟件設計中遇到的常見問題。

  說白了,就是“套路”,舉個例子:我們玩游戲,第一關用了半小時,第二關用了一小時,第三關用了兩小時,......,然后,你花了一個月練到了滿級;

  於是你開始練第二個號,這時候呢,其實你已經知道,每一關的捷徑、好的裝備在哪里,所以你按照這個套路,很快的,20 天又練滿了一個號;

  身邊有好友問你怎么這么快的又練了一個號,於是你為了造福大眾,你寫了一本 闖關攻略 ~。

  通過這個例子,你應該知道,什么是設計模式了吧?烹飪有菜譜,游戲有攻略,干啥都有一些能夠讓我們達到目標的“套路”,在程序世界,編程的“套路”就是設計模式。當然,如果真要給你個定義,我認為,設計模式就是在軟件設計、開發過程中,針對特定問題、場景的更優解決方案。

二、為什么會有設計模式

  就還是上邊的例子,魯迅先生說過 : “希望是本無所謂有,無所謂無的。這正如地上的路;其實地上本沒有路,走的人多了,也便成了路”,設計模式是前輩們針對開發中遇到的問題,提出大家公認且有效的解決方案

1、為什么需要設計模式?

  可能有小伙伴確實沒有用過,或者說用了但不知道這就是設計模式。那么為什么我們需要呢?是因為在我們遇到相似的問題、場景時,能快速找到更優的方式解決。

2、如何使用

  在 JS 設計模式中,最核心的思想 - 封裝變化。怎么理解,比如我們寫一個東西,這個東西在初始 v1.0 的時候是這樣,到了 v5.0v10.0 甚至 v99.0v100.0 還是這樣,你愛怎么寫就怎么寫,你只要實現就可以了。

設計模式的核心操作是去觀察你整個邏輯里面的變與不變,然后將變與不變分離,達到使變化的部分靈活、不變的地方穩定的目的。

3、都有啥設計模式 ?

  相信了解的,都知道有 20 多種。挺多的,我不扯那么多,其它的我沒用過:什么享元模式、外觀模式、生成器模式啥的,今天就主要聊下前端常用的 6 種設計模式。

三、前端常用的 -  策略模式

1、問題背景

  我們先來做一個題,很簡單的,大家肯定都做過 權限邏輯 判斷吧?需求:只用當用戶滿足以下條件,才能看阿寬的這篇文章。給大家 3min,代碼怎么寫? “ 呵,你這不是看不起老夫嗎?老夫拿起鍵盤,就是 if-else 梭哈,直接帶走,下一個 ! ”

function checkAuth(data) { if (data.role !== 'juejin') { console.log('不是掘金用戶'); return false; } if (data.grade < 1) { console.log('掘金等級小於 1 級'); return false; } if (data.job !== 'FE') { console.log('不是前端開發'); return false; } if (data.type !== 'eat melons') { console.log('不是吃瓜群眾'); return false; } }

  相信這段代碼,大家都會寫,那么這么寫,有什么問題 ?

  • checkAuth 函數會爆炸
  • 策略項無法復用
  • 違反開閉原則

  聰明的小伙伴已經知道這里要講的是什么模式了,對頭!這里講的就是 策略模式。那么什么是策略模式呢 ?

2、什么是策略模式

  定義 : 要實現某一個功能,有多種方案可以選擇。我們定義策略,把它們一個個封裝起來,並且使它們可以相互轉換。

策略 + 組合 = 絕配

  我們用策略模式來改造以下這段邏輯

// 維護權限列表
const jobList = ['FE', 'BE']; // 策略
var strategies = { checkRole: function(value) { return value === 'juejin'; }, checkGrade: function(value) { return value >= 1; }, checkJob: function(value) { return jobList.indexOf(value) > 1; }, checkEatType: function(value) { return value === 'eat melons'; } };

  我們已經寫完了策略,接下來要做的就是驗證了

// 校驗規則
var Validator = function() { this.cache = []; // 添加策略事件
  this.add = function(value, method) { this.cache.push(function() { return strategies[method](value); }); }; // 檢查
  this.check = function() { for (let i = 0; i < this.cache.length; i++) { let valiFn = this.cache[i]; var data = valiFn(); // 開始檢查
      if (!data) { return false; } } return true; }; };

  此時,小彭同學需要進行權限驗證的條件為:掘金用戶、掘金等級 1 級以上,那么代碼就可以這么寫 :

// 小彭使用策略模式進行操作
var compose1 = function() { var validator = new Validator(); const data1 = { role: 'juejin', grade: 3 }; validator.add(data1.role, 'checkRole'); validator.add(data1.grade, 'checkGrade'); const result = validator.check(); return result; };

  然后另一個小伙伴阿寬,他可能需要進行權限驗證的條件為:掘金用戶、前端工程師,那么代碼就可以這么寫:

// 阿寬使用策略模式進行操作
var compose2 = function() { var validator = new Validator(); const data2 = { role: 'juejin', job: 'FE' }; validator.add(data2.role, 'checkRole'); validator.add(data2.job, 'checkJob'); const result = validator.check(); return result; };

  這是不是比一直瘋狂寫 if-else 好太多了呢?還有什么例子?最常見的表單驗證啊 ~ 對於表單字段(名稱、密碼、郵箱、....)我們可以使用策略模式去設計優化它。

總結一下:

1、策略規則:先定義各個策略是怎么樣的  ——   策略

2、校驗規則:將所需校驗的策略 add 至緩存起來,需要多少個策略就 add 多少策略   ——  組合

3、校驗規則:最后統一 check ,將所有策略均檢查,得到一個 統一值 即可   ——  統一檢查

3、什么時候用策略模式?

  當你負責的模塊,基本滿足以下情況時

  • 各判斷條件下的策略相互獨立且可復用
  • 策略內部邏輯相對復雜
  • 策略需要靈活組合

四、前端常用的 -  發布/訂閱模式

1、問題背景

  需求 : 申請成功后,需要觸發對應的訂單、消息、審核模塊對應邏輯。機智如我,我會如何做呢?

function applySuccess() { // 通知消息中心獲取最新內容
 MessageCenter.fetch(); // 更新訂單信息
 Order.update(); // 通知相關方審核
 Checker.alert(); }

  不就這樣寫嗎,還想咋滴!!!是的,這么寫沒得毛病,但是呢,我們來思考幾個問題:

  比如 MessageCenter.fetch() 是小彭寫的,他哪天把模塊的方法名改了,現在叫 MessageCenter.request(),你咋辦,你這塊邏輯改唄~

  再比如,你和阿寬並行開發的,阿寬負責訂單模塊,你一氣呵成寫下這段代碼,然后一運行,報錯了,一詢問,發現,原來阿寬昨晚去蹦迪了,原本今天應該完成的訂單模塊 Order.update(),延遲一天,那你就只能先注釋代碼,等依賴的模塊開發完了,你再回來添加這段邏輯咯~

  更可怕的是,你可能不只是涉及到這三個模塊,maybe 還有很多模塊,比如你申請成功,現在還需要上報申請日志,你總不能這樣寫吧?

function applySuccess() { // 通知消息中心獲取最新內容
 MessageCenter.fetch(); // 更新訂單信息
 Order.update(); // 通知相關方審核
 Checker.alert(); // maybe 更多
 Log.write(); ... }

  到這里,我們的 發布-訂閱模式 要按捺不住了。

2、發布 / 訂閱模式

  有沒有覺得這個 EventEmitter 好熟悉啊,這不是面試常會問的?發布-訂閱是一種消息范式,消息的發布者,不會將消息直接發送給特定的訂閱者,而是通過消息通道廣播出去,然后呢,訂閱者通過訂閱獲取到想要的消息。我們用 發布-訂閱模式 修改以下上邊的代碼:

const EventEmit = function() { this.events = {}; this.on = function(name, cb) { if (this.events[name]) { this.events[name].push(cb); } else { this.events[name] = [cb]; } }; this.trigger = function(name, ...arg) { if (this.events[name]) { this.events[name].forEach(eventListener => { eventListener(...arg); }); } }; };

  上邊我們寫好了一個 EventEmit,然后我們的業務代碼可以改成這樣 ~

let event = new EventEmit(); event.trigger('success'); MessageCenter.fetch() { event.on('success', () => { console.log('更新消息中心'); }); } Order.update() { event.on('success', () => { console.log('更新訂單信息'); }); } Checker.alert() { event.on('success', () => { console.log('通知管理員'); }); }

  但是這樣就沒問題了嗎?其實還是有弊端的,比如說,過多的使用發布訂閱,就會導致難以維護調用關系。所以,還是看大家的設計吧,這里只是讓大家知道,發布訂閱模式是個啥~

3、什么時候用發布-訂閱模式?

  當你負責的模塊,基本滿足以下情況時

  • 各模塊相互獨立
  • 存在一對多的依賴關系
  • 依賴模塊不穩定、依賴關系不穩定
  • 各模塊由不同的人員、團隊開發

五、前端常用的 - 裝飾器模式

1、裝飾器模式是什么?

  個人理解:是為了給一個函數賦能,增強它的某種能力,它能動態的添加對象的行為,也就是我傳入的就是一個對象。在 JS 世界中,世間萬物,皆為對象。

  大家過年,都會買桔子樹,那么我們買了桔子樹之后,都會往上邊掛一些紅包,搖身一變,“紅包桔子樹”,牛掰!這個的紅包就是裝飾器,它不對桔子樹原有的功能產生影響。

  再比如 React 中的高階組件 HOC,了解 React 的都知道,高階組件其實就是一個函數,接收一個組件作為參數,然后返回一個新的組件。那么我們現在寫一個高階組件 HOC,用它來裝飾 Target Component

import React from 'react'; const yellowHOC = WrapperComponent => { return class extends React.Component { render() { <div style={{ backgroundColor: 'yellow' }}>
        <WrapperComponent {...this.props} />
      </div>; } }; }; export default yellowHOC;

  定義了一個帶有裝飾黃色背景的高階組件,我們用它來裝飾目標組件

import React from 'react'; import yellowHOC from './yellowHOC'; class TargetComponent extends Reac.Compoment { render() { return <div>66666</div>; } } export default yellowHOC(TargetComponent);

  你看,我們這不就用到了裝飾器模式了嘛?什么,你還聽不懂?那我最后再舉一個例子,不知道這個例子,能不能幫助你們理解

const kuanWrite = function() { this.writeChinese = function() { console.log('我只會寫中文'); }; }; // 通過裝飾器給阿寬加上寫英文的能力
const Decorator = function(old) { this.oldWrite = old.writeChinese; this.writeEnglish = function() { console.log('給阿寬賦予寫英文的能力'); }; this.newWrite = function() { this.oldWrite(); this.writeEnglish(); }; }; const oldKuanWrite = new kuanWrite(); const decorator = new Decorator(oldKuanWrite); decorator.newWrite();

六、前端常用的 - 適配器模式

  個人理解,為了解決我們不兼容的問題,把一個類的接口換成我們想要的接口。

  舉個例子, 我想聽歌的時候,我發現我沒帶耳機,我的手機是 iphone 的,而現在我只有一個 Type-C 的耳機,為了能夠聽歌,我用了一個轉換器(也就是適配器),然后我就可以開心的聽歌了。

  再舉個真實業務中的例子,前段時間需要做一個需求,是這樣的。

  看這個圖,圖中紅色方框區域是一個資源列表展示組件,該列表數據,有三處來源:本地上傳、資源列表添加、后台返回資源。怎么理解呢?可以看到圖中,該流程主要是:

  • 右邊的“資源概況”是調接口,返回的一個 MaterialsList ,可以從右邊點擊 “+” 添加進來
  • 也可以通過選擇本地文件上傳
  • 如果是編輯場景下,還有后台接口返回的數據

  由於歷史原因和之前后台接口返回的數據結構問題,這三個數據格式是不同的。

// 本地資源文件上傳之后的數據結構
export interface ResourceLocalFileType { uuid: string; name: string; size: number; created: number; lastModified: number; resourceType: number; cancel: () => void; status: string; } // 資源概況接口返回的數據結構
export interface ResourcePackageFileType { uuid: string; materialName: string; materialLink: string; materialType: number; uid?: string; ext?: string; } // 原先數據后台返回的數據接口
export interface ResourceBackendFileType { uuid: string; resourceName: string; resourceLink: string; resourceType: number; version: string; ext: string; }

  很蛋疼,三個數據來源,三種時候數據結構,我們的資源列表組件是只能接收一種數據格式的列表,我不想破壞純展示型組件的內部邏輯,想保持該組件的職責:展示!那該怎么處理?采用適配器模式,將不同的數據結構適配成展示組件所能接受的數據結構

  首先,定義一個統一的數據格式:AdapterResourceFileType

export interface AdapterResourceType { uuid: string; created: number; fileNo: number; fileName: string; fileOrigin: string; fileStatus: string; fileInfo: { type: number; size?: number; [key: string]: any; }; // 本地圖片額外操作
  action?: { cancel?: () => void; [key: string]: any; }; }

  然后通過適配器模塊,適配成我們需要的接口API。

  在數據進行組件列表展示時,將來源不同的數據經過適配器處理,進行整合,然后傳遞給展示組件,以達到我們的目的

  適配器就是:為了解決我們不兼容的問題,把一個類的接口換成我們想要的接口,可能不太能幫助理解,我再舉個現實業務中的例子。比如你請求一個接口,接口返回 data = { user: xxx, userName: '' },但是你頁面中用的是 userInfo 和 nickName,不兼容啊,為了解決和這個不兼容問題,我們寫成這樣 return { userInfo: data.user, nickName: data.userName } ,這樣就將請求接口的字段換成我們想要的接口字段了。

  簡言之:適配器不是讓你擁有兩種能力,裝飾器才是給一個函數賦能,增強它的某種能力。適配器是:你給我B,但我只能接受A,需要適配器將B轉成我能接受的數據

七、前端常用的 - 代理模式

1、什么是代理模式

  我們再來講一個叫做 代理模式,說到代理哈,我腦海里第一個浮現的詞語 : “事件委托、事件代理”,這算嗎?算噠。我舉些例子,讓大家知道代理模式是個啥玩意。作為程序員嘛,女朋友比較難找,就算找到了,咱這么瘦弱,怕是保護不了啊,所以我花錢找了個保鏢來保護我,穩妥。這就是代理模式。

  你翻qiang嗎?你能 google 嗎?老實人哪會什么翻qiang,我是不會的,會我也說我不會。其實正常來講,我們直接訪問 google 是無響應的。那怎么辦呢,通過第三方代理服務器。小飛機?懂 ?

  門票都被搶光了,無奈之下,只能找黃牛,這里,黃牛就起了代理的作用,懂?

  程序世界的代理者也是如此,我們不直接操作原有對象,而是委托代理者去進行。代理者的作用,就是對我們的請求預先進行處理或轉接給實際對象

代理模式是為其它對象提供一種代理以控制這個對象的訪問,具體執行的功能還是這個對象本身,就比如說,我們發郵件,通過代理模式,那么代理者可以控制,決定發還是不發,但具體發的執行功能,是外部對象所決定,而不是代理者決定。

// 發郵件,不是qq郵箱的攔截
const emailList = ['qq.com', '163.com', 'gmail.com']; // 代理
const ProxyEmail = function(email) { if (emailList.includes(email)) { // 屏蔽處理
  } else { // 轉發,進行發郵件
    SendEmail.call(this, email); } }; const SendEmail = function(email) { // 發送郵件
}; // 外部調用代理
ProxyEmail('cvte.com'); ProxyEmail('ojbk.com');

  下邊再來舉一個例子,來至 《JavaScript 設計模式與開發實踐》

// 本體
var domImage = (function() { var imgEle = document.createElement('img'); document.body.appendChild(imgEle); return { setSrc: function(src) { imgEle.src = src; } }; })(); // 代理
var proxyImage = (function() { var img = new Image(); img.onload = function() { domImage.setSrc(this.src); // 圖片加載完設置真實圖片src
 }; return { setSrc: function(src) { domImage.setSrc('./loading.gif'); // 預先設置圖片src為loading圖
      img.src = src; } }; })(); // 外部調用
proxyImage.setSrc('./product.png');

2、代理模式與裝飾器模式區別:

  裝飾器模式是給自己添置更高級的裝備,增強自己的屬性;代理模式是和隊友分工合作,把自己不擅長、不想搞的領域交給隊友。

  比如說,你很柔弱,你保護不了女朋友,所以你通過健身,擁有一身肌肉。代理模式是,你柔弱,然后花錢請了個有八塊腹肌的保鏢,但是這個保鏢可以決定保護不保護你女朋友。

如何高效學習 —— 學習新的東西的時候,把它擴展成自己比較熟悉的知識,更容易記憶,而且不容易忘。

3、什么時候用代理模式?

  當你負責的模塊,基本滿足以下情況時:

  • 模塊職責單一且可復用
  • 兩個模塊間的交互需要一定限制關系

八、前端常用的 - 職責鏈模式

1、問題背景

  需求 :我們申請設備之后,接下來要選擇收貨地址,然后選擇責任人,而且必須是上一個成功,才能執行下一個。小伙伴們驚訝了,這不簡單嘛?

function applyDevice(data) { // 處理巴拉巴拉...
  let devices = {}; let nextData = Object.assign({}, data, devices); // 執行選擇收貨地址
 selectAddress(nextData); } function selectAddress(data) { // 處理巴拉巴拉...
  let address = {}; let nextData = Object.assign({}, data, address); // 執行選擇責任人
 selectChecker(nextData); } function selectChecker(data) { // 處理巴拉巴拉...
  let checker = {}; let nextData = Object.assign({}, data, checker); // 還有更多
}

  你看,這不就完事了,有啥難的,然后過了第二天,你又接了兩個新的流程需求,可能一個就兩步驟,一個可能多了“檢查庫存”這個步驟

  你不由驚了,哎呀媽呀,老夫聊發少年狂,鍵盤伺候,Ctrl C + Ctrl V,直接copy然后改一下邏輯??這里就是要講的責任鏈模式。

2、什么是責任鏈模式呢?

  我給你們找了個定義 : 避免請求發送者與接收者耦合在一起,讓多個對象都有可能接收請求,將這些對象連接成一條鏈,並且沿着這條鏈傳遞請求,直到有對象處理它為止。

const Chain = function(fn) { this.fn = fn; this.setNext = function() {} this.run = function() {} } const applyDevice = function() {} const chainApplyDevice = new Chain(applyDevice); const selectAddress = function() {} const chainSelectAddress = new Chain(selectAddress); const selectChecker = function() {} const chainSelectChecker = new Chain(selectChecker); // 運用責任鏈模式實現上邊功能
chainApplyDevice.setNext(chainSelectAddress).setNext(chainSelectChecker); chainApplyDevice.run();

  這樣的好處是啥?首先是解耦了各節點關系,之前的方式是 A 里邊要寫 B,B 里邊寫 C,但是這里不同了,你可以在 B 里邊啥都不寫

  其次,各節點靈活拆分重組,正如上邊你接的兩個新需求。比如兩個步驟的你就只需要這么寫完事

const applyLincense = function() {} const chainApplyLincense = new Chain(applyLincense); const selectChecker = function() {} const chainSelectChecker = new Chain(selectChecker); // 運用責任鏈模式實現上邊功能
chainApplyLincense.setNext(chainSelectChecker); chainApplyLincense.run();

3、什么時候使用責任鏈模式?

  當你負責的模塊,基本滿足以下情況時

  • 你負責的是一個完整流程,或你只負責流程中的某個環節
  • 各環節可復用
  • 各環節有一定的執行順序
  • 各環節可重組

  補充一下:不是讓大家強行套用設計模式,而是想表達:我們首先需要理解,其次需要形成一種肌肉記憶,正如前邊說的策略模式、發布-訂閱模式的例子一樣,大家在真實開發場景中肯定都有遇到,只是沒有想到,原來這就是設計模式,或者說,原來這里可以用到設計模式去設計。

學習鏈接:https://juejin.cn/post/6844904138707337229


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM