Observer (Наблюдатель)

Паттерн Observer (Наблюдатель) устанавливает отношение «один-ко-многим» между объектами: при изменении состояния издателя все его подписчики получают уведомление и автоматически обновляются. Это помогает ослабить связанность между источником событий и их потребителями.

Когда применять Observer

  • Когда изменение одного объекта требует обновления других, но вы не хотите жёстко связывать их.
  • Когда важно реализовать реактивную модель событий/состояния (UI, доменные события, pub/sub).
  • Когда количество подписчиков должно меняться динамически во время выполнения.

PlantUML-диаграмма

UML диаграмма Observer

// Издатель (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-фреймворками.