наш блог

Разбор шаблона проектирования Observer на JavaScript

В очередной публикации разберем паттерн Observer. Еще один довольно простой, но вполне полезный шаблон проектирования на JavaScript. Он позволяет создавать объект Observable (Наблюдаемый), содержащий какие-либо данные. И объекты Observer (Наблюдатель), которые «следят» за изменениями в нем.

Изначально напишем класс Observable:

class Observable {
   constructor() {
       this.observers = [];
   }
   subscribe(observer) {
       this.observers.push(observer);
   }
   unsubscribe(observer) {
       this.observers = this.observers.filter(obs => obs !== observer);
   }
   broadcast() {
       this.observers.forEach(observer => observer.notify());
   }
}

У класса Observable есть обязательные методы subscribe, unsubscribe и broadcast. А также массив наблюдателей (подписчиков). Методы subscribe и unsubscribe подписывают и отписывают соответственно. Иначе говоря, добавляют или удаляют подписчиков в массив. А метод broadcast оповещает всех подписчиков о каких-либо событиях.

class Observer {
   constructor(fn) {
       this.fn = fn;
   }
   notify() {
       this.fn();
   }
}

Класс Observer крайне прост. При создании экземпляра он принимает в себя анонимную функцию, которую будет воспроизводить, если вызывается метод notify(). И сейчас станет понятно, зачем это нужно.

// Создадим наблюдателей
const Vasya = new Observer(() => { console.log('Вася') });
const Petya = new Observer(() => { console.log('Петя') });
const Kolya = new Observer(() => { console.log('Коля') });

 // Создаем объект, за которым будем наблюдать
const observable = new Observable();

// Подписываем наших наблюдателей
observable.subscribe(Vasya);
observable.subscribe(Petya);
observable.subscribe(Kolya);

// Оповещаем подписчиков
observable.broadcast(); // Выведет в консоль Вася Петя Коля
// Отписываем Петю
observable.unsubscribe(Petya);
console.log('Петя отписан');
// Повторно оповещаем подписчиков, но уже без Пети
observable.broadcast(); // Вася Коля

Если все еще не понятно, как это можно применить на практике, то рассмотрим еще один пример. Расширим класс Observable.

class Observable {
   constructor(state = {}) {
       this.observers = []; // Список наблюдателей
       this._state = state; // Состояние наблюдаемого объекта
   }
  // Изменить состояние
   set state(state) {
       this._state = {...this._state, ...state};
       this.broadcast();
   }
  // Получить состояние
   get state() {
       return this._state;
   }
   // Подписать наблюдателя
   subscribe(observer) {
       this.observers.push(observer);
   }
   // Отписать наблюдателя
   unsubscribe(observer) {
       this.observers = this.observers.filter(obs => obs !== observer);
   }
   // Уведомить всех наблюдателей
   broadcast() {
       this.observers.forEach(observer => observer.notify(this.state));
   }
}

В класс добавилось состояние. По сути, это простой объект, изменение которого будет вызывать метод broadcast(). А в самом методе теперь будет отправляться состояние каждому подписчику. Следовательно, необходимо изменить класс Observer так, чтобы его метод notify() ожидал параметры.

class Observer {
   constructor(fn) {
       this.fn = fn;
   }
   notify(state) {
       this.fn(state);
   }

И теперь анонимная функция, которая передается в конструктор Observer будет содержать в параметрах состояние observable-объекта.

Итак, подключим фантазию. Представим, что Вася, Коля и Петя пришли в спортивный магазин, где оставили номера телефонов, на которые в будущем должны приходить уведомления о новых поступлениях.

const Vasya = new Observer((state) => { console.log('Вася', state) });
const Petya = new Observer((state) => { console.log('Петя', state) });
const Kolya = new Observer((state) => { console.log('Коля', state) });

const shop = new Observable();

shop.subscribe(Vasya);
shop.subscribe(Petya);
shop.subscribe(Kolya);

shop.state = {nike: 10}; // Изменение состояние вызовет метод broadcast, чем вызовет анонимные функции всех подписчиков и передаст им текущее состояние.

shop.unsubscribe(Petya);
console.log('Петя отписан');

console.log('Изменяем состояние');
shop.state = {adidas: 8, nike: 10}; // Уведомит всех, кроме Пети

Юрий Кизилов, компания Craft Group