Skip to content

观察者模式

当对象间存在一对多关系时,则使用观察者模式(Observer Pattern)。比如,当一个对象被修改时,则会自动通知依赖它的对象。观察者模式属于行为型模式。

问题

假如你有两种类型的对象:顾客商店。 顾客对某个特定品牌的产品非常感兴趣 (例如最新型号的 iPhone 手机), 而该产品很快将会在商店里出售。

顾客可以每天来商店看看产品是否到货。 但如果商品尚未到货时, 绝大多数来到商店的顾客都会空手而归。

另一方面, 每次新产品到货时, 商店可以向所有顾客发送邮件 (可能会被视为垃圾邮件)。 这样, 部分顾客就无需反复前往商店了, 但也可能会惹恼对新产品没有兴趣的其他顾客。

我们似乎遇到了一个矛盾: 要么让顾客浪费时间检查产品是否到货, 要么让商店浪费资源去通知没有需求的顾客。

解决方案

拥有一些值得关注的状态的对象通常被称为目标, 由于它要将自身的状态改变通知给其他对象, 我们也将其称为发布者 (publisher)。 所有希望关注发布者状态变化的其他对象被称为订阅者 (subscribers)。

观察者模式建议你为发布者类添加订阅机制, 让每个对象都能订阅或取消订阅发布者事件流。 不要害怕! 这并不像听上去那么复杂。 实际上, 该机制包括 1) 一个用于存储订阅者对象引用的列表成员变量; 2) 几个用于添加或删除该列表中订阅者的公有方法。

现在, 无论何时发生了重要的发布者事件, 它都要遍历订阅者并调用其对象的特定通知方法。

实际应用中可能会有十几个不同的订阅者类跟踪着同一个发布者类的事件, 你不会希望发布者与所有这些类相耦合的。 此外如果他人会使用发布者类, 那么你甚至可能会对其中的一些类一无所知。

因此, 所有订阅者都必须实现同样的接口, 发布者仅通过该接口与订阅者交互。 接口中必须声明通知方法及其参数, 这样发布者在发出通知时还能传递一些上下文数据。

如果你的应用中有多个不同类型的发布者, 且希望订阅者可兼容所有发布者, 那么你甚至可以进一步让所有订阅者遵循同样的接口。 该接口仅需描述几个订阅方法即可。 这样订阅者就能在不与具体发布者类耦合的情况下通过接口观察发布者的状态。

观察者模式结构

  1. 发布者 (Publisher) 会向其他对象发送值得关注的事件。 事件会在发布者自身状态改变或执行特定行为后发生。 发布者中包含一个允许新订阅者加入和当前订阅者离开列表的订阅构架。

  2. 当新事件发生时, 发送者会遍历订阅列表并调用每个订阅者对象的通知方法。 该方法是在订阅者接口中声明的。

  3. 订阅者 (Subscriber) 接口声明了通知接口。 在绝大多数情况下, 该接口仅包含一个 update更新方法。 该方法可以拥有多个参数, 使发布者能在更新时传递事件的详细信息。

  4. 具体订阅者 (Concrete Subscribers) 可以执行一些操作来回应发布者的通知。 所有具体订阅者类都实现了同样的接口, 因此发布者不需要与具体类相耦合。

  5. 订阅者通常需要一些上下文信息来正确地处理更新。 因此, 发布者通常会将一些上下文数据作为通知方法的参数进行传递。 发布者也可将自身作为参数进行传递, 使订阅者直接获取所需的数据。

  6. 客户端 (Client) 会分别创建发布者和订阅者对象, 然后为订阅者注册发布者更新。

代码示例

ts
// 某文字编辑软件须提供如下功能:
// 在文本编辑窗口中包含一个可编辑文本区和3个文本信息统计区,
// 用户可以在可编辑文本区对文本进行编辑操作,

/**
 * Subject接口声明了一组用于管理订阅者的方法。
 */
interface Subject {
  // 在被观察者上添加一个观察者
  attach(observer: Observer): void;

  // 将观察者与被观察者分离。
  detach(observer: Observer): void;

  // 事件通知
  notify(): void;
}

/**
 * Subject拥有一些重要的状态,并在状态发生变化时通知观察者。
 */
class ConcreteSubject implements Subject {
  public textarea: string = "";

  private observers: Observer[] = [];

  /**
   * 订阅管理方法
   */
  public attach(observer: Observer): void {
    const isExist = this.observers.includes(observer);
    if (isExist) {
      return console.log("观察者已经存在");
    }

    console.log("添加一个观察者");
    this.observers.push(observer);
  }

  public detach(observer: Observer): void {
    const observerIndex = this.observers.indexOf(observer);
    if (observerIndex === -1) {
      return console.log("不存在该观察者");
    }

    this.observers.splice(observerIndex, 1);
    console.log("分离了一个观察者");
  }

  /**
   * 在每个订户中触发更新。
   */
  public notify(): void {
    console.log("通知观察者");
    for (const observer of this.observers) {
      observer.update(this);
    }
  }

  /**
   * 编辑文本并且触发更新
   */
  public edit(str: string): void {
    console.log("编辑文本");
    this.textarea = str;
    console.log(`Textarea has changed to: ${this.textarea}`);
    this.notify();
  }
}

/**
 * Observer接口声明了主题使用的更新方法。
 */
interface Observer {
  update(subject: Subject): void;
}

/**
 * 第一个文本信息统计区用于显示可编辑文本区中出现的单词总数量和字符总数量,
 */
class ConcreteObserverA implements Observer {
  public update(subject: Subject): void {
    if (subject instanceof ConcreteSubject && subject.textarea) {
      let strs = new Array();
      let str = subject.textarea;
      str.replace(/,/g, " ");
      strs = str.split(" ");
      console.log(
        `1. 单词总数:${strs.length}` + `  字符总数:${subject.textarea.length}`
      );
    }
  }
}

/**
 * 第二个文本信息统计区用于显示可编辑文本区中出现的单词(去重后按照字典序排序)
 */
class ConcreteObserverB implements Observer {
  public update(subject: Subject): void {
    if (subject instanceof ConcreteSubject && subject.textarea) {
      let strs = new Array();
      let str = subject.textarea;
      str = str.replace(/,/g, "");
      strs = str.split(" ");
      strs = strs.filter((item, index, self) => {
        return self.indexOf(item) === index;
      });
      strs.sort();
      console.log(`2. 单词(去重后按照字典序排序):${strs}`);
    }
  }
}

/**
 * 第三个文本信息统计区用于按照出现频次降序显示可编辑文本区中出现的单词以及每个单词出现的次数(例如:hello: 5)。
 */
interface WordCount {
  [key: string]: number;
}

class ConcreteObserverC implements Observer {
  public update(subject: Subject): void {
    if (subject instanceof ConcreteSubject && subject.textarea) {
      let strs = new Array();
      let str = subject.textarea;
      str = str.replace(/,/g, "");
      strs = str.split(" ");
      let obj: WordCount = {};
      for (const item of strs) {
        if (obj[item]) {
          obj[item]++;
        } else {
          obj[item] = 1;
        }
      }
      console.log("3. 单词(出现频次):");
      console.log(obj);
    }
  }
}

/**
 * The client code.
 */

const client = (function () {
  const subject = new ConcreteSubject();

  const observer1 = new ConcreteObserverA();
  subject.attach(observer1);

  const observer2 = new ConcreteObserverB();
  subject.attach(observer2);

  const observer3 = new ConcreteObserverC();
  subject.attach(observer3);

  subject.edit("Whatever is worth doing is worth doing well");
  subject.edit("Do what you say, say what you do");
})();

输出

ts
添加一个观察者
添加一个观察者
添加一个观察者

编辑文本
Textarea has changed to: Whatever is worth doing is worth doing well
通知观察者
1. 单词总数:8  字符总数:43
2. 单词(去重后按照字典序排序):Whatever,doing,is,well,worth
3. 单词(出现频次):
{ Whatever: 1, is: 2, worth: 2, doing: 2, well: 1 }

编辑文本
Textarea has changed to: Do what you say, say what you do
通知观察者
1. 单词总数:8  字符总数:32
2. 单词(去重后按照字典序排序):Do,do,say,what,you
3. 单词(出现频次):
{ Do: 1, what: 2, you: 2, say: 2, do: 1 }