Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Патерн спостерігач». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Патерн Observer (спостерігач)** - поведінковий патерн, де суб'єкт автоматично сповіщає всіх підписаних спостерігачів про зміни свого стану. ```javascript const createSubject = () => { let observers = []; return { subscribe: (fn) => observers.push(fn), unsubscribe: (fn) => { observers = observers.filter(o => o !== fn); }, notify: (data) => observers.forEach(fn => fn(data)), }; }; ``` **Головне:** суб'єкт не імпортує спостерігачів напряму - він викликає їхній метод `update()`, не знаючи їхніх типів чи кількості.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Патерн Observer (спостерігач)** - поведінковий патерн проектування, де суб'єкт тримає список спостерігачів і автоматично сповіщає їх про зміни свого стану. ## Теорія ### TL;DR - Аналогія: YouTube-канал надсилає сповіщення підписникам про нові відео, не зберігаючи їхні адреси напряму - Суб'єкт розсилає зміни всім слухачам, не знаючи їхніх типів чи кількості - Використовуй, коли 2+ об'єктів потребують синхронізації стану без жорстких посилань між собою - Базова механіка: `subscribe()`, `notify()`, `unsubscribe()` - Node.js EventEmitter і React `useEffect` - обидві реалізації Observer ### Базовий приклад ```javascript class Subject { constructor() { this.observers = []; } subscribe(obs) { this.observers.push(obs); } unsubscribe(obs) { this.observers = this.observers.filter(o => o !== obs); } notify(data) { this.observers.forEach(obs => obs.update(data)); } } class Logger { update(data) { console.log(`Отримано: ${data}`); } } const subject = new Subject(); subject.subscribe(new Logger()); subject.notify('user logged in'); // Отримано: user logged in ``` `Subject` тримає масив посилань на спостерігачів. `notify()` перебирає їх і викликає `update()` кожного. `Subject` не імпортує `Logger` - не знає про нього нічого. Ось і вся ідея. ### Головна відмінність Прямий виклик методів жорстко прив'язує відправника до конкретних об'єктів. Якщо сервіс викликає `logger.log()` напряму, не можна замінити `logger` не торкаючись сервісу. Observer змінює цю логіку: суб'єкт не знає хто слухає. Спостерігачів додають і видаляють в рантаймі, суб'єкт продовжує працювати без змін. Саме це означає слабке зв'язування на практиці. ### Коли використовувати - Один об'єкт змінюється, багато реагує: користувач входить у систему, треба оновити UI, скинути кеш і відправити аналітику. Один виклик `notify()` замість трьох ручних. - Кількість спостерігачів змінюється в рантаймі: плагіни, фіча-флаги, динамічні дашборди. Жорстко прописаний список обробників зламається, щойно зміняться вимоги. - Потрібна слабка синхронізація між модулями: MVC-в'юхи спостерігають за моделлю, компоненти Redux підписуються на стор. - Пропусти патерн, якщо вистачає одного простого колбека або слухач завжди рівно один і ніколи не змінюється. ### Як notify працює всередині В JavaScript `notify()` за замовчуванням ітерує масив спостерігачів синхронно. Кожен виклик `obs.update()` проходить через ланцюжок прототипів за допомогою динамічного диспетчеризування. Node.js EventEmitter будується на цьому й направляє асинхронну роботу через libuv, щоб emit не блокував event loop. `useEffect` у React іде далі: компонент підписується на масив залежностей, і React запускає ефект після коміту рендеру до DOM. Обидва - та сама ідея на різних рівнях абстракції. ### Типові помилки **Забута відписка спричиняє витоки пам'яті.** Спостерігач залишається в масиві суб'єкта, збирач сміття не може його зібрати. На серверах, що живуть довго, це накопичується до сотень мегабайт. ```javascript // Неправильно: спостерігач залишається в пам'яті назавжди subject.subscribe(obs); // Правильно: завжди прибирай за собою subject.subscribe(obs); const cleanup = () => subject.unsubscribe(obs); // У React: return cleanup із useEffect ``` **Синхронний notify у рекурсивних ланцюжках викликає переповнення стека.** Якщо `update()` спостерігача знову запускає `notify()` - це рекурсія. V8 обмежує стек викликів приблизно 10k кадрами. ```javascript // Неправильно: subject.notify() -> obs.update() -> subject.notify() -> crash // Правильно: розірвати ланцюг асинхронно notify(data) { setImmediate(() => this.observers.forEach(obs => obs.update(data))); } ``` **Передача мутабельних об'єктів створює баги зі спільним станом.** Два спостерігачі отримують посилання на один об'єкт, обидва його змінюють - стан стає непередбачуваним. React strict mode виявляє це швидко. ```javascript // Неправильно: обидва поділяють одне посилання subject.notify({ users: usersArray }); // Правильно: передай знімок subject.notify({ users: [...usersArray] }); ``` **Відсутність обробки помилок ламає весь broadcast.** Один спостерігач кидає помилку - решта не отримують сповіщення. Це вже призводило до втрати даних у продакшні. ```javascript notify(data) { this.observers.forEach(obs => { try { obs.update(data); } catch (err) { console.error('Observer error:', err); } }); } ``` ### Де зустрічається - React: масив залежностей у `useEffect` спостерігає за пропсами/станом; функція прибирання = відписка - Node.js EventEmitter: `req.on('data', handler)` спостерігає за чанками потоку в модулі http - Redux: `store.subscribe()` сповіщає підключені компоненти при dispatch; RTK Query використовує ту саму ідею для стану API - RxJS: `Observable.subscribe()` - це Observer плюс скасування через `Subscription` - Реактивність Vue: обчислювані властивості (computed properties) спостерігають за реактивними даними й перераховуються при змінах - Проти Pub-Sub: Observer використовує прямі посилання (суб'єкт тримає реф на спостерігачів), Pub-Sub додає брокера для повністю розв'язаного роутингу за темами ### Питання для співбесіди **Q:** Як реалізувати Observer без класів у 10 рядків? **A:** Через замикання (closure). `const createSubject = () => { let obs = []; return { subscribe: f => obs.push(f), unsubscribe: f => { obs = obs.filter(o => o !== f); }, notify: d => obs.forEach(f => f(d)) }; };`. Той самий контракт, без накладних витрат класів. **Q:** Яка різниця між Observer і Pub-Sub? **A:** Observer прямий: суб'єкт тримає посилання на спостерігачів. Pub-Sub додає брокера посередині. Підписники реєструються на тему, а видавець не знає хто слухає. Redis pub-sub і MQTT так і працюють. Observer - для синхронізації стану всередині процесу, Pub-Sub - для розподілених систем. **Q:** Чим відрізняється `useEffect` у React від класичного Observer? **A:** Класичний Observer сповіщає синхронно і негайно. React групує зміни і запускає ефекти після коміту рендеру до DOM. Функція прибирання відповідає відписці. Масив залежностей визначає що саме спостерігає компонент. **Q:** Як обробляти витоки EventEmitter у Node.js-кластерах? **A:** Слухачі для кожного форка накопичуються без прибирання. Для одноразових подій використовуй `emitter.once()` - він видаляється автоматично. Для постійних слухачів реєструй `process.on('exit', cleanup)`. В тестах викликай `removeAllListeners()` при teardown. Патерн `once()` джуніори зазвичай пропускають, сеньйори згадують одразу. ## Приклади ### Базовий Observer на TypeScript ```typescript interface IObserver { update(subject: ISubject): void; } interface ISubject { attach(observer: IObserver): void; detach(observer: IObserver): void; notify(): void; } class UserStore implements ISubject { private observers: IObserver[] = []; private loggedIn: boolean = false; attach(observer: IObserver): void { this.observers.push(observer); } detach(observer: IObserver): void { this.observers = this.observers.filter(o => o !== observer); } notify(): void { for (const observer of this.observers) { try { observer.update(this); } catch (err) { console.error('Observer error:', err); } } } login(): void { this.loggedIn = true; console.log('UserStore: user logged in'); this.notify(); } isLoggedIn(): boolean { return this.loggedIn; } } class NavbarObserver implements IObserver { update(subject: ISubject): void { if (subject instanceof UserStore && subject.isLoggedIn()) { console.log('Navbar: showing user menu'); } } } class AnalyticsObserver implements IObserver { update(subject: ISubject): void { if (subject instanceof UserStore && subject.isLoggedIn()) { console.log('Analytics: tracking login event'); } } } const store = new UserStore(); store.attach(new NavbarObserver()); store.attach(new AnalyticsObserver()); store.login(); // UserStore: user logged in // Navbar: showing user menu // Analytics: tracking login event ``` `UserStore` викликає `notify()` після зміни стану. Обидва спостерігачі реагують незалежно і не знають один про одного. Щоб додати третій (наприклад, інвалідацію кешу) - `UserStore` чіпати не потрібно. ### Node.js EventEmitter з правильним прибиранням EventEmitter - стандартна реалізація Observer у Node.js. Витік пам'яті тут легко пропустити на завантаженому сервері. ```javascript const { EventEmitter } = require('events'); const dataStream = new EventEmitter(); function startListening() { const handleData = (data) => { console.log('Отримано чанк:', data); }; dataStream.on('data', handleData); // Завжди повертай функцію прибирання return () => dataStream.off('data', handleData); } const stop = startListening(); dataStream.emit('data', 'chunk 1'); // Отримано чанк: chunk 1 dataStream.emit('data', 'chunk 2'); // Отримано чанк: chunk 2 stop(); // Відписка dataStream.emit('data', 'chunk 3'); // Нічого не виводиться ``` Без виклику `stop()` функція `handleData` залишається в масиві слухачів назавжди. На сервері, що обробляє тисячі запитів, це швидко накопичується. Бачив такий патерн, що з'їдав 200MB хіпу за одну продакшн-сесію. ### React useEffect як Observer Масив залежностей у React - це Observer, вбудований у фреймворк. Компонент підписується на зміни конкретних значень, React управляє циклом notify/unsubscribe. ```jsx import { useEffect, useState } from 'react'; function PriceDisplay({ productId }) { const [price, setPrice] = useState(null); useEffect(() => { let active = true; // Захист від застарілих оновлень async function fetchPrice() { const data = await fetch(`/api/prices/${productId}`).then(r => r.json()); if (active) setPrice(data.price); } fetchPrice(); // Прибирання: відписка при зміні productId або розмонтуванні return () => { active = false; }; }, [productId]); // productId - суб'єкт, за яким спостерігаємо return <div>Ціна: {price ?? 'Завантаження...'}</div>; } ``` Коли `productId` змінюється, React запускає прибирання (відписка від старого значення), потім знову запускає ефект (підписка на нове). Без прибирання застарілий запит може оновити стан після того, як компонент вже перейшов на інший продукт.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.