進擊的觀察者模式

夜曉宸發表於2019-03-03

原文連結: 進擊的觀察者模式

商品資訊改變帶來的煩惱

Talk is cheap. Show me the code. (譯: 屁話少說, 放碼過來)

以下所有程式碼參見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)

複製程式碼

現在我們就對上面的程式碼進行觀察者模式改造.

用觀察者模式改造

從上面的例子中??️不難看出, 每次價格變化時, 我們都需要重複呼叫滿足需求的方法. 不妨想想, 如果我們把這些方法儲存起來, 等到價格變化時再去統一呼叫, 豈不是很方便. 那麼問題來了, 這和之前所說的觀察者模式(從觀察者模式說起)有什麼區別呢? 在此, 我們試著用觀察者模式改造下.
首先觀察者模式都是一個套路. 先一個類維護一個列表, 對列表有增刪和通知更新功能. 另一個類則是提供了更新介面.

// 觀察目標類
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結合

支付寶的花唄都可以自己還錢了?, 我們為什麼還要別人管著?. 大家都知道經過 Objec.defineProperty 處理的物件, 在設定和獲取物件屬性的時候, 會自動觸發響應 setget 方法. 利用這一點, 我們就可以做到生活自理了. 熟悉的配方, 熟悉的味道. 熟悉的套路我們不妨再走一遍.

// 觀察目標類
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.pricedata.discount 改變時, 程式能夠自動觸發, 得到我們想要的結果. 換句話說, 通知更新的時機是在設定 data.pricedata.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.pricedata.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.pricedata.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])
複製程式碼

大功告成, 價格變動時, 我們就會自動獲取到想要的結果了. 我都能自理了, 你花唄為嘛還不能自己還錢呢?

觀察者模式系列:

相關文章