Разберите паттерн Наблюдатель (Observer): опишите реализацию в JavaScript и в C#, укажите возможные проблемы (порядок уведомлений, утечки) и способы их решения
Кратко: паттерн Наблюдатель (Observer) — объект (Subject) хранит набор наблюдателей (Observers) и уведомляет их при изменении состояния. Основные операции: подписка, отписка, уведомление. Важные вопросы: порядок уведомлений, удаление во время уведомления, утечки памяти, обработка исключений, потокобезопасность и производительность. 1) Пример на JavaScript (простая реализация) ```javascript class Subject { constructor() { this._observers = []; } // subscribe(fn) -> возвращает функцию для отписки subscribe(fn) { this._observers.push(fn); let removed = false; return () => { if (removed) return; removed = true; const i = this._observers.indexOf(fn); if (i >= 0) this._observers.splice(i, 1); }; } // notify(payload) — уведомляем каждого наблюдателя notify(payload) { // snapshot для защиты от изменений списка в процессе уведомления const snapshot = this._observers.slice(); for (const fn of snapshot) { try { fn(payload); } catch (e) { // по желанию логируем или собираем ошибки console.error('Observer error', e); } } } } ``` Пояснения: - Вызов `notify` делает копию списка -> защищаемся от отписок/добавлений во время итерации. - Стоимость подписки O(1)O(1)O(1) (push), отписки наивной реализации O(n)O(n)O(n), уведомления — O(n)O(n)O(n). - Исключения одного наблюдателя не прерывают других (обёртка try/catch). Слабые ссылки (чтобы избегать утечек): в современных движках можно применять `WeakRef` + `FinalizationRegistry` (сложно и небезопасно, но возможно) или хранить токен (объект) у подписчика и заставлять отписываться явно. Пример упрощённой идеи с `WeakRef`: ```javascript // упрощённый вариант: хранить WeakRef на объекты-слушатели class WeakSubject { constructor() { this._refs = []; this._registry = new FinalizationRegistry(token => { // можно пометить для очистки при GC }); } subscribe(obj, methodName) { const ref = new WeakRef(obj); this._refs.push({ref, methodName}); this._registry.register(obj, methodName); return () => { /* можно пометить для удаления при следующем notify */ }; } notify(...args) { const alive = []; for (const entry of this._refs) { const target = entry.ref.deref(); if (target) { try { target[entry.methodName](...args); } catch (e) { console.error(e); } alive.push(entry); } } this._refs = alive; // чистим мёртвые } } ``` (WeakRef/FinalizationRegistry — продвинутые инструменты, работают не во всех средах.) 2) Пример на C# (классический) ```csharp using System; class Subject { private event EventHandler _handlers; public IDisposable Subscribe(EventHandler handler) { _handlers += handler; return new Unsubscriber(() => _handlers -= handler); } public void Notify(object sender, T args) { // безопасная копия делегата для избежания race condition var handlers = _handlers; if (handlers == null) return; foreach (EventHandler h in handlers.GetInvocationList()) { try { h(sender, args); } catch (Exception ex) { // лог/сбор ошибок (не прерываем других обработчиков) Console.Error.WriteLine(ex); } } } private class Unsubscriber : IDisposable { private readonly Action _dispose; public Unsubscriber(Action dispose) { _dispose = dispose; } public void Dispose() { _dispose?.Invoke(); } } } ``` Пояснения: - При вызове события принято копировать делегат в локальную переменную (см. `handlers`) чтобы избежать состояния гонки с одновременной отпиской. - Перебираем `GetInvocationList()` и вызываем обработчики в порядке их комбинирования (обычно FIFO). Обработка каждого в try/catch предотвращает прерывание цепочки. - Подписка/отписка реализуются через `+=` / `-=`. Делегаты в CLR комбинируются атомарно, но для более сложных структур (например, список) нужен lock. Слабые ссылки в C#: чтобы избежать удержания подписчика живым через делегат, можно использовать паттерн Weak Event (WeakReference на целевой объект) или встроенный `WeakEventManager` (WPF) / реализовать обёртку, которая хранит `WeakReference` и `MethodInfo` и вызывает метод рефлекторно. Часто дают Subscribe, возвращающий `IDisposable`, и вызывают `Dispose()` при уничтожении подписчика. 3) Основные проблемы и способы решения (кратко) - Порядок уведомлений: - Проблема: нужно детерминированное поведение; изменение порядка при добавлении/удалении во время уведомления. - Решения: зафиксировать политику (обычно FIFO); делать snapshot перед notify; или использовать структуры, где удаление происходит лениво (пометка), затем очистка. - Утечки памяти: - Проблема: Subject удерживает ссылку на Observer -> Observer не GC. - Решения: - Требовать явной отписки (возвращать функцию/IDisposable). - Использовать слабые ссылки: `WeakRef`/`FinalizationRegistry` в JS, `WeakReference` или Weak Event pattern в C#. - Использовать короткоживущие посредники (event aggregator) и управлять временем жизни через DI/контекст. - Удаление во время уведомления (reentrancy): - Проблема: подписчик удаляет/добавляет других — может сломать итерацию. - Решения: итерировать по копии списка; поддерживать флаг «в процессе уведомления» и откладывать изменения; использовать двусвязный список с корректной логикой удаления. - Исключения в обработчике: - Проблема: одно исключение останавливает оповещение остальных. - Решения: ограждать вызов каждого обработчика try/catch; собирать и/или пробрасывать агрегированные ошибки (`AggregateError` в JS, `AggregateException` в C#), либо логировать. - Потокобезопасность: - Проблема: подписки/отписки/уведомления из разных потоков. - Решения: синхронизация (lock в C#), атомарные операции; при уведомлении копировать ссылку на делегат; в JS — одна нить, но асинхронность/реентрантность требует snapshot и осторожности с микротасками. - Производительность: - Проблема: уведомление nnn наблюдателей — стоимость O(n)O(n)O(n); частые операции удаления в массиве — O(n)O(n)O(n). - Решения: использовать подходящую структуру (связный список + хранение «ручек» для O(1)O(1)O(1)-удаления), или индексированные/хэшированные коллекции для быстрых поисков, избегать частых аллокаций (snapshot редко). Резюме (коротко): реализуйте Subject с явной отпиской (IDisposable / возвращаемая функция), делайте snapshot при notify, оборачивайте вызовы в try/catch, используйте WeakReference/WeakRef для автоматического освобождения при необходимости, и контролируйте потокобезопасность/порядок уведомлений в соответствии с требованиями приложения.
1) Пример на JavaScript (простая реализация)
```javascript
class Subject {
constructor() { this._observers = []; }
// subscribe(fn) -> возвращает функцию для отписки
subscribe(fn) {
this._observers.push(fn);
let removed = false;
return () => {
if (removed) return;
removed = true;
const i = this._observers.indexOf(fn);
if (i >= 0) this._observers.splice(i, 1);
};
}
// notify(payload) — уведомляем каждого наблюдателя
notify(payload) {
// snapshot для защиты от изменений списка в процессе уведомления
const snapshot = this._observers.slice();
for (const fn of snapshot) {
try {
fn(payload);
} catch (e) {
// по желанию логируем или собираем ошибки
console.error('Observer error', e);
}
}
}
}
```
Пояснения:
- Вызов `notify` делает копию списка -> защищаемся от отписок/добавлений во время итерации.
- Стоимость подписки O(1)O(1)O(1) (push), отписки наивной реализации O(n)O(n)O(n), уведомления — O(n)O(n)O(n).
- Исключения одного наблюдателя не прерывают других (обёртка try/catch).
Слабые ссылки (чтобы избегать утечек): в современных движках можно применять `WeakRef` + `FinalizationRegistry` (сложно и небезопасно, но возможно) или хранить токен (объект) у подписчика и заставлять отписываться явно. Пример упрощённой идеи с `WeakRef`:
```javascript
// упрощённый вариант: хранить WeakRef на объекты-слушатели
class WeakSubject {
constructor() { this._refs = []; this._registry = new FinalizationRegistry(token => {
// можно пометить для очистки при GC
}); }
subscribe(obj, methodName) {
const ref = new WeakRef(obj);
this._refs.push({ref, methodName});
this._registry.register(obj, methodName);
return () => { /* можно пометить для удаления при следующем notify */ };
}
notify(...args) {
const alive = [];
for (const entry of this._refs) {
const target = entry.ref.deref();
if (target) {
try { target[entry.methodName](...args); }
catch (e) { console.error(e); }
alive.push(entry);
}
}
this._refs = alive; // чистим мёртвые
}
}
```
(WeakRef/FinalizationRegistry — продвинутые инструменты, работают не во всех средах.)
2) Пример на C# (классический)
```csharp
using System;
class Subject {
private event EventHandler _handlers;
public IDisposable Subscribe(EventHandler handler) {
_handlers += handler;
return new Unsubscriber(() => _handlers -= handler);
}
public void Notify(object sender, T args) {
// безопасная копия делегата для избежания race condition
var handlers = _handlers;
if (handlers == null) return;
foreach (EventHandler h in handlers.GetInvocationList()) {
try {
h(sender, args);
} catch (Exception ex) {
// лог/сбор ошибок (не прерываем других обработчиков)
Console.Error.WriteLine(ex);
}
}
}
private class Unsubscriber : IDisposable {
private readonly Action _dispose;
public Unsubscriber(Action dispose) { _dispose = dispose; }
public void Dispose() { _dispose?.Invoke(); }
}
}
```
Пояснения:
- При вызове события принято копировать делегат в локальную переменную (см. `handlers`) чтобы избежать состояния гонки с одновременной отпиской.
- Перебираем `GetInvocationList()` и вызываем обработчики в порядке их комбинирования (обычно FIFO). Обработка каждого в try/catch предотвращает прерывание цепочки.
- Подписка/отписка реализуются через `+=` / `-=`. Делегаты в CLR комбинируются атомарно, но для более сложных структур (например, список) нужен lock.
Слабые ссылки в C#: чтобы избежать удержания подписчика живым через делегат, можно использовать паттерн Weak Event (WeakReference на целевой объект) или встроенный `WeakEventManager` (WPF) / реализовать обёртку, которая хранит `WeakReference` и `MethodInfo` и вызывает метод рефлекторно. Часто дают Subscribe, возвращающий `IDisposable`, и вызывают `Dispose()` при уничтожении подписчика.
3) Основные проблемы и способы решения (кратко)
- Порядок уведомлений:
- Проблема: нужно детерминированное поведение; изменение порядка при добавлении/удалении во время уведомления.
- Решения: зафиксировать политику (обычно FIFO); делать snapshot перед notify; или использовать структуры, где удаление происходит лениво (пометка), затем очистка.
- Утечки памяти:
- Проблема: Subject удерживает ссылку на Observer -> Observer не GC.
- Решения:
- Требовать явной отписки (возвращать функцию/IDisposable).
- Использовать слабые ссылки: `WeakRef`/`FinalizationRegistry` в JS, `WeakReference` или Weak Event pattern в C#.
- Использовать короткоживущие посредники (event aggregator) и управлять временем жизни через DI/контекст.
- Удаление во время уведомления (reentrancy):
- Проблема: подписчик удаляет/добавляет других — может сломать итерацию.
- Решения: итерировать по копии списка; поддерживать флаг «в процессе уведомления» и откладывать изменения; использовать двусвязный список с корректной логикой удаления.
- Исключения в обработчике:
- Проблема: одно исключение останавливает оповещение остальных.
- Решения: ограждать вызов каждого обработчика try/catch; собирать и/или пробрасывать агрегированные ошибки (`AggregateError` в JS, `AggregateException` в C#), либо логировать.
- Потокобезопасность:
- Проблема: подписки/отписки/уведомления из разных потоков.
- Решения: синхронизация (lock в C#), атомарные операции; при уведомлении копировать ссылку на делегат; в JS — одна нить, но асинхронность/реентрантность требует snapshot и осторожности с микротасками.
- Производительность:
- Проблема: уведомление nnn наблюдателей — стоимость O(n)O(n)O(n); частые операции удаления в массиве — O(n)O(n)O(n).
- Решения: использовать подходящую структуру (связный список + хранение «ручек» для O(1)O(1)O(1)-удаления), или индексированные/хэшированные коллекции для быстрых поисков, избегать частых аллокаций (snapshot редко).
Резюме (коротко): реализуйте Subject с явной отпиской (IDisposable / возвращаемая функция), делайте snapshot при notify, оборачивайте вызовы в try/catch, используйте WeakReference/WeakRef для автоматического освобождения при необходимости, и контролируйте потокобезопасность/порядок уведомлений в соответствии с требованиями приложения.