观察者模式终极指南:高级主题与最佳实践

479 阅读11分钟

在之前的两篇文章中,我们已经对观察者模式有了基础和深入的了解。现在,我们将探索观察者模式的高级主题和最佳实践。如果你对观察者模式还不太了解,不妨看一下那两篇文章:

1. 管理观察者列表

在使用观察者模式时,一个常见的问题是如何有效地管理观察者列表。特别是在有大量观察者,或者观察者的生命周期变得复杂时,管理观察者列表就变得尤其重要。比如,你可能需要动态地添加或移除观察者,或者需要在多个被观察者之间共享观察者。

对于这个问题,一种可能的解决方案是使用一个专门的数据结构来存储和管理观察者。例如,我们可以使用一个哈希表,其中的键是观察者的标识符,值是观察者对象。这样,添加、删除和查找观察者都变得非常快速和简单。

为解决这个问题,我们可以使用一个专门的数据结构来存储和管理观察者。例如,我们可以使用一个哈希表,其中的键是观察者的标识符,值是观察者对象。这样,添加、删除和查找观察者都变得非常快速和简单。我们也可以用 JavaScript 中的 MapSet 对象来实现。

下面的示例演示了如何使用 Map 对象来管理观察者列表:

class Subject {
  constructor() {
    // 创建一个 Map 对象来存储观察者列表
    this.observers = new Map();
  }

  // 添加观察者
  addObserver(id, observer) {
    // 将观察者添加到 Map 对象中
    this.observers.set(id, observer);
  }

  // 移除观察者
  removeObserver(id) {
    // 从 Map 对象中删除观察者
    this.observers.delete(id);
  }

  // 通知观察者
  notifyObservers() {
    // 遍历 Map 对象中的观察者列表并通知它们
    this.observers.forEach((observer, id) => observer.update(id));
  }
}

class Observer {
  // 更新观察者
  update(id) {
    console.log(`通知「${id}」`);
  }
}

const subject = new Subject();
const observer1 = new Observer();
const observer2 = new Observer();

// 添加观察者
subject.addObserver("观察者1", observer1);
subject.addObserver("观察者2", observer2);

// 通知观察者
subject.notifyObservers();

// 移除观察者
subject.removeObserver("观察者1");

在上面这段代码中,我们创建了一个 Subject 类(被观察者),它使用一个 Map 对象来存储观察者。添加、移除和通知观察者的操作都是通过这个 Map 对象进行的。

Subject 类有三个方法:

  • addObserver(id, observer):用于添加观察者到观察者列表中,通过 Map 的 set 方法将观察者对象保存,键是 id,值是观察者对象。
  • removeObserver(id):用于从观察者列表中移除观察者,通过 Map 的 delete 方法删除指定 id 的观察者。
  • notifyObservers():用于通知所有的观察者,通过 Map 的 forEach 方法遍历所有的观察者,并调用观察者的 update 方法。

Observer 类代表观察者,它有一个 update 方法,当被观察者(Subject)状态改变时,这个方法会被调用,从而通知观察者。

然后在代码的后面部分,创建了两个 Observer 对象(observer1observer2),并将它们添加到 Subject 对象中。然后通过 SubjectnotifyObservers 方法,通知所有的观察者。然后,通过 SubjectremoveObserver 方法,移除了一个观察者。

我们通过 Node.js 来执行上面的代码,效果如下:

管理观察者列表

这种方式的一个优点是,你可以使用任何类型的值作为观察者的标识符,而不仅仅是数字或字符串。例如,你可以使用一个对象或者一个函数作为标识符。此外,Map 对象保证了观察者的插入顺序,这在某些情况下可能会很有用。

2. 处理错误和异常

当我们在使用观察者模式的过程中,一个重要的挑战就是处理错误和异常。假设在观察者的更新方法中发生了一个错误,这个错误应该如何处理?如果在更新过程中抛出了异常,我们是否应该立即停止通知其他的观察者?

这个问题的答案可能会依赖于具体的应用场景。在某些情况下,你可能希望在发生错误时立即停止通知,以防止错误的传播。在其他情况下,你可能希望无视这个错误,继续通知剩下的观察者。

2.1 try/catch

无论选择哪种策略,你都需要在代码中明确地处理这种情况。一种常见的做法是在被观察者的 notify 方法中使用 try/catch 语句来捕获和处理异常。然后你可以选择是否要在catch语句中处理异常,或者简单地记录这个异常,并继续执行。

    class Observable {
      constructor() {
        this.observers = []; // 观察者列表
      }

      addObserver(observer) {
        // 添加观察者
        this.observers.push(observer); // 将观察者添加到观察者列表中
      }

      notify(data) {
        // 通知观察者
        for (let observer of this.observers) {
          // 遍历观察者列表
          try {
            observer.update(data); // 调用观察者的 update 方法
          } catch (error) {
            console.log(`通知观察者时发生了错误:\n${error}`);
            // 处理错误,例如记录日志、忽略错误或停止通知过程
            // 为了本示例,我们只记录日志并继续通知其他观察者
          }
        }
      }
    }

    class Observer {
      update(data) {
        // 更新观察者
        // 这里是更新逻辑。为了本示例,我们模拟一个错误
        throw new Error("在更新期间发生了错误");
      }
    }

    const observable = new Observable(); // 创建 Observable 实例
    const observer = new Observer(); // 创建 Observer 实例

    observable.addObserver(observer); // 添加观察者
    observable.notify("Some data"); // 通知观察者

这是一种基本的错误处理方式。这种方式的好处是简单明了,不好的地方是只能同步处理错误,如果 update 方法是一个异步操作,那么异常可能就不能被正确捕获了。

在这段示例代码中,Observable (被观察者)类有以下方法:

  1. addObserver(observer):该方法用于向 Observable 对象添加一个 Observer 对象。被添加的 Observer 对象会被放在 observers 数组中。
  2. notify(data):该方法用于通知所有的 Observer 对象有新的数据。这个方法会遍历 observers 数组,对每个 Observer 调用其 update(data) 方法。

Observer (观察者)类只有一个 update(data) 方法,当 Observable 对象调用其 notify(data) 方法时,该方法就会被调用。在这个示例中,update(data) 方法中会抛出一个错误。

在代码的最后,创建了一个 Observable 对象和一个 Observer 对象,并将这个 Observer 对象添加到 Observable 对象中,然后调用 Observable 对象的 notify(data) 方法。这会导致 Observer 对象的 update(data) 方法被调用,从而抛出一个错误。当观察者在更新时抛出一个异常,这个异常会被 notify ****方法中的 catch 语句捕获。然后,我们记录这个异常,并继续通知其余的观察者。

在终端工具中执行上面的代码示例,结果如下:

观察者模式错误处理

而对于异步操作中出现的错误或异常,我们可以通过 Promise 或 async/await 的方式来优雅的处理。

2.2 Promise

上面的示例代码通过 Promise 的方式改写后,实现如下:

    class Observable {
      constructor() {
        this.observers = [];
      }

      addObserver(observer) {
        this.observers.push(observer);
      }

      notify(data) {
        this.observers.forEach(observer => {
          // 返回一个新的 Promise 对象
          new Promise((resolve, reject) => {
            resolve(observer.update(data));
          })
          .catch(error => {
            console.log(`通知观察者时发生了错误:\n${error}`);
          });
        });
      }
    }

    class Observer {
      update(data) {
        throw new Error("在更新期间发生了错误");
      }
    }

    const observable = new Observable();
    const observer = new Observer();

    observable.addObserver(observer);
    observable.notify("Some data");

这段代码中,每一个观察者的 update 方法都被包装在一个 Promise 对象中。如果 update 方法成功完成,那么 Promise 会被解析(resolve);如果 update 方法抛出了异常,那么 Promise 会被拒绝(reject),并进入 .catch 方法中处理异常。这种方式的好处是可以处理异步操作中的错误,不好的地方是代码相对于 try/catch 方式来说复杂一些。

下面我们再看一下 async/await 方式是如何处理观察者模式中的错误和异常的。

2.3 async/await

    class Observable {
      constructor() {
        this.observers = [];
      }

      addObserver(observer) {
        this.observers.push(observer);
      }

      async notify(data) {
        for (let observer of this.observers) {
          try {
            // 使用 await 等待异步操作的结果
            await observer.update(data);
          } catch (error) {
            console.log(`通知观察者时发生了错误:\n${error}`);
          }
        }
      }
    }

    class Observer {
      update(data) {
        throw new Error("在更新期间发生了错误");
      }
    }

    const observable = new Observable();
    const observer = new Observer();

    observable.addObserver(observer);
    observable.notify("Some data");

async/await 是 Promise 的语法糖,它可以让异步代码看起来像同步代码。在这个代码中,notify 方法被标记为 async,这表示这个方法是一个异步方法。在 notify 方法中,调用观察者的 update 方法前加了 await 关键字,这表示会等待这个异步操作完成。如果 update 方法抛出了异常,那么和 try/catch 方式一样,catch 语句块会捕获到这个异常,并打印出异常信息。这种方式的好处是既可以处理异步操作中的错误,代码又相对于 Promise 方式来说简洁一些。

try/catch、Promise 和 async/await 这三种方式,在处理观察者模式中的异常或错误时各有优劣,你可以根据实际需求和个人喜好来选择使用哪种方式。

3. 大型项目中使用观察者模式

在大型项目中使用观察者模式时,如果有大量的被观察者和观察者,那么理解和管理这些对象之间的交互就变得非常复杂。被观察者和观察者间的通信可能会在很多不同的地方发生,这使得理解和跟踪程序流变得困难。更糟糕的是,由于观察者通常通过被观察者间接地进行通信,所以可能很难发现程序中的错误。

对于这个问题,一种可能的解决方案是使用一些可视化的工具来帮助理解和管理被观察者和观察者之间的关系。另一种可能的解决方案是使用一些设计模式,例如中介者模式或者命令模式,来降低被观察者和观察者之间的耦合度。

为了解决这个问题,这里我们介绍一个解决方案——使用事件总线(Event Bus)

什么是事件总线呢?

事件总线是一个在应用中传递事件的对象,所有的被观察者会向它发送事件,所有的观察者都会从它那里接收事件。这使得被观察者和观察者之间的通信变得更加明确和集中。

文字描述比较抽象,我们通过几张图来看一下事件总线是如何来管理大量观察者和被观察者的。

首先,观察者模式是一对多(一个被观察者和多个观察者)的场景。这种情形下,不需要借助事件总线,否则反而增加了代码的维护难度。

简单的观察者模式

我们可以让被观察者直接将消息发送给那些观察者即可。如果在这种比较简单的场景下应用事件总线,因为这样会增加代码的复杂度。图示如下:

简单的事件总线和观察者模式

相比于简单的观察者模式来说,没太有必要增加一个事件总线。而如果在大型项目中,有非常多的观察者和被观察者,这时候就有必要通过事件总线来管理了。

复杂的观察者模式和事件总线

被观察者会将所有事件对象发送给事件总线,那些观察者则统一在事件总线处接收通知。这样做的好处是,被观察者和观察者之间不存在紧密的耦合关系,它们之间的通信全部通过事件总线实现,这使得被观察者和观察者之间的通信变得更加明确和集中,从而易于维护和管理。

下面我们就通过一个简易的示例来演示事件总线在观察者模式中的应用。

首先,我们创建一个事件总线。用于管理观察者和被观察者之间的信息传递。它有一个观察者列表(observers),当有新的观察者出现时,可以通过 registerObserver 方法将其添加到列表中,也可以通过 removeObserver 方法将其从列表中移除。它还有一个 notifyObservers 方法,用于通知所有注册的观察者有新的事件发生。

    // 创建事件总线
    class EventBus {
      constructor() {
        this.observers = [];
      }

      registerObserver(observer) {
        this.observers.push(observer);
      }

      removeObserver(observer) {
        this.observers = this.observers.filter(obs => obs !== observer);
      }

      notifyObservers(event) {
        this.observers.forEach(observer => observer.update(event));
      }
    }

接着,我们创建两个观察者。它们需要关注事件总线上的某些事件,当这些事件发生时执行某些操作。每个观察者都有一个 update 方法,当事件总线通知它们有新的事件发生时,它们会执行这个方法。在这个例子中,Observer1Observer2update 方法分别会打印出一条消息,显示它们收到了一个事件。

    // 创建观察者1
    class Observer1 {
      update(event) {
        console.log(`观察者1收到了事件:${event}`);
      }
    }

    // 创建观察者2
    class Observer2 {
      update(event) {
        console.log(`观察者2收到了不同的事件:${event}`);
      }
    }

最后,我们创建了事件总线、观察者和被观察者的实例,并让被观察者发送了一些事件。每当被观察者通过事件总线发送事件时,所有注册到事件总线的观察者都会收到通知,并执行各自的 update 方法,这样就实现了观察者和被观察者之间的解耦。

    // 实例化事件总线
    const eventBus = new EventBus();

    // 实例化两个观察者并注册到事件总线
    const observer1 = new Observer1();
    const observer2 = new Observer2();
    eventBus.registerObserver(observer1);
    eventBus.registerObserver(observer2);

    // 实例化两个被观察者
    const subject1 = new Subject1(eventBus);
    const subject2 = new Subject2(eventBus);

    // 被观察者发送事件
    subject1.sendEvent('Hello');
    subject2.sendEvent('World');

上面这四部分代码便是完整的实现。我们在终端工具中尝试执行这段代码示例,结果如下:

复杂的观察者模式和事件总线.gif

在这个示例中,事件总线 EventBus 其实充当着中介者的角色,也就是中介者模式

在大型项目中优雅的管理观察者和被观察者,除了上面我们提到的事件总线还有很多方案,比如:中介者模式、命令模式、工厂模式或装饰者模式等。因篇幅有限,这些设计模式在这里就不一一讨论了,我们将在未来深入探讨这些设计模式。

4. 最佳实践

在使用观察者模式时,有一些最佳实践可以帮助我们避免一些常见的问题,使我们的代码更加健壮和易于维护。可以考虑以下三个方面:

  1. 尽量保持被观察者和观察者之间的解耦: 这是观察者模式的核心理念。被观察者不应该知道观察者的任何信息,观察者也不应该知道被观察者的任何信息。
  2. 考虑观察者的执行顺序: 在某些情况下,观察者的执行顺序可能很重要。我们应该提供一种方式来控制或者影响这个顺序。比如前面通过 Map 对象来管理观察者列表的例子。
  3. 考虑使用异步通知: 在某些情况下,我们可能希望异步地通知观察者,以避免阻塞被观察者的执行。

5. 总结

在这篇文章中,我们讨论了观察者模式的高级主题和最佳实践,希望这些知识能够帮助你更好地理解和使用观察者模式。包括如何有效地管理观察者列表,如何处理错误和异常,以及如何在大型项目中结合事件总线使用观察者模式。

如果你喜欢这篇文章,记得点赞分享给更多朋友哦~

比心

好了,今天就到这里,我们下期再见 :-)