簡單易懂的設計模式(下)

一、觀察者模式

1. 什麼是觀察者模式

當對象之間存在一對多的依賴關係時,其中一個對象的狀態發生改變,所有依賴它的對象都會收到通知,這就是觀察者模式。

2. 實際場景

1. DOM 事件

開發過程中,最常見的觀察者模式場景就是 DOM 事件函數,先看看代碼:

document.body.addEventListener('click', () => {
    alert(2)
}, false)

body 節點被點擊時,觸發 alert(2),從觀察者模式來解釋,就是我們訂閱了 bdoy 節點的點擊事件,當點擊事件觸發,我們就會收到通知。

2. 網站登錄

網站登錄功能,想必大多數做過平臺需求的同學都實現過,當網站中的不同模塊,如 Header 模塊、Nav 模塊、正文模塊,都依賴登錄後獲取的用戶數據時,該怎麼去實現呢?

2.1 普通做法

先看代碼:

login.succ((data => {
    header.setAvatar(data.avatar) // 設置頭像
    nav.setAvatar(data.avatar) // 設置導航區的頭像
    list.refresh() // 刷新列表
})

這樣的代碼是不是特別熟悉?把依賴的方法,放在回調函數中。上述就是在登錄成功的回調函數中,添加了各模塊的方法。這麼做導致各個模塊和登錄模塊高度耦合,當新增了一個地址欄模塊時,不得不再次修改登錄模塊的回調函數,違反了開放-封閉原則。

2.2 觀察者模式

用觀察者模式,優化上述需求。

登錄模塊是一個訂閱對象,Header 模塊、Nav 模塊、正文模塊添加對登錄模塊的訂閱,當登錄模塊發生改變時,通知各個訂閱了登錄模塊的模塊。代碼如下:

// 登錄模塊js
// 登錄成功後,發佈“loginSucc”登錄成功消息,並傳遞data數據
login.succ(data=> {
    login.trigger('loginSucc', data)
})

// header模塊js
// 訂閱“loginSucc”登錄成功消息
login.listen('loginSucc', () => {
    header.setAvatar(data.avatar)
})

// nav模塊js
// 訂閱“loginSucc”登錄成功消息
login.listen('loginSucc', () => {
    nav.setAvatar(data.avatar)
})

上述代碼用觀察者模式重構了網站登錄功能,後續不管新增多少業務模塊,依賴登錄功能,都只需要在模塊內新增對登錄成功的訂閱,無需再改動登錄模塊。

3. 雙向數據綁定

雙向數據綁定也可以通過觀察者模式實現。

雙向指的是視圖 view 和模型 model,當視圖發生改變時,模型也發生變化,同樣,當模型發生改變,視圖也跟着同步變化。

分爲以下幾個步驟實現:

3.1 新建發佈-訂閱對象

新建一個發佈-訂閱對象,用於發佈消息,訂閱消息。

  • subscribe:訂閱函數,當其他對象添加訂閱消息時,將回調函數 pushcallbacks 對象數組中;
  • publish:發佈函數,當發佈消息時,觸發 callbacks 中該消息對應的 callback.
const Pubsub = {
    subscribe: function (ev, callback) {
        this._callbacks || (this._callbacks = {});
        (this._callbacks[ev] || (this._callbacks[ev] = [])).push(callback);
    },

    publish: function () {
        const args = [...arguments]
        const ev = args.shift()

        if (!this._callbacks) return
        if (!this._callbacks[ev]) return

        this._callbacks[ev].forEach(callback => {
            callback(...args)
        })
    }
}

3.2 ui 更新

3.2.1 發佈 ui 更新消息

註冊 documentkeyup / change 事件,當激活事件的 dom 元素擁有 data-bind 屬性時,說明 ui 正在更新,發佈 ui 更新消息通知訂閱者。

function eventHander (e) {
    const { target } = e
    const { value: propValue } = target

    const propNameWhole = target.getAttribute('data-bind')
    if (propNameWhole) {
        // 發佈ui更新消息
        Pubsub.publish('ui-update-event', { propNameWhole, propValue })
    }
}

console.log(document.addEventListener)
document.addEventListener('change', eventHander, false)
document.addEventListener('keyup', eventHander, false)
3.2.2 訂閱 model 更新消息

所有包含 data-bind 屬性的 dom 元素,訂閱 model 更新消息,當 model 更新時,ui 將會收到通知。

// 訂閱model更新消息,更新後所有符合條件的dom節點都會收到通知,進行更新
Pubsub.subscribe('model-update-event', function ({propNameWhole, propValue}) {
    const elements = document.querySelectorAll(`[data-bind="${propNameWhole}"]`)

    elements.forEach(element => {
        const elementTagName = element.tagName.toLowerCase()
        const formTypeTagNames = ['input', 'select', 'textarea']
        if (formTypeTagNames.includes(elementTagName)) {
            element.value = propValue
        } else {
            element.innerHTML = propValue
        }
    })
})

3.3 model 更新

3.3.1 訂閱 ui 更新消息

訂閱 ui 更新消息,當 ui 更新時,觸發 modal 更新。

class Bind {
    constructor () {
        this.modelName = ''
    }

    initModel ({ modelName }) {
        this.modelName = modelName

        // 訂閱ui更新消息
        Pubsub.subscribe('ui-update-event', ({propNameWhole, propValue}) => {
            const [ , _propName] = propNameWhole.split('.')
            this.updateModalData(_propName, propValue)
        })
    }
    
    // xxx省略xxx

    updateModalData (propName, propValue) {
        const propNameWhole = `${this.modelName}.${propName}`
        // 發佈model更新消息
        Pubsub.publish('model-update-event', { propNameWhole, propValue });
    }

}
3.3.2 發佈model更新消息

model 更新時,發佈 model 更新消息,此時,訂閱了 model 更新消息的 ui,將得到通知。

class Bind {
    constructor () {
        this.modelName = ''
    }
    
    // xxx省略xxx

    loadModalData (modelData) {
        for (let propName in modelData) {
            this.updateModalData(propName, modelData[propName])
        }
    }

    updateModalData (propName, propValue) {
        const propNameWhole = `${this.modelName}.${propName}`
        // 發佈model更新消息
        Pubsub.publish('model-update-event', { propNameWhole, propValue });
    }

}

3. 總結

從上文的實際場景例子可見,觀察者模式建立了一套觸發機制,幫助我們完成更松耦合的代碼編寫。但是也不能過度使用,否則會導致程序難以追蹤和理解。

二、裝飾器模式

1. 什麼是裝飾器模式

簡單來說,裝飾器模式就是給對象動態增加功能。

有一個鴨子對象,它會發聲 makeVoice, 會睡覺 sleep,但是因爲它還太小,不會走路,代碼如下:

const duck =  {
    makeVoice: () => {
        console.log('我會嘎嘎嘎啦')
    },
    sleep: () => {
        console.log('誰又不會睡覺呢')
    },
    init: function () {
        this.makeVoice()
        this.sleep()
    }
}

duck.init()

當它 3 個月大的時候,突然學會走路 walk 了,這個時候,怎麼在代碼中,爲鴨子 duck 添加走路 walk 功能呢?大多數情況下,我們會選擇直接修改鴨子 duck 方法,代碼如下:

const duck =  {
    makeVoice: () => {
        console.log('我會嘎嘎嘎啦')
    },
    sleep: () => {
        console.log('誰又不會睡覺呢')
    },
    walk: () => {
        console.log('哈哈哈,我會走路啦')
    },
    init: function () {
        this.makeVoice()
        this.sleep()
        this.walk()
    }
}

duck.init()

快樂的時光總是短暫的,鴨子越長越大,功能也越來越多。有一天,你請假出去玩,拜託朋友幫你照顧這隻鴨子,不巧,鴨子要下蛋了,朋友需要幫鴨子添加一個下蛋的功能,這就有點麻煩了因爲這是他第一次照顧這隻鴨子,他擔心如果直接在鴨子內部添加方法會影響到什麼。

於是他想到了一個好方法,不直接修改鴨子內部,而是通過一個外部函數,引用這個鴨子,併爲外部函數添加下蛋的功能。

代碼如下:

const before = function (fn, beforeFn) {
    return function () {
        beforeFn.apply(this, arguments)
        return fn.apply(this, arguments)
    }
}

const after = function (fn, afterFn) {
    return function () {
        const __ = fn.apply(this, arguments)
        afterFn.apply(this, arguments)
        return __
    }
}

const duck =  {
    makeVoice: function () {
        console.log('我會嘎嘎嘎啦')
    },
    sleep: function () {
        console.log('誰又不會睡覺呢')
    },
    walk: function () {
        console.log('哈哈哈,我會走路啦')
    },
    init: function () {
        this.makeVoice()
        this.sleep()
        this.walk()
    }
}

after(duck.init, function egg () {
    console.log('生蛋快樂~')
}).apply(duck)

這就是裝飾器模式,動態的爲鴨子添加功能,而不直接修改鴨子本身。

2. 實際場景

1. 數據上報

自定義事件的數據上報一般都依賴於點擊事件,那麼這個點擊事件既要承擔原本的功能,又要承擔數據上報的功能。

1.1 普通做法

先上代碼:

const loginBtnClick = () => {
    console.log('去登錄')
    console.log('去上報')
} 

好像沒毛病,這樣的代碼中項目中隨處可見,逃避(面向過程編程)雖可恥但有用。

1.2 裝飾器模式做法

可以通過裝飾器模式來重構上述代碼,將職責劃分更細,代碼松耦合,可複用性更高。


const after = function (fn, afterFn) {
    return function () {
        const __ = fn.apply(this, arguments)
        afterFn.apply(this, arguments)
        return __
    }
}

const showLogin = function () {
    console.log('去登錄')
}

const log = function () {
    console.log('去上報')
}

const loginBtnClick = after(showLogin, log)

loginBtnClick()

2. 動態增加參數

一個常規的 ajax 請求參數包括 type / url / param,當突發一個特殊情況,需要在 ajax 的參數中,新增一個 token 參數。

2.1 普通做法

先上代碼:

const ajax = function (type, url, param) {
    // 新增token參數
    param.token = 'xxx'
    // ...ajax請求...省略
}

好了,又一次違反開放-封閉原則,直接修改了 ajax 函數內部.

2.2 裝飾器做法

通過裝飾器模式,在 ajax 調用之前,爲 ajax 增加 token 參數,代碼如下:

const before = function (fn, beforeFn) {
    return function () {
        beforeFn.apply(this, arguments)
        return fn.apply(this, arguments)
    }
}

let ajax = function (type, url, param) {
    console.log(arguments)
    // ...ajax請求...省略
}

ajax = before(ajax, function (type, url, param) {
    console.log(param)
    param.token = 'xxx'
})

ajax('type', 'url', {name: 'tj'})

這樣做可以減少 ajax 函數的職責,提高了 ajax 函數的可複用性,

3. 總結

本文通過給鴨子函數動態增加功能、數據上報、動態增加參數 3 個例子,講述了裝飾器模式的應用場景及帶給我們的好處。

裝飾器模式,讓對象更加穩定,且易於複用。而不穩定的功能,則可以在個性化定製時進行動態添加。

三、職責鏈模式

1. 什麼是職責鏈模式

職責鏈模式就是當一個對象 a,有多種可能的請求對象 bcde 時,我們爲 bcde 分別定義一個職責,組成一條職責鏈,這樣 a 只需要找到 b 發起請求,然後沿着職責鏈繼續請求,直到找到一個對象來處理 a

女孩子們都喜歡結伴喫飯,我現在要找一個人一起喫飯,代碼如下:

嗯.....女程序員確實是這樣,喫個飯也要寫代碼發請求的。

const [ astrid, brooke, calliope ] = [{
    name: 'astrid',
    requirement: '我要喫湘菜'
},{
    name: 'brooke',
    requirement: '我要找10個人一起喫飯'
},{
    name: 'calliope',
    requirement: '我要和男朋友一起喫飯'
}]

// 是否滿足Astrid的要求
function isSatisfyAstrid (user) { 
    // ... 省略... 
}

// 是否滿足Brooke的要求
function isSatisfyBrooke (user) { 
    // ... 省略... 
}

// 是否滿足Calliope的要求
function isSatisfyCalliope (user) { 
    // ... 省略... 
}

function eatDinner () {
    if (isSatisfyAstrid()) {
        console.log(`我可以和 astrid 一起喫晚飯啦`)
    } else if (isSatisfyBrooke()) {
        console.log(`我可以和 brooke 一起喫晚飯啦`)
    } else if (isSatisfyCalliope()) {
        console.log(`我可以和 calliope 一起喫晚飯啦`)
    } else {
        console.log(`哎呀,我要一個人喫晚飯啦`)
    }
}

由於 astridbrookecalliope 喫晚飯的要求不同,我需要一個個去發起晚餐請求,直到找到答應和我一起喫晚飯的人。

在這裏,我假設 astrid 的要求是要喫湘菜,brooke 的要求是要找 10 個人湊一桌一起喫,calliope 的要求是隻想和男朋友一起喫飯。

上述代碼用 if-else 的做法非常死板,假如我又多了個朋友 davi,我必須再次修改 eatDinner 方法,違反了開放-封閉原則,不易於維護。

下面使用職責鏈來優化上述代碼,代碼如下:

// 給每個人定義一個職責
const chainOrderA = new Chain(isSatisfyAstrid)
const chainOrderB = new Chain(isSatisfyBrooke)
const chainOrderC = new Chain(isSatisfyCalliope)

// 設置一下職責鏈的順序
chainOrderA.setNextSuccessor(chainOrderB)
chainOrderB.setNextSuccessor(chainOrderC)

// 發起請求,這時我只需要向職責鏈上的第一個人請求
function eatDinner () {
    chainOrder.passRequest() // 發起請求
}

將職責作爲轉入 Chain 函數,並通過 setNextSuccessor 定義該職責的下一個職責函數,組成一條 chainOrderA -> chainOrderB -> chainOrderC 職責鏈,這時,我只需要向 astrid 發起請求,如果請求失敗,將會沿着職責鏈繼續請求,直到找到和我一起喫晚飯的人。

下面將講述在實際場景中怎麼使用職責鏈模式,怎麼實現 Chain 方法,請繼續往下看。

2. 實際場景

1. 618 預售商品訂單

電商網站免不得會推出商品預售活動,假設在 618 之前,預付 500 定金,可獲得 100 元優惠券,預付 200 元定金,可獲得 50 優惠券,未付定金則無優惠券。618 當天的購買事件如下:

1.1 普通做法

先上代碼。

本文代碼僅舉例說明,和業務無關。

const order = function (orderType) {
    if (orderType === 500) {
        console.log('已預付500定金,享有100優惠券')
    } else if (orderType === 200) {
        console.log('已預付200定金,享有50元優惠券')
    } else {
        console.log('未付定金,無優惠')
    }
}

order(500) // '已預付500定金,享有100優惠券'

熟悉的代碼,一長段的 if-else 判斷,不利於維護。

1.2 職責鏈模式

定義一個職責類 Chain

  • 接收一個職責函數 fn 作爲參數;
  • setNextSuccessor 指定該職責的下一個職責函數;
  • passRequest 發起對職責函數 fn 的請求;
    • 如果返回結果是nextSuccesstor,說明請求失敗,繼續請求職責鏈上的下一個職責函數;
    • 如果不是返回 nextSuccesstor,說明找到了接收請求的對象,返回請求結果,不再繼續執行職責鏈上的下一個職責函數。

代碼如下:

const Chain = function(fn) {
  this.fn = fn;
  this.successor = null;
};

Chain.prototype.setNextSuccessor = function(successor) {
  return this.successor = successor;
}

Chain.prototype.passRequest = function() {
  const ret = this.fn.apply(this, arguments)
  if (ret === 'nextSuccessor') {
    return this.successor && this.successor.passRequest.apply(this.successor, arguments);
  }
  return ret;
};

然後定義職責類實例,通過 setNextSuccessor 組成職責鏈,代碼如下:

const order500 = function (orderType) {
  if (orderType === 500) {
      console.log('已預付500定金,享有100優惠券')
  } else {
    return 'nextSuccessor'
  }
} 

const order200 = function (orderType) {
  if (orderType === 200) {
    console.log('已預付200定金,享有50元優惠券')
  } else {
    return 'nextSuccessor'
  }
} 

const chainOrder500 = new Chain(order500)

const chainOrder200 = new Chain(order200)

chainOrder500.setNextSuccessor(chainOrder200)

chainOrder500.passRequest(200)

上述代碼將 chainOrder500chainOrder200 組成一條職責鏈,不管用戶是哪種類型,都只需要向 chainOrder500 發起請求,如果 chainOrder500 無法處理請求,就會繼續沿着職責鏈發起請求,直到找到能處理請求的職責方法。

通過職責鏈模式,解耦了請求發送者和多個接收者之間的複雜關係,不再需要知道具體哪個接收者來接收發送的請求,只需要向職責鏈的第一個階段發起請求。

3. 總結

職責鏈模式,幫助我們管理代碼,降低發起請求和接收請求對象之間的耦合。

職責鏈模式中的節點數量和順序是可以自由變動的,可以在運行時決定鏈中包含哪些節點。

可通過 github源碼 進行實操練習。

希望能對你有所幫助,感謝閱讀❤️


· 往期精彩 ·

【簡單易懂的設計模式(上)】

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章