进击的观察者模式

1,548 阅读4分钟

原文链接: 进击的观察者模式

商品信息改变带来的烦恼

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])

大功告成, 价格变动时, 我们就会自动获取到想要的结果了. 我都能自理了, 你花呗为嘛还不能自己还钱呢😒

观察者模式系列: