观察者模式进阶:深入理解与实战应用

300 阅读10分钟

你好,我是小甲。

今天我们要继续深入探索观察者模式。在上期《一文读懂观察者模式:通俗易懂的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 是被观察者,PromotionInventory 是观察者。当我们向购物车添加一个商品时,购物车会通知所有观察者(PromotionInventory)。然后,库存模块的更新又会触发促销模块的更新。这种连锁反应会使这段代码难以理解和维护。

我们用 node 在终端执行这段代码的效果如下:

电子商务购物车示例.gif

这就是观察者和被观察者之间的依赖关系过于紧密的一种典型场景。

咱就说,类似的模块不用太多,有三五个这样的就够你头疼了吧~

1.2 处理观察者的更新可能很复杂

如果观察者的更新逻辑很复杂,或者需要在多个观察者之间协调,那么代码可能会变得非常复杂。

有时候我们不想让代码变得越来越负责,但是写着写着就复杂了……

难道就没有办法解决上面的问题吗?当然有!我们可以考虑下面这两个方案:

  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℃时,它会通知所有的观察者(观察者是 TemperatureDisplayFan 对象)。因此,TemperatureDisplay 对象会打印出一条消息,表明它正在更新显示的温度,而 Fan 对象则会打印出一条消息,表明它正在关闭风扇。

同样的,当 WeatherStation 对象的温度被设置为30℃时,TemperatureDisplay 对象会再次打印出一条消息,表明它正在更新显示的温度,而 Fan 对象则会打印出一条消息,表明它正在打开风扇。

在终端使用 node 执行后结果如下:

天气预报工作站终端示例

这就是观察者模式的工作原理。每当被观察对象的状态发生变化时,所有的观察者都会得到通知,并根据新的状态进行响应。

在这个示例中,WeatherStation 是被观察者(发布者),它有一个 temperature 状态,当温度发生变化时,它会通知所有的观察者。TemperatureDisplayFan 都是观察者,它们对温度的变化有不同的反应: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 示例来揭示了观察者模式的强大能力。

五、小试牛刀

还记得文章开头的这个动图吗?

天气预报工作站

这是在前面示例代码的基础上实现的,建议你有时间的话自己也写一个类似的交互界面尝试一下。稍后,你也可以在文末查看源码。

希望这篇文章能对你所有帮助,文内若有错误和不妥之处还请在下方留言区批评指正哦,大家一起交流、学习 ❤

感谢阅读,我们下期再见 : )

比心.gif

源码:github.com/smpower/mas…

原创首发于微信公众号:mp.weixin.qq.com/s/_FQwjoxfv…