Наблюдатель
Также известен как: Издатель-Подписчик, Слушатель, Observer
Наблюдатель — это поведенческий паттерн проектирования, который создаёт механизм подписки, позволяющий одним объектам следить и реагировать на события, происходящие в других объектах.

Проблема
Представьте, что вы имеете два объекта: Покупатель и Магазин. В магазин вот-вот должны завезти новый товар, который интересен покупателю.
Покупатель может каждый день ходить в магазин, чтобы проверить наличие товара. Но при этом он будет злиться, без толку тратя своё драгоценное время.

Постоянное посещение магазина или спам?
С другой стороны, магазин может разослать спам каждому своему покупателю. Многих это расстроит, так как товар специфический, и не всем он нужен.
Получается конфликт: либо покупатель тратит время на периодические проверки, либо магазин тратит ресурсы на бесполезные оповещения.
Решение
Давайте называть Издателями те объекты, которые содержат важное или интересное для других состояние. Остальные объекты, которые хотят отслеживать изменения этого состояния, назовём Подписчиками.
Паттерн Наблюдатель предлагает хранить внутри объекта издателя список ссылок на объекты подписчиков, причём издатель не должен вести список подписки самостоятельно. Он предоставит методы, с помощью которых подписчики могли бы добавлять или убирать себя из списка.

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

Оповещения о событиях.
Увидев, как складно всё работает, вы можете выделить общий интерфейс, описывающий методы подписки и отписки, и для всех издателей. После этого подписчики смогут работать с разными типами издателей, а также получать оповещения от них через один и тот же метод.
Аналогия из жизни

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

- Издатель владеет внутренним состоянием, изменение которого интересно отслеживать подписчикам. Издатель содержит механизм подписки: список подписчиков и методы подписки/отписки.
- Когда внутреннее состояние издателя меняется, он оповещает своих подписчиков. Для этого издатель проходит по списку подписчиков и вызывает их метод оповещения, заданный в общем интерфейсе подписчиков.
- Подписчик определяет интерфейс, которым пользуется издатель для отправки оповещения. В большинстве случаев для этого достаточно единственного метода.
- Конкретные подписчики выполняют что-то в ответ на оповещение, пришедшее от издателя. Эти классы должны следовать общему интерфейсу подписчиков, чтобы издатель не зависел от конкретных классов подписчиков.
- По приходу оповещения подписчику нужно получить обновлённое состояние издателя. Издатель может передать это состояние через параметры метода оповещения. Более гибкий вариант — передавать через параметры весь объект издателя, чтобы подписчик мог сам получить требуемые данные. Как вариант, подписчик может постоянно хранить ссылку на объект издателя, переданный ему в конструкторе.
- Клиент создаёт объекты издателей и подписчиков, а затем регистрирует подписчиков на обновления в издателях.
Псевдокод
В этом примере Наблюдатель позволяет объекту текстового редактора оповещать другие объекты об изменениях своего состояния.

Пример оповещения объектов о событиях в других объектах.
Список подписчиков составляется динамически, объекты могут как подписываться на определённые события, так и отписываться от них прямо во время выполнения программы.
В этой реализации редактор не ведёт список подписчиков самостоятельно, а делегирует это вложенному объекту. Это даёт возможность использовать механизм подписки не только в классе редактора, но и в других классах программы.
Для добавления в программу новых подписчиков не нужно менять классы издателей, пока они работают с подписчиками через общий интерфейс.
|
Применимость
Когда после изменения состояния одного объекта требуется что-то сделать в других, но вы не знаете наперёд, какие именно объекты должны отреагировать.
Описанная проблема может возникнуть при разработке библиотек пользовательского интерфейса, когда вам надо дать возможность сторонним классам реагировать на клики по кнопкам.
Паттерн Наблюдатель позволяет любому объекту с интерфейсом подписчика зарегистрироваться на получение оповещений о событиях, происходящих в объектах-издателях.
Когда одни объекты должны наблюдать за другими, но только в определённых случаях.
Издатели ведут динамические списки. Все наблюдатели могут подписываться или отписываться от получения оповещений прямо во время выполнения программы.
Шаги реализации
- Разбейте вашу функциональность на две части: независимое ядро и опциональные зависимые части. Независимое ядро станет издателем. Зависимые части станут подписчиками.
- Создайте интерфейс подписчиков. Обычно в нём достаточно определить единственный метод оповещения.
- Создайте интерфейс издателей и опишите в нём операции управления подпиской. Помните, что издатель должен работать только с общим интерфейсом подписчиков.
- Вам нужно решить, куда поместить код ведения подписки, ведь он обычно бывает одинаков для всех типов издателей. Самый очевидный способ — вынести этот код в промежуточный абстрактный класс, от которого будут наследоваться все издатели.
Но если вы интегрируете паттерн в существующие классы, то создать новый базовый класс может быть затруднительно. В этом случае вы можете поместить логику подписки во вспомогательный объект и делегировать ему работу из издателей. - Создайте классы конкретных издателей. Реализуйте их так, чтобы после каждого изменения состояния они отправляли оповещения всем своим подписчикам.
- Реализуйте метод оповещения в конкретных подписчиках. Не забудьте предусмотреть параметры, через которые издатель мог бы отправлять какие-то данные, связанные с происшедшим событием.
Возможен и другой вариант, когда подписчик, получив оповещение, сам возьмёт из объекта издателя нужные данные. Но в этом случае вы будете вынуждены привязать класс подписчика к конкретному классу издателя. - Клиент должен создавать необходимое количество объектов подписчиков и подписывать их у издателей.
Преимущества и недостатки
Преимущества
- Издатели не зависят от конкретных классов подписчиков и наоборот.
- Вы можете подписывать и отписывать получателей на лету.
- Реализует принцип открытости/закрытости.
Недостатки
- Подписчики оповещаются в случайном порядке.
Примеры реализации паттерна
Python
- Сложность: 2/3
- Популярность: 3/3
- Применимость: Наблюдатель можно часто встретить в Python коде, особенно там, где применяется событийная модель отношений между компонентами. Наблюдатель позволяет отдельным компонентам реагировать на события, происходящие в других компонентах.
- Признаки применения паттерна: Наблюдатель можно определить по механизму подписки и методам оповещения, которые вызывают компоненты программы.
Этот пример показывает структуру паттерна Наблюдатель, а именно — из каких классов он состоит, какие роли эти классы выполняют и как они взаимодействуют друг с другом.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 | |
Output.txt: Результат выполнения
Subject: Attached an observer.
Subject: Attached an observer.
Subject: I'm doing something important.
Subject: My state has just changed to: 0
Subject: Notifying observers...
ConcreteObserverA: Reacted to the event
ConcreteObserverB: Reacted to the event
Subject: I'm doing something important.
Subject: My state has just changed to: 5
Subject: Notifying observers...
ConcreteObserverB: Reacted to the event
Subject: I'm doing something important.
Subject: My state has just changed to: 0
Subject: Notifying observers...
ConcreteObserverB: Reacted to the event
PHP
- Сложность: 2/3
- Популярность: 3/3
- Применимость: Наблюдатель можно часто встретить в PHP коде, особенно там, где применяется событийная модель отношений между компонентами. Наблюдатель позволяет отдельным компонентам реагировать на события, происходящие в других компонентах.
PHP имеет несколько встроенных интерфейсов (SplSubject,SplObserver), на которых можно строить свои реализации Наблюдателя, совместимые с остальным PHP-кодом. - Признаки применения паттерна: Наблюдатель можно определить по механизму подписки и методам оповещения, которые вызывают компоненты программы.
Этот пример показывает структуру паттерна Наблюдатель, а именно — из каких классов он состоит, какие роли эти классы выполняют и как они взаимодействуют друг с другом.
После ознакомления со структурой, вам будет легче воспринимать второй пример, который рассматривает реальный случай использования паттерна в мире PHP.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 | |
Subject: Attached an observer.
Subject: Attached an observer.
Subject: I'm doing something important.
Subject: My state has just changed to: 2
Subject: Notifying observers...
ConcreteObserverA: Reacted to the event.
ConcreteObserverB: Reacted to the event.
Subject: I'm doing something important.
Subject: My state has just changed to: 4
Subject: Notifying observers...
ConcreteObserverB: Reacted to the event.
Subject: Detached an observer.
Subject: I'm doing something important.
Subject: My state has just changed to: 1
Subject: Notifying observers...
ConcreteObserverA: Reacted to the event.
В этом примере паттерн Наблюдатель позволяет различным объектам наблюдать за событиями, происходящими в пользовательском репозитории приложения. Репозиторий генерирует различные типы событий и позволяет наблюдателям прослушивать их все, а так же лишь отдельные из них.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 | |
UserRepository: Loading user records from a file.
UserRepository: Broadcasting the 'users:init' event.
Logger: I've written 'users:init' entry to the log.
UserRepository: Creating a user.
UserRepository: Broadcasting the 'users:created' event.
OnboardingNotification: The notification has been emailed!
Logger: I've written 'users:created' entry to the log.
UserRepository: Deleting a user.
UserRepository: Broadcasting the 'users:deleted' event.
Logger: I've written 'users:deleted' entry to the log.
Задачи
Задача 1. Система уведомлений о ценах товаров
Реализуйте магазин как издателя, который оповещает подписчиков об изменении цен. Подписчики: покупатель (получает уведомление если цена ниже желаемой), аналитик (логирует все изменения цен), менеджер скидок (автоматически добавляет скидку если цена выросла более чем на 10%).
from __future__ import annotations
from abc import ABC, abstractmethod
class PriceObserver(ABC):
@abstractmethod
def update(self, product: str, old_price: float,
new_price: float) -> None: ...
class ProductCatalog:
def __init__(self) -> None:
self._prices: dict[str, float] = {}
self._observers: list[PriceObserver] = []
def subscribe(self, observer: PriceObserver) -> None:
self._observers.append(observer)
def unsubscribe(self, observer: PriceObserver) -> None:
self._observers.remove(observer)
def set_price(self, product: str, price: float) -> None:
# TODO: сохранить цену, уведомить подписчиков если цена изменилась
pass
def get_price(self, product: str) -> float | None:
return self._prices.get(product)
def _notify(self, product: str, old_price: float,
new_price: float) -> None:
# TODO: оповестить всех подписчиков
pass
class WishlistObserver(PriceObserver):
"""Покупатель с желаемой максимальной ценой для товаров."""
def __init__(self, name: str) -> None:
self.name = name
self._wishlist: dict[str, float] = {} # product -> max_price
self.notifications: list[str] = []
def add_to_wishlist(self, product: str, max_price: float) -> None:
self._wishlist[product] = max_price
def update(self, product: str, old_price: float,
new_price: float) -> None:
# TODO: уведомить если товар в списке и цена <= желаемой
pass
class PriceAnalytics(PriceObserver):
"""Логирует все изменения цен и считает статистику."""
def __init__(self) -> None:
self.log: list[tuple[str, float, float]] = []
def update(self, product: str, old_price: float,
new_price: float) -> None:
# TODO: записать изменение в лог
pass
def average_change(self) -> float:
# TODO: вернуть среднее изменение цены в процентах
pass
class DiscountManager(PriceObserver):
"""Автоматически создаёт скидку при росте цены более 10%."""
def __init__(self) -> None:
self.discounts: dict[str, float] = {} # product -> discount%
def update(self, product: str, old_price: float,
new_price: float) -> None:
# TODO: если рост > 10% — добавить скидку 5%
pass
# Ожидаемый результат:
# catalog = ProductCatalog()
# alice = WishlistObserver("Alice")
# alice.add_to_wishlist("iPhone", max_price=800.0)
# analytics = PriceAnalytics()
# discounts = DiscountManager()
# catalog.subscribe(alice); catalog.subscribe(analytics); catalog.subscribe(discounts)
# catalog.set_price("iPhone", 1000.0)
# catalog.set_price("iPhone", 750.0)
# # [Alice] iPhone теперь стоит 750.0 — меньше вашего лимита 800.0!
# catalog.set_price("iPhone", 900.0)
# # DiscountManager: скидка 5% на iPhone (цена выросла на 20%)
# print(analytics.log)
<?php
interface PriceObserver
{
public function update(string $product, float $oldPrice, float $newPrice): void;
}
class ProductCatalog
{
private array $prices = [];
private array $observers = [];
public function subscribe(PriceObserver $observer): void
{
$this->observers[] = $observer;
}
public function unsubscribe(PriceObserver $observer): void
{
$this->observers = array_filter(
$this->observers,
fn($o) => $o !== $observer
);
}
public function setPrice(string $product, float $price): void
{
// TODO: сохранить цену, уведомить если изменилась
}
public function getPrice(string $product): ?float
{
return $this->prices[$product] ?? null;
}
private function notify(string $product, float $oldPrice, float $newPrice): void
{
// TODO: оповестить всех подписчиков
}
}
class WishlistObserver implements PriceObserver
{
private array $wishlist = []; // product => maxPrice
public array $notifications = [];
public function __construct(private readonly string $name) {}
public function addToWishlist(string $product, float $maxPrice): void
{
$this->wishlist[$product] = $maxPrice;
}
public function update(string $product, float $oldPrice, float $newPrice): void
{
// TODO: уведомить если товар в списке и цена <= желаемой
}
}
class PriceAnalytics implements PriceObserver
{
public array $log = [];
public function update(string $product, float $oldPrice, float $newPrice): void
{
// TODO: записать изменение в лог
}
public function averageChange(): float
{
// TODO: среднее изменение цены в процентах
}
}
class DiscountManager implements PriceObserver
{
public array $discounts = [];
public function update(string $product, float $oldPrice, float $newPrice): void
{
// TODO: если рост > 10% — добавить скидку 5%
}
}
// Ожидаемый результат:
// $catalog = new ProductCatalog();
// $alice = new WishlistObserver("Alice");
// $alice->addToWishlist("iPhone", 800.0);
// $analytics = new PriceAnalytics();
// $discounts = new DiscountManager();
// $catalog->subscribe($alice); $catalog->subscribe($analytics); $catalog->subscribe($discounts);
// $catalog->setPrice("iPhone", 1000.0);
// $catalog->setPrice("iPhone", 750.0);
// // [Alice] iPhone теперь стоит 750.0 — меньше вашего лимита 800.0!
// $catalog->setPrice("iPhone", 900.0);
// // DiscountManager: скидка 5% на iPhone (цена выросла на 20%)
// print_r($analytics->log);
Задача 2. EventEmitter с типизированными событиями
Реализуйте универсальный EventEmitter, поддерживающий: подписку с именованным событием (on), одноразовую подписку (once), отписку (off), генерацию события (emit). Подписчиками могут быть любые callable (функции, лямбды, методы).
from __future__ import annotations
from collections import defaultdict
from typing import Any, Callable
class EventEmitter:
def __init__(self) -> None:
self._listeners: dict[str, list[Callable]] = defaultdict(list)
self._once: dict[str, set[Callable]] = defaultdict(set)
def on(self, event: str, callback: Callable) -> "EventEmitter":
# TODO: подписать callback на event, вернуть self для цепочки
pass
def once(self, event: str, callback: Callable) -> "EventEmitter":
# TODO: подписать callback только на одно срабатывание
pass
def off(self, event: str, callback: Callable) -> "EventEmitter":
# TODO: отписать callback от event
pass
def emit(self, event: str, *args: Any, **kwargs: Any) -> int:
# TODO: вызвать все подписанные callback'и, вернуть кол-во вызовов
# одноразовые после вызова удалить
pass
def listener_count(self, event: str) -> int:
return len(self._listeners.get(event, []))
def event_names(self) -> list[str]:
return list(self._listeners.keys())
# Ожидаемый результат:
# emitter = EventEmitter()
# log = []
# emitter.on("data", lambda x: log.append(f"A:{x}"))
# emitter.on("data", lambda x: log.append(f"B:{x}"))
# emitter.once("data", lambda x: log.append(f"ONCE:{x}"))
# emitter.emit("data", "hello")
# print(log) # ["A:hello", "B:hello", "ONCE:hello"]
# emitter.emit("data", "world")
# print(log) # ["A:hello", "B:hello", "ONCE:hello", "A:world", "B:world"]
# print(emitter.listener_count("data")) # 2 (once уже удалён)
<?php
class EventEmitter
{
private array $listeners = [];
private array $once = [];
public function on(string $event, callable $callback): static
{
// TODO: подписать callback, вернуть $this для цепочки
}
public function once(string $event, callable $callback): static
{
// TODO: подписать callback только на одно срабатывание
}
public function off(string $event, callable $callback): static
{
// TODO: отписать callback от event
}
public function emit(string $event, mixed ...$args): int
{
// TODO: вызвать все callback'и, одноразовые удалить после вызова,
// вернуть количество вызовов
}
public function listenerCount(string $event): int
{
return count($this->listeners[$event] ?? []);
}
public function eventNames(): array
{
return array_keys($this->listeners);
}
}
// Ожидаемый результат:
// $emitter = new EventEmitter();
// $log = [];
// $emitter->on("data", function($x) use (&$log) { $log[] = "A:$x"; });
// $emitter->on("data", function($x) use (&$log) { $log[] = "B:$x"; });
// $emitter->once("data", function($x) use (&$log) { $log[] = "ONCE:$x"; });
// $emitter->emit("data", "hello");
// print_r($log); // ["A:hello", "B:hello", "ONCE:hello"]
// $emitter->emit("data", "world");
// print_r($log); // [..., "A:world", "B:world"]
// echo $emitter->listenerCount("data"); // 2
Задача 3. Реактивные данные (Reactive Properties)
Реализуйте реактивное свойство ReactiveProperty<T>, которое оповещает подписчиков при изменении значения. На основе него постройте ReactiveModel для формы регистрации: поля username, email, password — реактивные; подписчик FormValidator проверяет валидность при каждом изменении; LivePreview отображает актуальный предпросмотр.
from __future__ import annotations
from typing import Generic, TypeVar, Callable
T = TypeVar("T")
class ReactiveProperty(Generic[T]):
def __init__(self, initial: T) -> None:
self._value = initial
self._subscribers: list[Callable[[T, T], None]] = []
@property
def value(self) -> T:
return self._value
@value.setter
def value(self, new_val: T) -> None:
# TODO: установить значение и уведомить подписчиков
# если значение изменилось (old_val, new_val)
pass
def subscribe(self, callback: Callable[[T, T], None]) -> None:
# TODO: добавить подписчика
pass
def unsubscribe(self, callback: Callable[[T, T], None]) -> None:
self._subscribers.remove(callback)
class FormValidator:
def __init__(self, model: "RegistrationModel") -> None:
self.errors: dict[str, str] = {}
# TODO: подписаться на изменения полей модели
def _validate_username(self, old: str, new: str) -> None:
# TODO: длина >= 3, только буквы и цифры
pass
def _validate_email(self, old: str, new: str) -> None:
# TODO: должен содержать '@' и '.'
pass
def _validate_password(self, old: str, new: str) -> None:
# TODO: длина >= 8
pass
@property
def is_valid(self) -> bool:
return len(self.errors) == 0
class LivePreview:
def __init__(self, model: "RegistrationModel") -> None:
self.preview = ""
# TODO: подписаться на изменения полей, обновлять preview
def _update(self, old: str, new: str) -> None:
# TODO: собрать превью из текущих значений модели
pass
class RegistrationModel:
def __init__(self) -> None:
self.username: ReactiveProperty[str] = ReactiveProperty("")
self.email: ReactiveProperty[str] = ReactiveProperty("")
self.password: ReactiveProperty[str] = ReactiveProperty("")
# Ожидаемый результат:
# model = RegistrationModel()
# validator = FormValidator(model)
# preview = LivePreview(model)
# model.username.value = "Al"
# print(validator.errors) # {"username": "слишком короткое"}
# model.username.value = "Alice"
# print(validator.errors) # {}
# model.email.value = "bad-email"
# print(validator.is_valid) # False
# model.email.value = "[email protected]"
# model.password.value = "secret123"
# print(validator.is_valid) # True
# print(preview.preview) # "Alice | [email protected]"
<?php
/**
* @template T
*/
class ReactiveProperty
{
private mixed $value;
private array $subscribers = [];
public function __construct(mixed $initial)
{
$this->value = $initial;
}
public function get(): mixed
{
return $this->value;
}
public function set(mixed $newVal): void
{
// TODO: установить значение и уведомить подписчиков если изменилось
}
public function subscribe(callable $callback): void
{
// TODO: добавить подписчика
}
public function unsubscribe(callable $callback): void
{
$this->subscribers = array_filter(
$this->subscribers,
fn($s) => $s !== $callback
);
}
}
class RegistrationModel
{
public ReactiveProperty $username;
public ReactiveProperty $email;
public ReactiveProperty $password;
public function __construct()
{
$this->username = new ReactiveProperty("");
$this->email = new ReactiveProperty("");
$this->password = new ReactiveProperty("");
}
}
class FormValidator
{
public array $errors = [];
public function __construct(private readonly RegistrationModel $model)
{
// TODO: подписаться на изменения всех полей
}
private function validateUsername(mixed $old, mixed $new): void
{
// TODO: длина >= 3, только буквы/цифры
}
private function validateEmail(mixed $old, mixed $new): void
{
// TODO: содержит '@' и '.'
}
private function validatePassword(mixed $old, mixed $new): void
{
// TODO: длина >= 8
}
public function isValid(): bool
{
return empty($this->errors);
}
}
class LivePreview
{
public string $preview = "";
public function __construct(private readonly RegistrationModel $model)
{
// TODO: подписаться на изменения, обновлять preview
}
}
// Ожидаемый результат:
// $model = new RegistrationModel();
// $validator = new FormValidator($model);
// $preview = new LivePreview($model);
// $model->username->set("Al");
// print_r($validator->errors); // ["username" => "слишком короткое"]
// $model->username->set("Alice");
// print_r($validator->errors); // []
// $model->email->set("bad-email");
// var_dump($validator->isValid()); // false
// $model->email->set("[email protected]");
// $model->password->set("secret123");
// var_dump($validator->isValid()); // true
// echo $preview->preview; // "Alice | [email protected]"
Задача 4. Мониторинг состояния сервера
Реализуйте систему мониторинга сервера. ServerMonitor периодически собирает метрики (CPU, RAM, диск) и оповещает подписчиков. Подписчики: AlertSystem (триггерит алерт если метрика превышает порог), MetricsDashboard (хранит историю последних N значений), AutoScaler (увеличивает количество инстансов при высокой нагрузке).
from __future__ import annotations
from abc import ABC, abstractmethod
from collections import deque
from dataclasses import dataclass
@dataclass
class ServerMetrics:
cpu_percent: float
ram_percent: float
disk_percent: float
timestamp: str = ""
class MetricsObserver(ABC):
@abstractmethod
def update(self, metrics: ServerMetrics) -> None: ...
class ServerMonitor:
def __init__(self) -> None:
self._observers: list[MetricsObserver] = []
self._current: ServerMetrics | None = None
def subscribe(self, observer: MetricsObserver) -> None:
self._observers.append(observer)
def unsubscribe(self, observer: MetricsObserver) -> None:
self._observers.remove(observer)
def report_metrics(self, metrics: ServerMetrics) -> None:
# TODO: сохранить метрики и оповестить подписчиков
pass
def _notify(self) -> None:
# TODO: оповестить всех подписчиков текущими метриками
pass
class AlertSystem(MetricsObserver):
def __init__(self, thresholds: dict[str, float]) -> None:
# thresholds: {"cpu": 80.0, "ram": 90.0, "disk": 95.0}
self.thresholds = thresholds
self.active_alerts: list[str] = []
def update(self, metrics: ServerMetrics) -> None:
# TODO: проверить каждую метрику против порога,
# добавить в active_alerts если превышен
pass
class MetricsDashboard(MetricsObserver):
def __init__(self, history_size: int = 10) -> None:
self._history: deque[ServerMetrics] = deque(maxlen=history_size)
def update(self, metrics: ServerMetrics) -> None:
# TODO: добавить метрики в историю
pass
def average_cpu(self) -> float:
# TODO: средний CPU за всю историю
pass
def get_history(self) -> list[ServerMetrics]:
return list(self._history)
class AutoScaler(MetricsObserver):
def __init__(self, cpu_threshold: float = 75.0) -> None:
self.instances = 1
self.cpu_threshold = cpu_threshold
self.scale_events: list[str] = []
def update(self, metrics: ServerMetrics) -> None:
# TODO: если CPU > порога — увеличить instances на 1,
# если CPU < порога / 2 и instances > 1 — уменьшить
pass
# Ожидаемый результат:
# monitor = ServerMonitor()
# alert = AlertSystem({"cpu": 80.0, "ram": 90.0, "disk": 95.0})
# dash = MetricsDashboard(history_size=5)
# scaler = AutoScaler(cpu_threshold=70.0)
# monitor.subscribe(alert); monitor.subscribe(dash); monitor.subscribe(scaler)
# monitor.report_metrics(ServerMetrics(cpu_percent=85.0, ram_percent=60.0, disk_percent=50.0))
# print(alert.active_alerts) # ["CPU превышен: 85.0% > 80.0%"]
# print(scaler.instances) # 2
# monitor.report_metrics(ServerMetrics(cpu_percent=20.0, ram_percent=40.0, disk_percent=50.0))
# print(scaler.instances) # 1
# print(dash.average_cpu()) # 52.5
<?php
class ServerMetrics
{
public function __construct(
public readonly float $cpuPercent,
public readonly float $ramPercent,
public readonly float $diskPercent,
public readonly string $timestamp = "",
) {}
}
interface MetricsObserver
{
public function update(ServerMetrics $metrics): void;
}
class ServerMonitor
{
private array $observers = [];
private ?ServerMetrics $current = null;
public function subscribe(MetricsObserver $observer): void
{
$this->observers[] = $observer;
}
public function unsubscribe(MetricsObserver $observer): void
{
$this->observers = array_filter($this->observers, fn($o) => $o !== $observer);
}
public function reportMetrics(ServerMetrics $metrics): void
{
// TODO: сохранить и оповестить подписчиков
}
private function notify(): void
{
// TODO: оповестить всех подписчиков
}
}
class AlertSystem implements MetricsObserver
{
public array $activeAlerts = [];
public function __construct(private readonly array $thresholds) {}
public function update(ServerMetrics $metrics): void
{
// TODO: проверить каждую метрику против порога
}
}
class MetricsDashboard implements MetricsObserver
{
private array $history = [];
public function __construct(private readonly int $historySize = 10) {}
public function update(ServerMetrics $metrics): void
{
// TODO: добавить в историю, обрезать до historySize
}
public function averageCpu(): float
{
// TODO: средний CPU за историю
}
public function getHistory(): array { return $this->history; }
}
class AutoScaler implements MetricsObserver
{
public int $instances = 1;
public array $scaleEvents = [];
public function __construct(private readonly float $cpuThreshold = 75.0) {}
public function update(ServerMetrics $metrics): void
{
// TODO: масштабировать вверх/вниз по CPU
}
}
// Ожидаемый результат:
// $monitor = new ServerMonitor();
// $alert = new AlertSystem(["cpu" => 80.0, "ram" => 90.0, "disk" => 95.0]);
// $dash = new MetricsDashboard(historySize: 5);
// $scaler = new AutoScaler(cpuThreshold: 70.0);
// $monitor->subscribe($alert); $monitor->subscribe($dash); $monitor->subscribe($scaler);
// $monitor->reportMetrics(new ServerMetrics(85.0, 60.0, 50.0));
// print_r($alert->activeAlerts); // ["CPU превышен: 85.0% > 80.0%"]
// echo $scaler->instances; // 2
// $monitor->reportMetrics(new ServerMetrics(20.0, 40.0, 50.0));
// echo $scaler->instances; // 1
// echo $dash->averageCpu(); // 52.5
Задача 5. Система наблюдения за изменениями в коллекции
Реализуйте ObservableCollection<T> — коллекцию, оповещающую подписчиков при добавлении, удалении и изменении элементов. События: item_added, item_removed, item_changed, collection_cleared. Дополнительно реализуйте FilteredView — «живой» отфильтрованный вид, который автоматически обновляется при изменении базовой коллекции.
from __future__ import annotations
from typing import Generic, TypeVar, Callable, Iterator
T = TypeVar("T")
class CollectionEvent(Generic[T]):
def __init__(self, event_type: str, item: T | None = None,
index: int = -1, old_item: T | None = None) -> None:
self.event_type = event_type # "added", "removed", "changed", "cleared"
self.item = item
self.index = index
self.old_item = old_item
class ObservableCollection(Generic[T]):
def __init__(self) -> None:
self._items: list[T] = []
self._listeners: dict[str, list[Callable[[CollectionEvent[T]], None]]] = {
"added": [], "removed": [], "changed": [], "cleared": []
}
def on(self, event: str,
callback: Callable[[CollectionEvent[T]], None]) -> None:
# TODO: подписать на событие
pass
def off(self, event: str,
callback: Callable[[CollectionEvent[T]], None]) -> None:
self._listeners[event].remove(callback)
def _emit(self, event: CollectionEvent[T]) -> None:
# TODO: уведомить подписчиков на event_type
pass
def append(self, item: T) -> None:
# TODO: добавить элемент и эмитить "added"
pass
def remove(self, item: T) -> None:
# TODO: удалить элемент и эмитить "removed"
pass
def __setitem__(self, index: int, value: T) -> None:
# TODO: заменить элемент и эмитить "changed" (old_item = предыдущий)
pass
def clear(self) -> None:
# TODO: очистить и эмитить "cleared"
pass
def __iter__(self) -> Iterator[T]:
return iter(self._items)
def __len__(self) -> int:
return len(self._items)
def __getitem__(self, index: int) -> T:
return self._items[index]
class FilteredView(Generic[T]):
"""Живой отфильтрованный вид коллекции."""
def __init__(self, source: ObservableCollection[T],
predicate: Callable[[T], bool]) -> None:
self._source = source
self._predicate = predicate
self._view: list[T] = []
# TODO: инициализировать _view из source, подписаться на события
def _rebuild(self, _: CollectionEvent[T]) -> None:
# TODO: пересобрать _view по предикату из _source
pass
def __iter__(self) -> Iterator[T]:
return iter(self._view)
def __len__(self) -> int:
return len(self._view)
# Ожидаемый результат:
# numbers = ObservableCollection()
# evens = FilteredView(numbers, lambda x: x % 2 == 0)
# numbers.append(1); numbers.append(2); numbers.append(3); numbers.append(4)
# print(list(evens)) # [2, 4]
# numbers.remove(2)
# print(list(evens)) # [4]
# numbers.append(6)
# print(list(evens)) # [4, 6]
# log = []
# numbers.on("added", lambda e: log.append(f"added:{e.item}"))
# numbers.append(8)
# print(log) # ["added:8"]
<?php
class CollectionEvent
{
public function __construct(
public readonly string $eventType, // "added", "removed", "changed", "cleared"
public readonly mixed $item = null,
public readonly int $index = -1,
public readonly mixed $oldItem = null,
) {}
}
class ObservableCollection
{
private array $items = [];
private array $listeners = [
"added" => [], "removed" => [], "changed" => [], "cleared" => [],
];
public function on(string $event, callable $callback): void
{
// TODO: подписать на событие
}
public function off(string $event, callable $callback): void
{
$this->listeners[$event] = array_filter(
$this->listeners[$event] ?? [],
fn($c) => $c !== $callback
);
}
private function emit(CollectionEvent $event): void
{
// TODO: уведомить подписчиков
}
public function append(mixed $item): void
{
// TODO: добавить и эмитить "added"
}
public function remove(mixed $item): void
{
// TODO: удалить и эмитить "removed"
}
public function set(int $index, mixed $value): void
{
// TODO: заменить и эмитить "changed"
}
public function clear(): void
{
// TODO: очистить и эмитить "cleared"
}
public function getItems(): array { return $this->items; }
public function count(): int { return count($this->items); }
}
class FilteredView
{
private array $view = [];
public function __construct(
private readonly ObservableCollection $source,
private readonly \Closure $predicate,
) {
// TODO: инициализировать view, подписаться на события source
}
private function rebuild(CollectionEvent $event): void
{
// TODO: пересобрать view по предикату
}
public function getItems(): array { return $this->view; }
public function count(): int { return count($this->view); }
}
// Ожидаемый результат:
// $numbers = new ObservableCollection();
// $evens = new FilteredView($numbers, fn($x) => $x % 2 === 0);
// $numbers->append(1); $numbers->append(2); $numbers->append(3); $numbers->append(4);
// print_r($evens->getItems()); // [2, 4]
// $numbers->remove(2);
// print_r($evens->getItems()); // [4]
// $numbers->append(6);
// print_r($evens->getItems()); // [4, 6]
// $log = [];
// $numbers->on("added", function($e) use (&$log) { $log[] = "added:{$e->item}"; });
// $numbers->append(8);
// print_r($log); // ["added:8"]