Разберите паттерн Наблюдатель (Observer): опишите реализацию в JavaScript и в C#, укажите возможные проблемы (порядок уведомлений, утечки) и способы их решения

16 Янв в 10:32
12 +1
0
Ответы
1
Кратко: паттерн Наблюдатель (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 для автоматического освобождения при необходимости, и контролируйте потокобезопасность/порядок уведомлений в соответствии с требованиями приложения.
16 Янв в 10:40
Не можешь разобраться в этой теме?
Обратись за помощью к экспертам
Гарантированные бесплатные доработки в течение 1 года
Быстрое выполнение от 2 часов
Проверка работы на плагиат
Поможем написать учебную работу
Прямой эфир