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());
# Интерфейс Наблюдателя
class Observer:
def update(self, subject):
raise NotImplementedError("Метод update должен быть переопределен")
# Интерфейс Субъекта
class Subject:
def __init__(self):
self.observers = []
def attach(self, observer):
raise NotImplementedError("Метод attach должен быть переопределен")
def detach(self, observer):
raise NotImplementedError("Метод detach должен быть переопределен")
def notify(self):
raise NotImplementedError("Метод notify должен быть переопределен")
# Конкретный субъект - Магазин
class Store(Subject):
def __init__(self, name):
super().__init__()
self.name = name
self.new_products = []
def attach(self, observer):
print(f"{observer.name} подписался на уведомления от {self.name}")
self.observers.append(observer)
def detach(self, observer):
try:
self.observers.remove(observer)
print(f"{observer.name} отписался от уведомлений от {self.name}")
except ValueError:
pass # Наблюдатель не был подписан на этот субъект
def notify(self):
print(f"{self.name} уведомляет {len(self.observers)} подписчиков о новых товарах")
for observer in self.observers:
observer.update(self)
def add_new_product(self, product):
self.new_products.append(product)
print(f"{self.name} получил новый товар: {product['name']}")
self.notify()
def get_new_products(self):
return self.new_products
# Конкретный наблюдатель - Покупатель
class Customer(Observer):
def __init__(self, name, interests=None):
super().__init__()
self.name = name
self.interests = interests if interests is not None else []
self.notifications = []
def update(self, subject):
new_products = subject.get_new_products()
relevant_products = [
product
for product in new_products
if any(interest.lower() in product['name'].lower() for interest in self.interests)
]
if relevant_products:
product_names = ", ".join([p['name'] for p in relevant_products])
message = f"Уважаемый {self.name}! В магазине {subject.name} поступили интересующие вас товары: {product_names}"
self.notifications.append(message)
print(message)
def get_notifications(self):
return self.notifications
# Использование
electronics_store = Store('Электроника Плюс')
fashion_store = Store('Мода и Стиль')
# Создаем покупателей с разными интересами
john = Customer('Джон', ['iphone', 'macbook', 'airpods'])
sarah = Customer('Сара', ['dress', 'shoes', 'handbag'])
mike = Customer('Майк', ['iphone', 'samsung', 'headphones'])
# Подписываем покупателей на магазины
electronics_store.attach(john)
electronics_store.attach(mike)
fashion_store.attach(sarah)
print('\n=== Поступление товаров ===')
# Добавляем новые товары
electronics_store.add_new_product({'name': 'iPhone 15', 'price': 80000})
electronics_store.add_new_product({'name': 'MacBook Pro', 'price': 150000})
fashion_store.add_new_product({'name': 'Summer Dress', 'price': 5000})
electronics_store.add_new_product({'name': 'Samsung Galaxy', 'price': 70000})
print('\n=== Уведомления покупателей ===')
print('Уведомления Джона:', john.get_notifications())
print('Уведомления Сары:', sarah.get_notifications())
print('Уведомления Майка:', mike.get_notifications())
// Интерфейс наблюдателя
interface Observer {
update(subject: Subject): void;
}
// Интерфейс субъекта
interface Subject {
attach(observer: Observer): void;
detach(observer: Observer): void;
notify(): void;
}
// Конкретный субъект - погодная станция
class WeatherStation implements Subject {
private observers: Observer[] = [];
private temperature: number = 0;
private humidity: number = 0;
private pressure: number = 0;
attach(observer: Observer): void {
this.observers.push(observer);
}
detach(observer: Observer): void {
const index = this.observers.indexOf(observer);
if (index > -1) {
this.observers.splice(index, 1);
}
}
notify(): void {
for (const observer of this.observers) {
observer.update(this);
}
}
setMeasurements(temperature: number, humidity: number, pressure: number): void {
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
console.log(`Погодные данные обновлены: ${temperature}°C, ${humidity}%, ${pressure} hPa`);
this.notify();
}
getTemperature(): number {
return this.temperature;
}
getHumidity(): number {
return this.humidity;
}
getPressure(): number {
return this.pressure;
}
}
// Конкретные наблюдатели
class CurrentConditionsDisplay implements Observer {
private temperature: number = 0;
private humidity: number = 0;
update(subject: Subject): void {
if (subject instanceof WeatherStation) {
this.temperature = subject.getTemperature();
this.humidity = subject.getHumidity();
this.display();
}
}
display(): void {
console.log(`Текущие условия: ${this.temperature}°C, влажность ${this.humidity}%`);
}
}
class StatisticsDisplay implements Observer {
private temperatures: number[] = [];
private humidities: number[] = [];
update(subject: Subject): void {
if (subject instanceof WeatherStation) {
this.temperatures.push(subject.getTemperature());
this.humidities.push(subject.getHumidity());
this.display();
}
}
display(): void {
const avgTemp = this.temperatures.reduce((a, b) => a + b, 0) / this.temperatures.length;
const avgHumidity = this.humidities.reduce((a, b) => a + b, 0) / this.humidities.length;
console.log(`Статистика: средняя температура ${avgTemp.toFixed(1)}°C, средняя влажность ${avgHumidity.toFixed(1)}%`);
}
}
class ForecastDisplay implements Observer {
private lastPressure: number = 0;
private currentPressure: number = 0;
update(subject: Subject): void {
if (subject instanceof WeatherStation) {
this.lastPressure = this.currentPressure;
this.currentPressure = subject.getPressure();
this.display();
}
}
display(): void {
if (this.currentPressure > this.lastPressure) {
console.log('Прогноз: улучшение погоды');
} else if (this.currentPressure < this.lastPressure) {
console.log('Прогноз: ухудшение погоды');
} else {
console.log('Прогноз: погода остается неизменной');
}
}
}
// Использование
const weatherStation = new WeatherStation();
const currentDisplay = new CurrentConditionsDisplay();
const statisticsDisplay = new StatisticsDisplay();
const forecastDisplay = new ForecastDisplay();
// Подписываем наблюдателей
weatherStation.attach(currentDisplay);
weatherStation.attach(statisticsDisplay);
weatherStation.attach(forecastDisplay);
console.log('=== Обновления погодных данных ===');
weatherStation.setMeasurements(25, 65, 1013);
weatherStation.setMeasurements(27, 70, 1015);
weatherStation.setMeasurements(23, 60, 1010);
// Отписываем одного наблюдателя
weatherStation.detach(forecastDisplay);
console.log('\n=== После отписки прогноза ===');
weatherStation.setMeasurements(20, 55, 1008);
Преимущества
- Код становится удобен в обслуживании засчет меньшей степени зацепления между объектами — не требуется разбираться в отношениях и зависимостях между ними;
- Можно добавлять новых Субъектов и наблюдаталей, не внося изменений в код уже существующих;
- Обязанности между наблюдаемым объектом и наблюдателями разделены, что делает код более гибким и модульным.
Недостатки
- Программа может стать ресурсозатратной при большом количестве наблюдателей;
- Есть вероятность утечки защищенных данных с угрозой для их безопасности.
Отличия от других паттернов
- 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 — это основа реактивного программирования!