Введение
Qt сигналы/слоты — это готовая реализация Observer, не требующая написания вручную — в этой статье разберём паттерн Observer систематически: его классическую, «ручную» реализацию на чистом C++, и явное сопоставление каждого элемента этой классической структуры с соответствующим механизмом Qt.
Концепция
Observer определяет зависимость «один-ко-многим» между объектами — когда состояние одного объекта (Subject/Observable) изменяется, все зависящие от него объекты (Observers), зарегистрированные для получения уведомлений, автоматически извещаются об этом изменении, без необходимости Subject знать конкретные, точные типы своих наблюдателей (только то, что они реализуют общий, ожидаемый интерфейс получения уведомлений). Классическая реализация требует ручного написания интерфейса наблюдателя, списка зарегистрированных наблюдателей внутри Subject, и методов подписки/отписки — система сигналов и слотов Qt (Q_OBJECT, signals, connect, статьи основного цикла и QML-цикла) реализует ровно ту же концептуальную структуру, но через встроенный, языковой (точнее, через систему метаобъектов Qt) механизм, устраняющий необходимость в ручном написании этой повторяющейся инфраструктуры подписки/уведомления для каждого нового случая использования.
Пример кода
#include <iostream>
#include <vector>
#include <functional>
// Классическая, "ручная" реализация Observer — без Qt
class IObserver
{
public:
virtual void onTemperatureChanged(double newValue) = 0;
virtual ~IObserver() = default;
};
class TemperatureSensorClassic // Subject — РУЧНОЕ управление списком наблюдателей
{
public:
void addObserver(IObserver *observer) { m_observers.push_back(observer); }
void removeObserver(IObserver *observer)
{
m_observers.erase(std::remove(m_observers.begin(), m_observers.end(), observer), m_observers.end());
}
void setTemperature(double value)
{
m_temperature = value;
for (auto *observer : m_observers) { // РУЧНОЕ уведомление КАЖДОГО зарегистрированного наблюдателя
observer->onTemperatureChanged(value);
}
}
private:
double m_temperature = 0.0;
std::vector<IObserver *> m_observers; // "сырые" указатели — требуют АККУРАТНОГО управления временем жизни!
};
class DisplayObserver : public IObserver
{
public:
void onTemperatureChanged(double newValue) override
{
std::cout << "Дисплей: новая температура " << newValue << std::endl;
}
};
// ТА ЖЕ задача через Qt — сигналы/слоты ЗАМЕНЯЮТ всю ручную инфраструктуру выше
#include <QObject>
class TemperatureSensorQt : public QObject // Subject — БЕЗ ручного списка наблюдателей, БЕЗ IObserver
{
Q_OBJECT
public:
void setTemperature(double value)
{
m_temperature = value;
emit temperatureChanged(value); // ОДНА строка вместо ручного цикла по списку наблюдателей
}
signals:
void temperatureChanged(double newValue); // сигнал = "точка подписки", аналог IObserver::onTemperatureChanged
private:
double m_temperature = 0.0;
};
class DisplayObserverQt : public QObject
{
Q_OBJECT
public slots:
void onTemperatureChanged(double newValue) // слот = реализация конкретного наблюдателя
{
std::cout << "Дисплей: новая температура " << newValue << std::endl;
}
};
int main()
{
TemperatureSensorQt sensor;
DisplayObserverQt display;
QObject::connect(&sensor, &TemperatureSensorQt::temperatureChanged,
&display, &DisplayObserverQt::onTemperatureChanged); // ЗАМЕНЯЕТ addObserver()
sensor.setTemperature(23.5); // вызывает emit -> Qt АВТОМАТИЧЕСКИ доставляет уведомление подписчикам
return 0;
}
Пояснения к коду
TemperatureSensorClassic показывает классическую реализацию Observer «от руки» — m_observers (список наблюдателей), addObserver/removeObserver (управление подпиской), и явный цикл уведомления внутри setTemperature — каждый из этих элементов нужно написать самостоятельно, и при использовании «сырых» указателей (как в примере) разработчик несёт полную ответственность за корректное управление временем жизни наблюдателей (своевременный вызов removeObserver перед уничтожением наблюдателя, иначе Subject будет хранить «висячий» указатель). TemperatureSensorQt/DisplayObserverQt показывают, что та же самая концептуальная структура полностью покрывается встроенным механизмом Qt — connect() заменяет addObserver(), emit temperatureChanged(value) заменяет ручной цикл уведомления, и, что особенно важно, Qt автоматически разрывает соединение при уничтожении любого из связанных объектов (через систему родитель-потомок или явный QObject::destroyed), что устраняет именно тот риск «висячих» указателей, требующий ручного, аккуратного управления в классической реализации.
Подводные камни
- Написание классического, «ручного» Observer в Qt-проекте вместо использования встроенных сигналов/слотов — поскольку Qt предоставляет полностью эквивалентную, более безопасную (автоматическое управление временем жизни соединений) и идиоматичную реализацию этого паттерна, написание классической версии с нуля в Qt-коде обычно избыточно и менее интегрировано с остальной архитектурой Qt-приложения, и предпочтение встроенного механизма Qt для этой задачи — общепринятая, рекомендуемая практика.
- Забытый вызов
removeObserver()перед уничтожением объекта-наблюдателя в классической реализации, оставляющий Subject с «висячим» указателем на уже уничтоженный объект — это классический, серьёзный риск ручной реализации Observer с «сырыми» указателями, который Qt автоматически устраняет через свой механизм отслеживания времени жизни подключенных объектов, что является одним из главных практических преимуществ использования готового механизма Qt вместо ручной реализации. - Избыточное количество наблюдателей одного Subject, существенно замедляющее операцию уведомления при очень частом изменении наблюдаемого значения — как для классической реализации, так и для Qt-сигналов, уведомление большого количества подписчиков при каждом изменении имеет реальную, измеримую стоимость, и для случаев с очень частыми изменениями и большим количеством подписчиков (статья 494 QML-цикла про общий принцип производительности) стоит явно учитывать эту стоимость при проектировании архитектуры с интенсивным использованием паттерна Observer.
- Изменение списка наблюдателей (добавление/удаление) непосредственно ВО ВРЕМЯ итерации по этому списку для уведомления (например, наблюдатель в своём обработчике уведомления решает отписаться от того же Subject) — это классическая проблема изменения коллекции во время её обхода , способная привести к неопределённому поведению, и качественная реализация Observer (как ручная, так и встроенный механизм Qt) должна явно учитывать и корректно обрабатывать эту ситуацию, обычно через копирование списка перед итерацией или отложенное применение изменений списка наблюдателей.