進擊的觀察者模式
Talk is cheap. Show me the code. (譯: 屁話少說, 放碼過來)
以下所有程式碼參見ofollow,noindex">Design pattern transformation .
// 商品的資訊: 價格 & 折扣 const data = { price: 100, discount: 0.8 } // 顧客資訊: 是否威會員 & 購買數量 & 總消費 & 購買時間戳 const customer = { "VIP": true, "quantity": 10, "total": 0, } // 總消費計算方式 total = (info) => { if(!info.VIP) { info.total = data.price * info.quantity; } else { info.total = data.price * data.discount * info.quantity; } } total(customer); console.log('customer', customer); // customer { VIP: true, quantity: 10, total: 800 } 複製程式碼
從程式碼中很容易看得出來, 我們就是想實現一個簡單的計費功能. 可現實中, 商品的價格可能並不是一成不變的.
data.price = 200
價格變動後, 我們需要及時地獲取總消費, 那麼就必須重新呼叫下total 計費.
total(customer); console.log('customer', customer); // customer { VIP: true, quantity: 10, total: 1600 } 複製程式碼
這是一個大資料時代, 任何資料都有價值. 現在, 我們還想要每次購買時的時間點.
const customer = { "VIP": true, "quantity": 10, "total": 0, +"timeStamp": 0 } // 獲取購買時間 purchaseTime = (info) => { info.timeStamp = Date.now(); } 複製程式碼
於是, 我們需要執行的函式就多了一個.
total(customer) purchaseTime(customer) console.log('customer', customer) // { VIP: true, quantity: 10, total: 1600, timeStamp: 1542293676297 } 複製程式碼
如果我們的需求還有很多, 而且不知一個customer呢. 那麼, 每次價格變化我們需要執行很多步驟, 每次啊, 麻煩得很.
+const customer1 = { +"VIP": false, +"quantity": 8, +"total": 0, +"timeStamp": 0 +} total(customer) purchaseTime(customer) func(customer) ... funcN(customer1) total(customer1) purchaseTime(customer1) func(customer1) ... funcN(customer) ... funcN(customerN) 複製程式碼
現在我們就對上面的程式碼進行觀察者模式改造.
用觀察者模式改造
從上面的例子中:chestnut::mahjong:️不難看出, 每次價格變化時, 我們都需要重複呼叫滿足需求的方法. 不妨想想, 如果我們把這些方法儲存起來, 等到價格變化時再去統一呼叫, 豈不是很方便. 那麼問題來了, 這和之前所說的觀察者模式(從觀察者模式說起)有什麼區別呢?在此, 我們試著用觀察者模式改造下. 首先觀察者模式都是一個套路. 先一個類維護一個列表, 對列表有增刪和通知更新功能. 另一個類則是提供了更新介面.
// 觀察目標類 class Subject { constructor() { this.observerList = [] } addObserver(observer) { this.observerList.push(observer) } notify(params) { this.observerList.forEach(observer => { observer.update(params) }) } } // 觀察者類 class Observer { constructor(fn) { this.update = fn } } 複製程式碼
接著, 把我們想要呼叫的方法包裝一下, 儲存起來.
// 將要重複使用的包裝一下 observer1 = new Observer(total) observer2 = new Observer(purchaseTime) // 存起來 let subject = new Subject() subject.addObserver(observer1) subject.addObserver(observer2) 複製程式碼
每次價格改變時, 只需要通知一下即可.
// 調整商品價格 data.price = 100 subject.notify(customer) subject.notify(customer1) 複製程式碼
改造結束. 初看起來, 可能變得繁瑣了. 但是, 遇到複雜的情況, 這不失是一個好辦法. 接下來, 我們看看結合Objec.defineProperty會有什麼驚喜.
與Objec.defineProperty結合
支付寶的花唄都可以自己還錢了, 我們為什麼還要別人管著:smirk:. 大家都知道經過Objec.defineProperty處理的物件, 在設定和獲取物件屬性的時候, 會自動觸發響應set和get方法. 利用這一點, 我們就可以做到生活自理了. 熟悉的配方, 熟悉的味道. 熟悉的套路我們不妨再走一遍.
// 觀察目標類 class Dependency { constructor() { this.watcherList = [] } addObserver(observer) { this.watcherList.push(observer) } notify(params) { this.watcherList.forEach(watcher => { watcher.update(params) }) } } // 觀察類 class Watcher { constructor(fn) { this.update = fn } } 複製程式碼
我們此行的目的, 是要在data.price 或data.discount改變時, 程式能夠自動觸發, 得到我們想要的結果. 換句話說, 通知更新的時機是在設定data.price或data.discount的時候.
Object.keys(data).forEach(key => { let value = data[key] const dep = new Dependency() Object.defineProperty(data, key, { set(newVal) { value = newVal dep.notify() }, get() { return value } }) }) 複製程式碼
物件的每個屬性都給了一個依賴例項, 管理自己的依賴. 考慮到customer有很多個, 需要通知到位. 另外, 新增依賴和管理依賴, 前者是因, 後者是果. 在管理之前我們需要想好怎麼新增依賴. 回頭看一看.
// 總消費計算方式 total = (info) => { if(!info.VIP) { info.total = data.price * info.quantity; } else { info.total = data.price * data.discount * info.quantity; } } // 獲取購買時間 purchaseTime = (info) => { info.timeStamp = Date.now(); } 複製程式碼
我們發現,total 函式依賴於data.price或data.discount的. 如果我們在獲取屬性時去新增依賴倒是一個好時機.
class Dependency { // 省略 } +Dependency.targey = null; class Watcher { constructor(fn, key) { this.update = fn +this.key = key +this.value = this.getter() } +getter() { +Dependency.targey = this; +// 出發下面的get() +this.value = data[this.key]; +Dependency.targey = null; +} } Object.keys(data).forEach(key => { let value = data[key] const dep = new Dependency() Object.defineProperty(data, key, { set(newVal) { value = newVal dep.notify() }, get() { +if (Dependency.targey) { +dep.addObserver(Dependency.targey) +} return value } }) }) 複製程式碼
然而purchaseTime 方法裡並沒有data.price或data.discount可以設定. 所以這個方法行不通. 那麼, 乾脆緊接著依賴例項去新增依賴吧. 同時考慮到多個customer, 我們封裝下.
// 與defineProperty結合 function defineReactive(data, watcherList, funcList) { Object.keys(data).forEach(key => { let value = data[key] const dep = new Dependency() funcList.forEach(func => { dep.addObserver(new Watcher(func)) }) Object.defineProperty(data, key, { set(newVal) { value = newVal watcherList.forEach(watcher => { dep.notify(watcher) }) }, get() { return value } }) }) } defineReactive(data, [customer, customer1], [total, purchaseTime]) 複製程式碼
大功告成, 價格變動時, 我們就會自動獲取到想要的結果了. 我都能自理了, 你花唄為嘛還不能自己還錢呢:unamused: