Lapa Knowledge Base
Поведенческие паттерны

Observer

Паттерн Observer

Observer позволяет конкретным объектам наблюдать за изменениями, происходящими с другими объектами, и реагировать на них.

Требуемое число объектов-наблюдателей закреплено за определенным субъектом, содержащим нужные данные. Когда данные внутри этого субъекта изменяются, наблюдатели должны быть оповещены об этих изменениях и передать информацию о них. Чаще всего этот процесс происходит динамически прямо во время выполнения программы, поэтому Наблюдатель создает механизм подписки.

Является паттерном поведенческого типа.


Для чего используется?

В крупных, масштабных проектах зависимости между объектами могут быть очень сложными. Изменение даже единственного параметра влияет на работу множества других элементов программы. Сообщить всем зависимых объектах о внесенных изменениях позволяет паттерн Наблюдатель.

Пример — пользователь новостного спортивного сайта заинтересован в чтении статей только из категории «Футбол». Ему неудобно каждый раз проверять, опубликованы ли новые дайджесты в данной категории, но и присылать ему по электронной почте все обновления на сайте тоже неверный подход — почтовый ящик быстро забьётся неинтересными пользователю новостями. Решение в данном случае — подписаться конкретно на уведомления о новостях футбола. Информация о пользователе и о типе контента, на который пользователь подписан, будет сохранена и применена, пока пользователь не откажется от подписки.

Когда удобно применять:

  • Заранее неизвестно число объектов-подписчиков, которые будут получать сообщения;
  • Не требуется устанавливать сильную связь и зависимость между объектами — объекты взаимодействуют, практически не обладая информацией друг о друге.

Варианты кейсов использования:

  • Реализация механизма оформления подписки для пользователей;
  • Автоматическое обновление пользовательского интерфейса в результате выполнения определенных действий;
  • Реализация настраиваемых пользователем уведомлений в мессенджере.

Какова механика реализации?

Функциональность состоит из независимого ядра — Субъекта, хранящего нужную информацию, и опциональных зависимых частей — подписчиков (объектов-наблюдателей). Для всех подписчиков определяется метод оповещения и дальнейшие действия по реагированию на изменения в Субьекте.

При этом Субъекту не интересно то, как распорядятся полученной информацией объекты-подписчики.


Структура и отношения

Subject
├── observers: Observer[]
├── attach(observer: Observer): void
├── detach(observer: Observer): void
└── notify(): void

ConcreteSubject
├── state: any
├── attach(observer: Observer): void
├── detach(observer: Observer): void
├── notify(): void
└── setState(state: any): void

Observer
└── update(subject: Subject): void

ConcreteObserver
├── subject: Subject
└── update(subject: Subject): void

Пример реализации

// Интерфейс наблюдателя
  class Observer {
      update(subject) {
          throw new Error('Метод update должен быть переопределен');
      }
  }

  // Интерфейс субъекта
  class Subject {
      attach(observer) {
          throw new Error('Метод attach должен быть переопределен');
      }
  
      detach(observer) {
          throw new Error('Метод detach должен быть переопределен');
      }
  
      notify() {
          throw new Error('Метод notify должен быть переопределен');
      }
  }

  // Конкретный субъект - магазин
  class Store extends Subject {
      constructor(name) {
          super();
          this.name = name;
          this.observers = [];
          this.newProducts = [];
      }
  
      attach(observer) {
          console.log(`${observer.name} подписался на уведомления от ${this.name}`);
          this.observers.push(observer);
      }
  
      detach(observer) {
          const index = this.observers.indexOf(observer);
          if (index > -1) {
              console.log(`${observer.name} отписался от уведомлений от ${this.name}`);
              this.observers.splice(index, 1);
          }
      }
  
      notify() {
          console.log(`${this.name} уведомляет ${this.observers.length} подписчиков о новых товарах`);
          for (const observer of this.observers) {
              observer.update(this);
          }
      }
  
      addNewProduct(product) {
          this.newProducts.push(product);
          console.log(`${this.name} получил новый товар: ${product.name}`);
          this.notify();
      }
  
      getNewProducts() {
          return this.newProducts;
      }
  }

  // Конкретные наблюдатели - покупатели
  class Customer extends Observer {
      constructor(name, interests = []) {
          super();
          this.name = name;
          this.interests = interests;
          this.notifications = [];
      }
  
      update(subject) {
          const newProducts = subject.getNewProducts();
          const relevantProducts = newProducts.filter(product => 
              this.interests.some(interest => 
                  product.name.toLowerCase().includes(interest.toLowerCase())
              )
          );
      
          if (relevantProducts.length > 0) {
              const message = `Уважаемый ${this.name}! В магазине ${subject.name} поступили интересующие вас товары: ${relevantProducts.map(p => p.name).join(', ')}`;
              this.notifications.push(message);
              console.log(message);
          }
      }
  
      getNotifications() {
          return this.notifications;
      }
  }

  // Использование
  const electronicsStore = new Store('Электроника Плюс');
  const fashionStore = new Store('Мода и Стиль');

  // Создаем покупателей с разными интересами
  const john = new Customer('Джон', ['iphone', 'macbook', 'airpods']);
  const sarah = new Customer('Сара', ['dress', 'shoes', 'handbag']);
  const mike = new Customer('Майк', ['iphone', 'samsung', 'headphones']);

  // Подписываем покупателей на магазины
  electronicsStore.attach(john);
  electronicsStore.attach(mike);
  fashionStore.attach(sarah);

  console.log('\n=== Поступление товаров ===');

  // Добавляем новые товары
  electronicsStore.addNewProduct({ name: 'iPhone 15', price: 80000 });
  electronicsStore.addNewProduct({ name: 'MacBook Pro', price: 150000 });
  fashionStore.addNewProduct({ name: 'Summer Dress', price: 5000 });
  electronicsStore.addNewProduct({ name: 'Samsung Galaxy', price: 70000 });

  console.log('\n=== Уведомления покупателей ===');
  console.log('Уведомления Джона:', john.getNotifications());
  console.log('Уведомления Сары:', sarah.getNotifications());
  console.log('Уведомления Майка:', mike.getNotifications());

Преимущества

  • Код становится удобен в обслуживании засчет меньшей степени зацепления между объектами — не требуется разбираться в отношениях и зависимостях между ними;
  • Можно добавлять новых Субъектов и наблюдаталей, не внося изменений в код уже существующих;
  • Обязанности между наблюдаемым объектом и наблюдателями разделены, что делает код более гибким и модульным.

Недостатки

  • Программа может стать ресурсозатратной при большом количестве наблюдателей;
  • Есть вероятность утечки защищенных данных с угрозой для их безопасности.

Отличия от других паттернов

  • Mediator — создает зависимости между одним главным Субъектом и компонентами, которые ему подчиняются. Наблюдатель же должен установить динамические связи между объектами, и некоторые объекты могут быть в подчинении у других.
  • Abstract Factory — похож на Прототип, но создает интерфейсы для продуктов, составляющих семейства.

Реальный пример

// Observer для системы событий в приложении
class EventEmitter {
    constructor() {
        this.events = {};
    }
    
    on(eventName, callback) {
        if (!this.events[eventName]) {
            this.events[eventName] = [];
        }
        this.events[eventName].push(callback);
    }
    
    off(eventName, callback) {
        if (this.events[eventName]) {
            this.events[eventName] = this.events[eventName].filter(cb => cb !== callback);
        }
    }
    
    emit(eventName, data) {
        if (this.events[eventName]) {
            this.events[eventName].forEach(callback => callback(data));
        }
    }
}

// Использование в приложении
class UserManager extends EventEmitter {
    constructor() {
        super();
        this.users = [];
    }
    
    addUser(user) {
        this.users.push(user);
        this.emit('userAdded', user);
    }
    
    removeUser(userId) {
        const user = this.users.find(u => u.id === userId);
        if (user) {
            this.users = this.users.filter(u => u.id !== userId);
            this.emit('userRemoved', user);
        }
    }
}

// Обработчики событий
const userManager = new UserManager();

// Логирование
userManager.on('userAdded', (user) => {
    console.log(`Пользователь добавлен: ${user.name}`);
});

userManager.on('userRemoved', (user) => {
    console.log(`Пользователь удален: ${user.name}`);
});

// Отправка уведомлений
userManager.on('userAdded', (user) => {
    console.log(`Отправка приветственного email пользователю ${user.email}`);
});

// Обновление статистики
userManager.on('userAdded', (user) => {
    console.log(`Обновление статистики: всего пользователей ${userManager.users.length}`);
});

// Тестируем
userManager.addUser({ id: 1, name: 'John', email: 'john@example.com' });
userManager.addUser({ id: 2, name: 'Jane', email: 'jane@example.com' });
userManager.removeUser(1);

Observer — это основа реактивного программирования!

Copyright © 2026