华为面试官:手写一个观察者模式

1,736 阅读4分钟

面试官:观察者模式在面向对象的语言中很常见...

我:阿巴阿巴阿巴(内心:没记啊,完了完了,我这leetcode不是白刷了)

graph TD
    Subject["主题 (Subject)"] 
    Observer1["观察者1 (Observer)"]
    Observer2["观察者2 (Observer)"]
    Observer3["观察者3 (Observer)"]
    
    Subject -->|注册| Observer1
    Subject -->|注册| Observer2
    Subject -->|注册| Observer3
    Subject -->|通知| Observer1
    Subject -->|通知| Observer2
    Subject -->|通知| Observer3

image-20241010212031108.png

可以看出:主题和观察者是关键两个角色,我个人喜欢第一张图,也更好理解一下。

我在记忆主题和观察者的时候就没有那么清晰,那么可以设想为两个具象的角色,比如主题为小宝宝,观察者为爸爸妈妈,一下子就有感觉了。

易于理解版

下面我结合代码来讲解:


    // 定义主题(Subject)
    class Subject {
      constructor() {
        this.observers = []; // 存储所有的观察者
      }
    ​
      // 注册观察者
      addObserver(observer) {
        this.observers.push(observer);
      }
    ​
      // 通知所有观察者
      notify(message) {
        this.observers.forEach(observer => observer.updated(message));
      }
    }
    ​
    // 定义观察者(Observer)
    class Observer {
      constructor(name) {
        this.name = name; // 观察者的名字
      }
    ​
      // 接收到通知后执行的操作
      updated(message) {
        if (message === '哭了') {
          console.log(this.name + ':给宝宝喂奶');
        } else {
          console.log(this.name + ':看看宝宝是不是要换尿布了');
        }
      }
    }
    ​
    // 使用示例
    const baby = new Subject(); // 创建主题const father = new Observer('爸爸'); // 创建观察者:爸爸
    const mother = new Observer('妈妈'); // 创建观察者:妈妈
    ​
    baby.addObserver(father); // 注册爸爸为观察者
    baby.addObserver(mother); // 注册妈妈为观察者// 主题状态发生变化,通知所有观察者
    baby.notify('哭了');

代码输出为:

爸爸:给宝宝喂奶
妈妈:给宝宝喂奶

注册观察者,我们可以假设成小宝宝只认爸爸妈妈,其他人喂奶就不喝!

拒绝喝奶.webp

代码的关键点:通知所有观察者用的是this.observer.forEach(observer => observer.update(message))

当然也有以公众号和用户来描述这种关系的,公众号为主题,用户为观察者,观察者需要有注册这么一个动作,然后就能接受到公众号发布的消息了。

以上主要是方便记忆的方式:一想到观察者模式,小宝宝爸爸妈妈

华为面试官:大概20行就能写出来

我:这...a...(开始瞎写)

标准版

下面我们手写正规的观察者模式:

    class Subject {
        constructor() {
            this.observers = [];
        }
        addObserver(observer) {
            this.observers.push(observer);
        }
        removeObserver(observer) {
            this.observers = this.observers.filter((obs) => obs !== observer);
        }
        notify(data) {
            this.observers.forEach((observer) => observer.updated(data));
        }
    }
    class Observer {
        constructor(name) {
            this.name = name;
        }
        updated(data) {
            console.log(this.name + ' 收到通知:' + data);
        }
    }
    const subject = new Subject();
    const observer1 = new Observer('观察者1');
    const observer2 = new Observer('观察者2');
    subject.addObserver(observer1);
    subject.addObserver(observer2);
    subject.notify('更新');
    subject.removeObserver(observer1);;
    subject.notify('再次更新');

其中filter:浅拷贝通过过滤的元素

新增了移除观察者

输出:
观察者1 收到通知:更新
观察者2 收到通知:更新
观察者2 收到通知:再次更新

当时写成这样估计能过了💔

扩展版

实际应用中,观察者模式可能会更复杂,可以加入一次性观察者、异步通知、优先级等

一次性观察者

一次性观察者可以通过在添加观察者时体现

        addObserver(observer, once = false) {
            this.observers.push({ observer, once });
        }
        notify(data) {
            this.observers.slice().forEach((obs, index) => {
                obs.observer.update(data);
                // 如果是一次性观察者,则移除
                if (obs.once) {
                    this.observers.splice(index, 1);
                }
            });
        }

小tips:

forEach的语法为forEach(callbackFn, thisArg),因为使用了箭头函数,所以 thisArg 参数无关紧要,箭头函数没有自己的 this 绑定,callbackFn 接收3个参数,分别是

  • element:数组当前正在处理的元素
  • index:正在处理的元素在数组中的索引
  • array:调用该方法的数组

前方预警!!!🕳️

在 forEach 前需要加入 .slice() 来进行浅拷贝,否则在迭代期间修改数组,会出现跳元素的情况

以下面例子为例:three 被跳过了

image-20241018113647242.png

splice(拼接)的语法为 splice(start, deleteCount, item1, item2, /* …, */ itemN),而且是就地修改

可以通过如下方法设置一次性观察者

    subject.addObserver(observer1);
    subject.addObserver(observer2, true); // 设置为一次性观察者

异步处理

因为js是单线程语言,异步通知可以避免阻塞主线程,提高性能和响应速度等

        notify(data) {
            this.observers.forEach(observer => {
                setTimeout(() => {
                    observer.update(data);
                }, 0);
            });
        }

可以这样处理

优先级

优先级机制可以确保高优先级的观察者先被推送到,在任务调度等场景可以用到

        /**
         * 添加观察者
         * @param {Observer} observer - 观察者实例
         * @param {number} priority - 观察者的优先级(数值越高优先级越高)
         */
        addObserver(observer, priority = 0) {
            this.observers.push({ observer, priority });
            // 按优先级从高到低排序
            this.observers.sort((a, b) => b.priority - a.priority);
        }
    // 创建观察者
    const observer1 = new Observer('观察者1');
    const observer2 = new Observer('观察者2');
    const observer3 = new Observer('观察者3');
    ​
    // 添加观察者到被观察者,设置不同的优先级
    subject.addObserver(observer1, 1); // 优先级1
    subject.addObserver(observer2, 3); // 优先级3
    subject.addObserver(observer3, 2); // 优先级2

输出顺序会变成观察者2、3、1

END

面试官最后又以伪代码给我讲了一遍观察者模式

谨以此文纪念我逝去的华子😭