Готовые методы реализаций

Реализация паттерна «Наблюдатель» через сигналы/слоты Qt

Введение

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

Концепция

Сигнал в Qt — это, по сути, точка уведомления, к которой может подписаться произвольное число слотов в разных объектах, без необходимости в субъекте знать о существовании конкретных наблюдателей — ровно то, что описывает паттерн «Наблюдатель». Преимущество перед классической реализацией через виртуальные интерфейсы — автоматическое управление подписками: при удалении объекта-наблюдателя Qt автоматически отключает все его соединения, что устраняет целый класс ошибок (обращение к удалённому наблюдателю), типичных для ручной реализации паттерна.

Пример кода

#pragma once
#include <QObject>

// Субъект наблюдения — не знает о конкретных наблюдателях
class StockPrice : public QObject
{
    Q_OBJECT
public:
    explicit StockPrice(const QString &symbol, QObject *parent = nullptr)
        : QObject(parent), m_symbol(symbol), m_price(0.0) {}

    void updatePrice(double newPrice)
    {
        if (qFuzzyCompare(m_price, newPrice)) return;
        m_price = newPrice;
        emit priceChanged(m_symbol, m_price);
    }

signals:
    void priceChanged(const QString &symbol, double newPrice);

private:
    QString m_symbol;
    double m_price;
};

// Наблюдатель 1: логирование изменений
class PriceLogger : public QObject
{
    Q_OBJECT
public slots:
    void onPriceChanged(const QString &symbol, double newPrice)
    {
        qDebug() << "Лог: цена" << symbol << "изменилась на" << newPrice;
    }
};

// Наблюдатель 2: уведомление при превышении порога
class PriceAlertNotifier : public QObject
{
    Q_OBJECT
public:
    explicit PriceAlertNotifier(double threshold) : m_threshold(threshold) {}

public slots:
    void onPriceChanged(const QString &symbol, double newPrice)
    {
        if (newPrice > m_threshold) {
            qDebug() << "Уведомление: цена" << symbol << "превысила порог" << m_threshold;
        }
    }

private:
    double m_threshold;
};
// Подключение нескольких независимых наблюдателей к одному субъекту
int main()
{
    StockPrice apple("AAPL");
    PriceLogger logger;
    PriceAlertNotifier alert(150.0);

    QObject::connect(&apple, &StockPrice::priceChanged, &logger, &PriceLogger::onPriceChanged);
    QObject::connect(&apple, &StockPrice::priceChanged, &alert, &PriceAlertNotifier::onPriceChanged);

    apple.updatePrice(145.0); // сработает только логгер
    apple.updatePrice(155.0); // сработают оба наблюдателя

    return 0;
}

Пояснения к коду

StockPrice — субъект наблюдения, который испускает сигнал priceChanged при изменении цены, не имея ни малейшего представления о том, кто и сколько наблюдателей на него подписано. PriceLogger и PriceAlertNotifier — два полностью независимых друг от друга наблюдателя с разной логикой реакции на одно и то же событие, подключённые к субъекту отдельными вызовами connect().

Эта структура — прямое соответствие классической диаграмме паттерна «Наблюдатель», но без единой строчки кода для управления списком подписчиков, обработки отписки или защиты от обращения к удалённому наблюдателю — всё это уже встроено в механизм сигналов/слотов Qt.

Подводные камни

  • Иллюзия «наблюдатель не нужно отписывать вручную». Хотя Qt автоматически отключает соединения при удалении QObject-наследника, явная отписка (disconnect()) всё равно может понадобиться для временной приостановки уведомлений без разрушения объекта-наблюдателя — забывание об этой возможности иногда приводит к избыточным затратам на повторное создание/удаление объектов там, где достаточно было бы временно отключить сигнал.
  • Порядок вызова слотов при множественной подписке на один сигнал соответствует порядку подключения (connect()), но явно не гарантирован документацией как незыблемый контракт во всех случаях — код не должен зависеть от конкретного порядка срабатывания независимых наблюдателей.
  • Передача больших объектов по значению через сигнал (а не по константной ссылке) при множестве подписанных наблюдателей вызывает повторное копирование данных для каждого слота при QueuedConnection — для часто испускаемых сигналов с тяжёлыми данными стоит обдумать использование указателей на неизменяемые данные или QSharedPointer.
  • Циклическая подписка наблюдателей друг на друга (наблюдатель A реагирует на изменение и сам испускает сигнал, на который подписан B, который в свою очередь реагирует и снова меняет состояние A) может привести к бесконечному циклу уведомлений — такую логику стоит проектировать с явными проверками условий остановки.