Паттерн Observer (Наблюдатель) устанавливает отношение «один-ко-многим» между объектами: при изменении состояния издателя все его подписчики получают уведомление и автоматически обновляются. Это помогает ослабить связанность между источником событий и их потребителями.
Когда применять Observer
- Когда изменение одного объекта требует обновления других, но вы не хотите жёстко связывать их.
- Когда важно реализовать реактивную модель событий/состояния (UI, доменные события, pub/sub).
- Когда количество подписчиков должно меняться динамически во время выполнения.
PlantUML-диаграмма
// Издатель (Subject)
public interface IStock
{
void Attach(IObserver observer);
void Detach(IObserver observer);
void Notify();
string Symbol { get; }
decimal Price { get; }
}
public class Stock : IStock
{
private readonly List<IObserver> _observers = new();
public string Symbol { get; }
public decimal Price { get; private set; }
public Stock(string symbol, decimal initialPrice)
{
Symbol = symbol;
Price = initialPrice;
}
public void SetPrice(decimal newPrice)
{
if (newPrice == Price) return;
Price = newPrice;
Notify();
}
public void Attach(IObserver observer) => _observers.Add(observer);
public void Detach(IObserver observer) => _observers.Remove(observer);
public void Notify()
{
foreach (var o in _observers)
o.Update(this);
}
}
// Подписчик (Observer)
public interface IObserver
{
void Update(IStock stock);
}
// Конкретные наблюдатели
public class ConsoleTicker : IObserver
{
public void Update(IStock stock)
{
Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] {stock.Symbol}: {stock.Price:C}");
}
}
public class ThresholdAlert : IObserver
{
private readonly decimal _limit;
public ThresholdAlert(decimal limit) => _limit = limit;
public void Update(IStock stock)
{
if (stock.Price >= _limit)
Console.WriteLine($"⚠️ {stock.Symbol} достиг {_limit:C}. Текущее: {stock.Price:C}");
}
}
// Клиентский код
class Program
{
static void Main()
{
var stock = new Stock("ACME", 100m);
var ticker = new ConsoleTicker();
var alert150 = new ThresholdAlert(150m);
stock.Attach(ticker);
stock.Attach(alert150);
stock.SetPrice(120m); // уведомит всех
stock.SetPrice(151m); // уведомит и сработает ThresholdAlert
stock.Detach(ticker);
stock.SetPrice(160m); // уведомит только оставшихся наблюдателей
}
}
type Stock interface {
Symbol() string
Price() float64
}
type Observer interface {
Update(stock Stock)
}
type stock struct {
symbol string
price float64
observers []Observer
}
func NewStock(symbol string, initialPrice float64) *stock {
return &stock{symbol: symbol, price: initialPrice}
}
func (s *stock) Symbol() string { return s.symbol }
func (s *stock) Price() float64 { return s.price }
func (s *stock) Attach(o Observer) { s.observers = append(s.observers, o) }
func (s *stock) Detach(target Observer) {
for i, o := range s.observers {
if o == target {
s.observers = append(s.observers[:i], s.observers[i+1:]...)
return
}
}
}
func (s *stock) SetPrice(newPrice float64) {
if newPrice == s.price {
return
}
s.price = newPrice
s.notify()
}
func (s *stock) notify() {
for _, o := range s.observers {
o.Update(s)
}
}
type ConsoleTicker struct{}
func (ConsoleTicker) Update(s Stock) {
fmt.Printf("[%s] %s: %.2f\n", time.Now().Format("15:04:05"), s.Symbol(), s.Price())
}
type ThresholdAlert struct {
limit float64
}
func (a ThresholdAlert) Update(s Stock) {
if s.Price() >= a.limit {
fmt.Printf("%s достиг %.2f. Текущее: %.2f\n", s.Symbol(), a.limit, s.Price())
}
}
func main() {
acme := NewStock("ACME", 100)
ticker := ConsoleTicker{}
alert150 := ThresholdAlert{limit: 150}
acme.Attach(ticker)
acme.Attach(alert150)
acme.SetPrice(120)
acme.SetPrice(151)
acme.Detach(ticker)
acme.SetPrice(160)
}
from datetime import datetime
from typing import Protocol
class Stock(Protocol):
@property
def symbol(self) -> str: ...
@property
def price(self) -> float: ...
class Observer(Protocol):
def update(self, stock: Stock) -> None: ...
class SimpleStock:
def __init__(self, symbol: str, initial_price: float) -> None:
self._symbol = symbol
self._price = initial_price
self._observers: list[Observer] = []
@property
def symbol(self) -> str:
return self._symbol
@property
def price(self) -> float:
return self._price
def attach(self, observer: Observer) -> None:
self._observers.append(observer)
def detach(self, observer: Observer) -> None:
self._observers.remove(observer)
def set_price(self, new_price: float) -> None:
if new_price == self._price:
return
self._price = new_price
self._notify()
def _notify(self) -> None:
for observer in self._observers:
observer.update(self)
class ConsoleTicker:
def update(self, stock: Stock) -> None:
print(f"[{datetime.now():%H:%M:%S}] {stock.symbol}: {stock.price:.2f}")
class ThresholdAlert:
def __init__(self, limit: float) -> None:
self._limit = limit
def update(self, stock: Stock) -> None:
if stock.price >= self._limit:
print(f"{stock.symbol} достиг {self._limit:.2f}. Текущее: {stock.price:.2f}")
acme = SimpleStock("ACME", 100)
ticker = ConsoleTicker()
alert150 = ThresholdAlert(150)
acme.attach(ticker)
acme.attach(alert150)
acme.set_price(120)
acme.set_price(151)
acme.detach(ticker)
acme.set_price(160)
interface Stock {
readonly symbol: string;
readonly price: number;
}
interface Observer {
update(stock: Stock): void;
}
class SimpleStock implements Stock {
private readonly observers: Observer[] = [];
private currentPrice: number;
constructor(readonly symbol: string, initialPrice: number) {
this.currentPrice = initialPrice;
}
get price(): number {
return this.currentPrice;
}
attach(observer: Observer): void {
this.observers.push(observer);
}
detach(observer: Observer): void {
const i = this.observers.indexOf(observer);
if (i >= 0) this.observers.splice(i, 1);
}
setPrice(newPrice: number): void {
if (newPrice === this.currentPrice) return;
this.currentPrice = newPrice;
this.notify();
}
private notify(): void {
for (const observer of this.observers) observer.update(this);
}
}
class ConsoleTicker implements Observer {
update(stock: Stock): void {
console.log(`[${new Date().toLocaleTimeString()}] ${stock.symbol}: ${stock.price.toFixed(2)}`);
}
}
class ThresholdAlert implements Observer {
constructor(private readonly limit: number) {}
update(stock: Stock): void {
if (stock.price >= this.limit) {
console.log(`${stock.symbol} достиг ${this.limit}. Текущее: ${stock.price}`);
}
}
}
const acme = new SimpleStock("ACME", 100);
const ticker = new ConsoleTicker();
const alert150 = new ThresholdAlert(150);
acme.attach(ticker);
acme.attach(alert150);
acme.setPrice(120);
acme.setPrice(151);
acme.detach(ticker);
acme.setPrice(160);
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.List;
interface Stock {
String getSymbol();
double getPrice();
}
interface Observer {
void update(Stock stock);
}
class SimpleStock implements Stock {
private final List<Observer> observers = new ArrayList<>();
private final String symbol;
private double price;
SimpleStock(String symbol, double initialPrice) {
this.symbol = symbol;
this.price = initialPrice;
}
public String getSymbol() {
return symbol;
}
public double getPrice() {
return price;
}
void attach(Observer observer) {
observers.add(observer);
}
void detach(Observer observer) {
observers.remove(observer);
}
void setPrice(double newPrice) {
if (newPrice == price) return;
price = newPrice;
notifyObservers();
}
private void notifyObservers() {
for (Observer observer : observers) {
observer.update(this);
}
}
}
class ConsoleTicker implements Observer {
public void update(Stock stock) {
System.out.printf("[%tT] %s: %.2f%n", LocalTime.now(), stock.getSymbol(), stock.getPrice());
}
}
class ThresholdAlert implements Observer {
private final double limit;
ThresholdAlert(double limit) {
this.limit = limit;
}
public void update(Stock stock) {
if (stock.getPrice() >= limit) {
System.out.printf("%s достиг %.2f. Текущее: %.2f%n",
stock.getSymbol(), limit, stock.getPrice());
}
}
}
public class Program {
public static void main(String[] args) {
SimpleStock acme = new SimpleStock("ACME", 100);
ConsoleTicker ticker = new ConsoleTicker();
ThresholdAlert alert150 = new ThresholdAlert(150);
acme.attach(ticker);
acme.attach(alert150);
acme.setPrice(120);
acme.setPrice(151);
acme.detach(ticker);
acme.setPrice(160);
}
}
import java.time.LocalTime
interface Stock {
val symbol: String
val price: Double
}
// Наблюдатель — это одна функция, поэтому интерфейс не нужен.
typealias Observer = (Stock) -> Unit
class SimpleStock(override val symbol: String, initialPrice: Double) : Stock {
private val observers = mutableListOf<Observer>()
override var price: Double = initialPrice
private set
fun attach(observer: Observer) {
observers += observer
}
fun detach(observer: Observer) {
observers -= observer
}
fun setPrice(newPrice: Double) {
if (newPrice == price) return
price = newPrice
observers.forEach { it(this) }
}
}
val consoleTicker: Observer = { stock ->
println("[${LocalTime.now().withNano(0)}] ${stock.symbol}: ${stock.price}")
}
fun thresholdAlert(limit: Double): Observer = { stock ->
if (stock.price >= limit) {
println("${stock.symbol} достиг $limit. Текущее: ${stock.price}")
}
}
fun main() {
val acme = SimpleStock("ACME", 100.0)
val alert150 = thresholdAlert(150.0)
acme.attach(consoleTicker)
acme.attach(alert150)
acme.setPrice(120.0)
acme.setPrice(151.0)
acme.detach(consoleTicker)
acme.setPrice(160.0)
}
Плюсы и минусы
| Плюсы | Минусы |
|---|---|
| Ослабляет связанность между источником и потребителями событий | Возможны «лавины» уведомлений и скрытые цепочки вызовов |
| Подписчики подключаются/отключаются динамически | Сложнее отладка и трассировка при большом числе подписчиков |
| Поддерживает реактивные и событийно-ориентированные архитектуры | Требуется следить за утечками ссылок (неотписавшиеся наблюдатели) |
Советы по использованию
- Отделяйте данные события от транспорта уведомлений (DTO для payload).
- Добавляйте буферизацию/дебаунс, если события приходят часто.
- Рассмотрите слабые ссылки или явную отписку, чтобы избежать утечек.
Заключение
Observer — основной паттерн для построения событийных систем. Он позволяет гибко реагировать на изменения состояния, не связывая компоненты напрямую, и хорошо сочетается с pub/sub-шинами, реактивными потоками и UI-фреймворками.