Введение
Паттерн «Наблюдатель» (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) может привести к бесконечному циклу уведомлений — такую логику стоит проектировать с явными проверками условий остановки.