你好,我是小甲。
今天我们要继续深入探索观察者模式。在上期《一文读懂观察者模式:通俗易懂的JS例子帮你理解》中,我们已经了解了观察者模式的基础知识。现在我们要进一步深入理解观察者模式,以及如何在实际项目中应用它。如果你还不太了解观察者模式,建议你看看我们上期的文章。如果你喜欢我们的内容,记得点赞和分享给更多朋友哦。
依照惯例,我们先来看下最终实现的效果图:
接下来,让我们正式开始吧~
1. 观察者模式的缺点及如何解决
虽然观察者模式有很多优点,但是它也并非完美。
在上期文章《一文读懂观察者模式:通俗易懂的JS例子帮你理解》中,我们有提到观察者模式的三个优点,如果你对此还不甚了解,可以去看看。
除了我们提到的那些优点,还有下面常见的两个问题。
1.1 依赖关系过于紧密
如果观察者和被观察者之间的依赖关系过于紧密,那么一旦被观察者发生变化,可能会引发观察者的连锁反应,使得程序变得难以理解和维护。
比如我们有一个电子商务网站,这个网站有一个购物车模块,用户可以在这个模块中添加或删除商品。另外,我们还有一个促销模块,它根据购物车中的商品数量提供一些优惠。当购物车中的商品数量改变时,促销模块需要更新以反映新的优惠。这里的购物车模块就是被观察者,促销模块是观察者。
然后,我们引入了一个新的模块,称为库存模块。这个模块需要监听购物车的改变,以便更新库存。然而,我们的产品经理决定,每当库存发生变化时,我们也需要更新促销模块,因为我们可能会根据库存提供一些特别的优惠。
这就引入了一个问题:当购物车的状态改变时,促销模块和库存模块都会被通知。然后,库存模块的状态改变会再次通知促销模块。这种连锁反映使程序变得难以理解和维护,因为我们很难预测一个改变会导致多少次的更新。
以下是一个简化的示例代码:
// 购物车模块
class Cart {
constructor() {
this.observers = []; // 观察者列表
this.items = []; // 购物车中的商品列表
}
attach(observer) {
this.observers.push(observer); // 向观察者列表添加新的观察者
}
addItem(item) {
this.items.push(item); // 向购物车添加新的商品
this.notifyAllObservers(); // 通知所有的观察者
}
notifyAllObservers() {
for (let observer of this.observers) {
observer.update(this); // 通知每个观察者进行更新
}
}
}
// 促销模块
class Promotion {
constructor(cart) {
this.cart = cart; // 与促销相关的购物车对象
this.cart.attach(this); // 将当前的促销对象添加到购物车的观察者列表
}
update() {
console.log("Promotion updated!"); // 更新促销信息
// 根据购物车中的商品更新促销信息
}
}
// 库存模块
class Inventory {
constructor(cart) {
this.cart = cart; // 与库存相关的购物车对象
this.cart.attach(this); // 将当前的库存对象添加到购物车的观察者列表
this.promotion = new Promotion(cart); // 创建一个新的促销对象并将购物车对象传给它
}
update() {
console.log("Inventory updated!"); // 更新库存信息
// 根据购物车中的商品更新库存信息
// 再次通知促销模块进行更新
this.promotion.update();
}
}
const cart = new Cart(); // 创建一个新的购物车对象
const inventory = new Inventory(cart); // 创建库存对象并传入购物车对象
cart.addItem("item1"); // 这将触发一系列的更新操作
这段代码中,Cart 是被观察者,Promotion 和 Inventory 是观察者。当我们向购物车添加一个商品时,购物车会通知所有观察者(Promotion 和 Inventory)。然后,库存模块的更新又会触发促销模块的更新。这种连锁反应会使这段代码难以理解和维护。
我们用 node 在终端执行这段代码的效果如下:
这就是观察者和被观察者之间的依赖关系过于紧密的一种典型场景。
咱就说,类似的模块不用太多,有三五个这样的就够你头疼了吧~
1.2 处理观察者的更新可能很复杂
如果观察者的更新逻辑很复杂,或者需要在多个观察者之间协调,那么代码可能会变得非常复杂。
有时候我们不想让代码变得越来越负责,但是写着写着就复杂了……
难道就没有办法解决上面的问题吗?当然有!我们可以考虑下面这两个方案:
-
使用中介者模式来减少观察者和被观察者之间的直接交互: 中介者模式可以将复杂的交互逻辑封装在一个中介者对象中,使得观察者和被观察者之间的交互变得更加简单。
关于中介者模式,我们将在未来与大家深入探讨。就像我们正在讨论的观察者模式一样,你可以先关注我的微信公众号码上花甲当我们发布文章时,你(作为订阅者)会及时收到通知,这样就不会错过我们的内容了。
-
将复杂的更新逻辑抽象到单独的对象中: 如果处理观察者的更新很复杂,可以考虑将这部分逻辑抽象到个单独的对象中,使得代码更加模块化和易于理解。
2. 观察者模式在前端开发中的应用
观察者模式在前端开发中有着非常广泛的应用。比如,你可能已经在使用一些基于观察者模式的库或框架。如 Redux 或者 Vue.js。
- 在 Redux 中,store 就是一个被观察者,它保存了应用的状态。每当状态发生变化,store 就会通知所有的观察者(即 reducers)进行更新。
- 在 Vue.js 中,每一个 Vue 实例都是一个被观察者。当实例的数据发生变化时,Vue 会通知所有依赖这些数据的观察者(即 watchers)进行更新。
3. 复杂示例:天气预报系统
前面的理论部分介绍完了,现在让我们通过一个更复杂的 JS 示例来看看观察者模式的强大之处。在这个例子中,我们要创建一个天气预报系统。
3.1 创建发布者
class WeatherStation {
constructor() {
this.observers = []; // 观察者数组,用于存储观察者对象
this.temperature = 0; // 温度初始值为0
}
}
我们可以看到,在 WeatherState 类中有一个 observers 数组,这个数组用来存储所有观察者。同时,它还有一个 temperature 属性,代表当前的温度,默认值为 0。
接着,我们要给这个类添加一些功能。首先,我们需要能够添加和移除观察者。让我们来写一下这两个方法:
class WeatherStation {
// ...
addObserver(observer) {
this.observers.push(observer); // 将观察者对象添加到观察者数组中
}
removeObserver(observer) {
const index = this.observers.indexOf(observer); // 获取观察者对象在数组中的索引
if (index > -1) {
this.observers.splice(index, 1); // 如果观察者对象存在于数组中,则从数组中移除
}
}
// ...
}
现在这个天气预报系统的 WeatherStation 类已经可以添加和移除观察者了。接下来,我们需要能够设置温度并通知所有的观察者。
class WeatherStation {
// ...
setTemperature(temp) {
console.log(`天气预报工作站: 当前温度为 ${temp}℃`); // 打印新的温度测量值
this.temperature = temp; // 更新温度值
this.notifyObservers(); // 通知所有观察者对象
}
notifyObservers() {
for(let observer of this.observers) {
observer.update(this.temperature); // 调用每个观察者对象的update方法,传递当前温度值作为参数
}
}
}
现在,天气预报系统已经可以设置温度,并且在设置温度后,它会通知所有观察者。
到这里,我们已经实现一大半的功能 ~
然后,我们需要创建一些观察者。
3.2 创建具体的订阅者
首先,我们创建一个“温度显示器”的观察者类:
class TemperatureDisplay {
constructor(weatherStation) {
this.weatherStation = weatherStation; // 引用WeatherStation对象
this.weatherStation.addObserver(this); // 将自身作为观察者对象添加到WeatherStation的观察者数组中
}
update(temperature) {
console.log(`[订阅者-温度显示器]: 我得赶紧把显示的温度更新为 ${temperature}℃`); // 打印需要更新显示的温度
// 这里是更新显示的逻辑
}
}
这样,天气预报系统的温度改变时,观察者 TemperatureDisplay 类中的 update 方法就会接收到 WeatherStation 发布的最新温度。
接下来,我们再创建一个代表“风扇”的观察者 Fan:
class Fan {
constructor(weatherStation) {
this.weatherStation = weatherStation; // 引用WeatherStation对象
this.weatherStation.addObserver(this); // 将自身作为观察者对象添加到WeatherStation的观察者数组中
}
update(temperature) {
if (temperature > 25) {
console.log("[订阅者-风扇]: 太 TM 热了,我得赶紧开风扇凉快凉快..."); // 如果温度大于25度,则打印开启风扇的信息
} else {
console.log("[订阅者-风扇]: 太 TM 冷了,赶紧把风扇给关了吧先......"); // 如果温度小于等于25度,则打印关闭风扇的信息
}
}
}
当温度超过 25 摄氏度时,这个观察者便会“打开风扇”。当温度降到 25 摄氏度或以下时,它便会“关闭风扇”。
现在,我们的观察者模式的所有部分都已准备就绪。让我们来执行看看效果:
const weatherStation = new WeatherStation(); // 创建WeatherStation对象
const tempDisplay = new TemperatureDisplay(weatherStation); // 创建TemperatureDisplay对象,并传入WeatherStation对象
const fan = new Fan(weatherStation); // 创建Fan对象,并传入WeatherStation对象
weatherStation.setTemperature(20); // 设置温度为20度,将触发所有观察者对象的更新操作
weatherStation.setTemperature(30); // 设置温度为30度,将触发所有观察者对象的更新操作
当 WeatherStation 对象的温度被设置为20℃时,它会通知所有的观察者(观察者是 TemperatureDisplay 和 Fan 对象)。因此,TemperatureDisplay 对象会打印出一条消息,表明它正在更新显示的温度,而 Fan 对象则会打印出一条消息,表明它正在关闭风扇。
同样的,当 WeatherStation 对象的温度被设置为30℃时,TemperatureDisplay 对象会再次打印出一条消息,表明它正在更新显示的温度,而 Fan 对象则会打印出一条消息,表明它正在打开风扇。
在终端使用 node 执行后结果如下:
这就是观察者模式的工作原理。每当被观察对象的状态发生变化时,所有的观察者都会得到通知,并根据新的状态进行响应。
在这个示例中,WeatherStation 是被观察者(发布者),它有一个 temperature 状态,当温度发生变化时,它会通知所有的观察者。TemperatureDisplay 和 Fan 都是观察者,它们对温度的变化有不同的反应:TemperatureDisplay 会更新显示的温度,而 Fan 则会根据温度的高低决定是否开启。
以下是完整的示例代码:
class WeatherStation {
constructor() {
this.observers = []; // 观察者数组,用于存储观察者对象
this.temperature = 0; // 温度初始值为0
}
addObserver(observer) {
this.observers.push(observer); // 将观察者对象添加到观察者数组中
}
removeObserver(observer) {
const index = this.observers.indexOf(observer); // 获取观察者对象在数组中的索引
if (index > -1) {
this.observers.splice(index, 1); // 如果观察者对象存在于数组中,则从数组中移除
}
}
setTemperature(temp) {
console.log(`天气预报工作站: 当前温度为 ${temp}℃`); // 打印新的温度测量值
this.temperature = temp; // 更新温度值
this.notifyObservers(); // 通知所有观察者对象
}
notifyObservers() {
for(let observer of this.observers) {
observer.update(this.temperature); // 调用每个观察者对象的update方法,传递当前温度值作为参数
}
}
}
class TemperatureDisplay {
constructor(weatherStation) {
this.weatherStation = weatherStation; // 引用WeatherStation对象
this.weatherStation.addObserver(this); // 将自身作为观察者对象添加到WeatherStation的观察者数组中
}
update(temperature) {
console.log(`[订阅者-温度显示器]: 我得赶紧把显示的温度更新为 ${temperature}℃`); // 打印需要更新显示的温度
// 这里是更新显示的逻辑
}
}
class Fan {
constructor(weatherStation) {
this.weatherStation = weatherStation; // 引用WeatherStation对象
this.weatherStation.addObserver(this); // 将自身作为观察者对象添加到WeatherStation的观察者数组中
}
update(temperature) {
if (temperature > 25) {
console.log("[订阅者-风扇]: 太 TM 热了,我得赶紧开风扇凉快凉快..."); // 如果温度大于25度,则打印开启风扇的信息
} else {
console.log("[订阅者-风扇]: 太 TM 冷了,赶紧把风扇给关了吧先......"); // 如果温度小于等于25度,则打印关闭风扇的信息
}
}
}
const weatherStation = new WeatherStation(); // 创建WeatherStation对象
const tempDisplay = new TemperatureDisplay(weatherStation); // 创建TemperatureDisplay对象,并传入WeatherStation对象
const fan = new Fan(weatherStation); // 创建Fan对象,并传入WeatherStation对象
weatherStation.setTemperature(20); // 设置温度为20度,将触发所有观察者对象的更新操作
weatherStation.setTemperature(30); // 设置温度为30度,将触发所有观察者对象的更新操作
四、总结
至此,我们已经深入探讨了观察者模式,包括它的缺点以及如何解决这些问题,以及它在前端开发中的应用。我们通过一个复杂的 JavaScript 示例来揭示了观察者模式的强大能力。
五、小试牛刀
还记得文章开头的这个动图吗?
这是在前面示例代码的基础上实现的,建议你有时间的话自己也写一个类似的交互界面尝试一下。稍后,你也可以在文末查看源码。
希望这篇文章能对你所有帮助,文内若有错误和不妥之处还请在下方留言区批评指正哦,大家一起交流、学习 ❤
感谢阅读,我们下期再见 : )
原创首发于微信公众号:mp.weixin.qq.com/s/_FQwjoxfv…。