有沒有一種可能,這些設計模式其實你都用過了?

語言: CN / TW / HK

theme: smartblue highlight: arduino-light


by - 星期一

一、前言

往往提到設計模式這個概念,有些人都會望而卻步,覺得它是一個極其抽象而且高深莫測的東西,想著我又不造火箭(框架),搞搞業務或者造些小輪子用到設計模式也太小題大做了吧!(ps: 這是對設計模式很大的誤解)

正巧,小夥伴前面已經分享了好幾個框架的原始碼,也是時候趁熱打鐵把設計模式撿起來了。

通過閱讀本文,你會發現其實很多設計模式我們在實際開發中都用到了或者說用到了它們的核心思想,只是我們並不知道這樣寫程式碼叫某種設計模式以及它的優缺點。

二、介紹

1. 什麼是設計模式?

通俗的講,設計模式就是一種書寫程式碼的方式,是為了解決某些或者某類特定的問題給出的簡潔優雅的解決方案。

2. 概覽

三、設計模式

1. 單例模式

定義與實現

顧名思義:這種模式就是單一的例項。

特點:一個建構函式只能有一個例項,無論new多少次,都是同一個例項。

顯然,在這種模式中,我們需要儲存一個變數來維護這個不變的例項,呼叫的時候始終返回這個例項就可以。

自然,就可以簡單的如此實現:

```js // 定義一個Person類 const Person = function () { this.name = '張三'; }; Person.prototype.getName = function () { return this.name; };

let instance = null; // 儲存例項的變數 function singleton() { if(!instance) { // 如果不存在該例項,則建立 instance = new Person();
} // 如果存在,直接返回 return instance; }

let p1 = new singleton() let p2 = new singleton() console.log(p1 === p1) // true

```

由此可以看出,單例模式的核心就是兩個元素:儲存例項的變數判斷是否存在並返回例項的方法

既然需要將變數一直儲存,就有了更優化的方案:閉包。來實現變數私有化。

js const singleton = (function () { // 會被儲存在一個不會被銷燬的函式執行空間裡面 let instance = null; return function () { if (!instance) { instance = new Person(); } return instance; }; })();

除此之外,單例模式還有更多優化的實現方案,例如惰性模式來實現使用時建立例項等等,我們可以根據使用中的實際場景來對它進行優化。

應用場景

  • 全域性模態框
  • Vuex 
  • ES Module, 用export匯出的例項物件

    先來看看這個問題,下面這段程式碼中,import會執行幾次?

    import { A } from './a.js' import { A } from './a.js' 答案當然是1次,等同於import { A } from './a.js'

    第二個例子,import會執行幾次? import { A } from './a.js' import { B } from './a.js' 還是一次,等同於import { A, B } from './a.js'

    那如果在不同檔案中執行同一個import呢?匯出的還是同一個物件。 所以,如果多次重複執行同一句import語句,那麼只會執行一次,而不會執行多次。也就是說, import語句是單例模式。 在這裡再丟擲一個問題,那如果通過 Webpack 將 ES6 轉成ES5 以後呢,這種方式還會是單例物件嗎?

2. 觀察者模式和釋出訂閱模式

這兩種設計模式應該是我們最耳熟能詳的模式了,把它們放在一起是因為很多人認為它們其實是一種,也有人認為是兩種不同的設計模式。先讓我們分別瞭解下這兩種設計模式。

觀察者模式:

特點: 當一個物件的狀態發生改變時,所有依賴於它的物件都將得到通知,並自動更新

根據它的特點,我們需要兩個建構函式來實現,一個被觀察者,一個觀察者。

被觀察者建構函式中需要有的內容:

  • 一個狀態
  • 一個數組,記錄觀察者
  • 一個能設定自己的狀態的方法
  • 一個通知觀察者更新的方法
  • 一個新增觀察者的方法
  • 一個刪除觀察者的方法

```js // 建立被觀察者 class Subject { constructor(state) { this.state = state; // 狀態 this.observers = []; // 觀察者 }

// 狀態更新 setState(state) { this.state = state; this.notifyAllObservers(); }

// 通知觀察者更新 notifyAllObservers() { this.observers.forEach((observer) => { observer.update(this.state); }); }

// 註冊觀察者 addObserver(observer) { this.observers.push(observer); }

// 刪除觀察者 removeObserver(observer) { this.observers = this.observers.filter((obs) => obs !== observer); } } ```

觀察者建構函式需要有的內容:

  • 一個名字或者說身份
  • 一個要更新的方法

```js // 建立觀察者 class observer { constructor(name) { this.name = name; }

update(state) { console.log(${this.name} update: ${state}); } } ```

使用:

```js // 建立一個被觀察者 - 學生,觀察狀態是學習 const student = new Subject('上學'); // 建立兩個觀察者 const headMaster = new observer('班主任'); const father = new observer('家長'); // 給被觀察者新增觀察者 student.addObserver(headMaster); student.addObserver(father); // 更新狀態 student.setState('網咖');

// console // 班主任 update: 網咖 // 家長 update: 網咖 ```

釋出訂閱模式

特點: 基於一個事件通道,訂閱者通過自定義事件訂閱事件,釋出者通過釋出事件的方式通知訂閱者

有點繞,其實他的核心思想就是通過一個建構函式管理一個訊息佇列,這個訊息佇列是個狀態和更新行為的集合。

分析下這個建構函式的主要組成:

  • 一個訊息佇列格式be like:[{stateA: [ fn1 , fn2 ]}, {stateB: [fn1, fn2]}]
  • 一個向訊息佇列新增內容的方法
  • 一個刪除訊息佇列內容的方法
  • 一個觸發訊息佇列內容的方法

```js class observer { constructor() { this.message = []; }

// 新增訊息 add(state, fn) { if (!this.message[state]) { // 訊息佇列還未註冊此狀態相關事件 this.message[state] = []; } this.message[state].push(fn); }

// 刪除訊息 remove(state, fn) { if (!this.message[state]) { return; } if (!fn) { // 刪除所有相關事件 delete this.message[state]; return; } // 刪除指定事件 this.message[state] = this.message[state].filter((item) => item !== fn); }

// 更新訊息 update(state) { if (!this.message[state]) { return; } this.message[state].forEach((item) => item()); } }

// 建立一個例項 - 第三方平臺 const platform = new observer(); // 拜託平臺觀察一些事情 platform.add('書到了', () => { console.log('發簡訊:您訂閱的書到了'); }); platform.add('書到了', () => { console.log('郵寄到xxx地址'); }); // 更新狀態 platform.update('書到了'); // 輸出 // 發簡訊:您訂閱的書到了 // 郵寄到xxx地址 ``` 區別:

觀察者模式把觀察者物件維護在目標物件中的,需要釋出訊息時直接發訊息給觀察者。在觀察者模式中,目標物件本身是知道觀察者存在的。

釋出/訂閱模式中,釋出者並不維護訂閱者,也不知道訂閱者的存在,所以也不會直接通知訂閱者,而是通知排程中心,由排程中心通知訂閱者。

應用場景:

  • 事件監聽 addEventListener()

  • vue響應式原理

  • mobx

    根據前面mob原始碼的分享,我們都知道mobx的核心是採用了觀察者模式的,這裡我們來回顧一下。

    在mobx中,我們需要一個值或者一個物件更新時,觸發相應的響應。

    mobx原始碼中,在get方法中,通過一系列的處理,最終將值包裝成ObservableValue,這個ObservableValue就是被觀察者角色,觀察者就是在這個過程中讀取過值的reaction們,在set值時,最終會觸發notifyListeners通知觀察者更新。

3. 策略模式

定義與實現

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

結合一個實際例子就很好理解了,例如雙11的購物車結算,經歷過雙十一都知道每件商品都有自己的折扣和滿減方式,199-20、200-30、前一小時兩件5折等等。那麼我們首先想到的最順手的解決方式就是if/else嘛,雖然可以解決,但是逼死程式碼潔癖患者哈哈,而且下次雙十二的結算呢?複製貼上?顯然一個能夠複用的折扣計算方法會比較實用。

我們來看看策略模式如何來解決此類問題,先來看看策略模式包含有什麼:

  • 一個定義了多種方案的Strategy物件
  • 一個執行指定方案的方法
  • 一個新增策略的方法
  • 一個刪除策略的方法

```js class ShopCartCalc { constructor() { this.strategy = { '200_30': function (price) { return price - parseInt(price / 200) * 30; }, '199_20': function (price) { return price - parseInt(price / 199) * 20; }, half_off_two: function (price) { return price - parseInt(price / 2); } }; }

// 新增策略 add(discount, fn) { this.strategy[discount] = fn; }

// 刪除策略 remove(discount) { delete this.strategy[discount]; }

// 計算價格 getPrice(price, discount) { if (!this.strategy[discount]) { return price; // 沒有這個折扣,返回原價 } return this.strategydiscount; } }

const calcPrice = new ShopCartCalc(); calcPrice.add('80%', (price) => price * 0.8); console.log(calcPrice.getPrice(100, '200_30')); console.log(calcPrice.getPrice(100, '80%'));

```

應用場景

  • 表單驗證
  • 購物車結算
  • 獎金計算

4.  介面卡模式

定義與實現

別名:包裝器。目的是為了不改變原始碼的情況下,讓兩個不相容的物件在同樣的呼叫下正常運作。

可以理解為介面卡模式就是基於源物件的再封裝來處理相容問題。

介面卡模式中有三種角色:

  • 目標(target)
  • 介面卡(adapter)
  • 適配者(adaptee)

```js // 目標類 class Target { constructor() { this.name = 'Target'; } request() { console.log('Target request'); } }

// 適配者類 class Adaptee { constructor() { this.name = 'Adaptee'; } // 自身的方法,不相容目標類的方法 specificRequest() { console.log('Adaptee specificRequest'); } }

// 介面卡類 class Adapter extends Adaptee { constructor() { super(); this.name = 'Adapter'; } // 適配目標類的方法 request() { // 呼叫適配者的方法 this.specificRequest(); } }

const target = new Target(); target.request(); const adapter = new Adapter(); adapter.request(); ```

應用場景

  • vue的計算屬性實現
  • axios的介面卡

```js // default.js function getDefaultAdapter() { var adapter; if (typeof XMLHttpRequest !== 'undefined') { // For browsers use XHR adapter adapter = require('./adapters/xhr'); } else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') { // For node use HTTP adapter adapter = require('./adapters/http'); } return adapter; }

// http介面卡 module.exports = function httpAdapter(config) { return new Promise(function dispatchHttpRequest(resolvePromise, rejectPromise) { // ... }

// xhr介面卡 module.exports = function xhrAdapter(config) { return new Promise(function dispatchXhrRequest(resolve, reject) { // ... } ```

五、設計原則

設計模式的目的就是為了讓程式碼有更好的複用性,可讀性,增加可維護性和更易於擴充套件。所以也就有了一些原則來約束我們如何設計程式碼。

SOLID原則:

S - 【單一職責原則】:一個物件或方法應該只負責一件事

O - 【開放封閉原則】:對擴充套件開放,對修改關閉

L - 【里氏置換原則】:子類能夠覆蓋、替換父類,且不破壞程式的正常執行(主要適用於繼承)

I - 【介面獨立原則】:保持介面的單一獨立(js使用較少)

D - 【依賴倒置原則】:面向介面程式設計,依賴於抽象而不依賴於具體。(js使用較少)

這五大原則中,S、O這兩個原則更為重要。

六、總結

設計模式的核心是觀察整個邏輯中的變與不變,將變與不變分離,達到使變化的部分靈活,不變的地方穩定的目的。

希望通過這一次對設計模式的初探,起到一個拋磚引玉的作用,打破部分同學心中對設計模式的陌生感和畏懼感。也希望我們在實際開發中能夠合理運用設計模式,多考慮設計原則,更輕鬆愉快的開發。

招賢納士

青藤前端團隊是一個年輕多元化的團隊,坐落在有九省通衢之稱的武漢。我們團隊現在由 20+ 名前端小夥伴構成,平均年齡26歲,日常的核心業務是網路安全產品,此外還在基礎架構、效率工程、視覺化、體驗創新等多個方面開展了許多技術探索與建設。在這裡你有機會挑戰類阿里雲的管理後臺、多產品矩陣的大型前端應用、安全場景下的視覺化(網路、溯源、大屏)、基於Node.js的全棧開發等等。

如果你追求更好的使用者體驗,渴望在業務/技術上折騰出點的成果,歡迎來撩~ [email protected]