Введение
Симметрично предыдущей статье, разберём агрегацию вывода — единый способ показать пользователю информацию или состояние приложения сразу через несколько физических индикаторов (LCD-дисплей, RGB-светодиод, зуммер), не дублируя логику обновления каждого индикатора в разных местах кода приложения.
Концепция
Идея агрегации вывода — единый интерфейс OutputIndicator, через который любой конкретный физический индикатор реализует метод showStatus(status), и центральный StatusBroadcaster, владеющий списком всех зарегистрированных индикаторов и пересылающий им одно и то же изменение статуса приложения — каждый конкретный индикатор сам решает, как именно выразить полученный статус доступными ему средствами (текстом на LCD, цветом RGB-светодиода, звуковым сигналом).
Пример кода
// outputindicator.h — единый интерфейс для любого физического индикатора состояния
#pragma once
#include <QObject>
enum class AppStatus { Idle, Processing, Success, Error };
class OutputIndicator : public QObject
{
Q_OBJECT
public:
explicit OutputIndicator(QObject *parent = nullptr) : QObject(parent) {}
virtual void showStatus(AppStatus status) = 0;
};
// lcdoutputindicator.h, rgboutputindicator.h, buzzeroutputindicator.h —
// три разные реализации одного интерфейса, каждая со своим способом выражения статуса
#pragma once
#include "outputindicator.h"
#include "arduinoconnection.h"
class LcdOutputIndicator : public OutputIndicator
{
public:
explicit LcdOutputIndicator(ArduinoConnection *connection, QObject *parent = nullptr)
: OutputIndicator(parent), m_connection(connection) {}
void showStatus(AppStatus status) override
{
QString text;
switch (status) {
case AppStatus::Idle: text = "Ожидание..."; break;
case AppStatus::Processing: text = "Обработка..."; break;
case AppStatus::Success: text = "Готово!"; break;
case AppStatus::Error: text = "Ошибка!"; break;
}
m_connection->sendCommand("LCD:LINE1:" + text);
}
private:
ArduinoConnection *m_connection;
};
class RgbOutputIndicator : public OutputIndicator
{
public:
explicit RgbOutputIndicator(ArduinoConnection *connection, QObject *parent = nullptr)
: OutputIndicator(parent), m_connection(connection) {}
void showStatus(AppStatus status) override
{
QString rgbCommand;
switch (status) {
case AppStatus::Idle: rgbCommand = "RGB:0,0,255"; break; // синий — спокойное ожидание
case AppStatus::Processing: rgbCommand = "RGB:255,255,0"; break; // желтый — что-то происходит
case AppStatus::Success: rgbCommand = "RGB:0,255,0"; break; // зелёный — успех
case AppStatus::Error: rgbCommand = "RGB:255,0,0"; break; // красный — ошибка
}
m_connection->sendCommand(rgbCommand);
}
private:
ArduinoConnection *m_connection;
};
class BuzzerOutputIndicator : public OutputIndicator
{
public:
explicit BuzzerOutputIndicator(ArduinoConnection *connection, QObject *parent = nullptr)
: OutputIndicator(parent), m_connection(connection) {}
void showStatus(AppStatus status) override
{
// Зуммер реагирует только на значимые, требующие внимания события,
// намеренно оставаясь молчаливым для повседневных, ожидаемых статусов
if (status == AppStatus::Success) {
m_connection->sendCommand("BUZZER:BEEP");
} else if (status == AppStatus::Error) {
m_connection->sendCommand("BUZZER:ALARM");
}
}
private:
ArduinoConnection *m_connection;
};
// statusbroadcaster.h — единая точка изменения статуса, рассылающая его всем индикаторам
#pragma once
#include <QObject>
#include <QList>
#include "outputindicator.h"
class StatusBroadcaster : public QObject
{
Q_OBJECT
public:
explicit StatusBroadcaster(QObject *parent = nullptr) : QObject(parent) {}
void registerIndicator(OutputIndicator *indicator) { m_indicators.append(indicator); }
void setStatus(AppStatus status)
{
for (OutputIndicator *indicator : m_indicators) {
indicator->showStatus(status);
}
}
private:
QList<OutputIndicator *> m_indicators;
};
Пояснения к коду
Каждая из трёх реализаций OutputIndicator интерпретирует один и тот же абстрактный статус (AppStatus::Success, например) совершенно по-своему, в соответствии с собственными физическими возможностями — LCD выводит текст, RGB-светодиод меняет цвет, а зуммер издаёт короткий звук подтверждения, причём зуммер сознательно остаётся «молчаливым» для статусов Idle/Processing, поскольку постоянные звуковые сигналы на каждое незначительное изменение статуса были бы избыточны и раздражающи.
StatusBroadcaster хранит список всех зарегистрированных индикаторов и при вызове setStatus() просто вызывает showStatus() у каждого из них по очереди — остальной код приложения, инициирующий изменение статуса (например, после завершения какой-то операции), вызывает единственный метод broadcaster.setStatus(AppStatus::Success), не заботясь о том, сколько и какие конкретно физические индикаторы в данный момент зарегистрированы и активны — добавление или удаление конкретного индикатора (скажем, временное отключение зуммера) требует изменения только в одном месте — где индикаторы регистрируются, а не во всех местах, где статус устанавливается.
Подводные камни
- Синхронная, последовательная рассылка статуса всем индикаторам внутри
setStatus()означает, что если один из индикаторов выполняет относительно долгую операцию (например, ожидание подтверждения от Arduino), остальные индикаторы в списке получат обновление статуса с задержкой, ожидая своей очереди — для большинства практических случаев с быстрой отправкой команд через последовательный порт эта задержка незаметна, но для потенциально медленных операций стоит рассмотреть асинхронную рассылку, не блокирующую обработку оставшихся индикаторов в списке ожиданием медленного. - Отсутствие обработки ошибок на уровне отдельных индикаторов — если, например, LCD-дисплей физически отключён или вышел из строя, его реализация
showStatus()всё равно будет вызванаStatusBroadcasterбез какой-либо проверки успешности, и без явного механизма обратной связи об ошибке остальная часть приложения может не узнать, что конкретный индикатор не смог фактически отразить переданный статус. - Жёстко закодированное соответствие между абстрактным статусом и конкретным визуальным/звуковым выражением внутри каждого индикатора (например, конкретные цвета RGB для каждого статуса) затрудняет последующую настройку этого соответствия пользователем (например, изменение цветовой схемы по предпочтению) без изменения кода — для проектов, где такая настраиваемость важна, соответствие между статусом и конкретным выражением можно было бы сделать конфигурируемым (через настройки приложения), а не зафиксированным напрямую в коде индикатора.
- Расширение перечисления
AppStatusновыми значениями без соответствующего обновления всех существующих реализаций индикаторов — аналогично подводному камню предыдущей статьи про входные источники, добавление нового статуса требует явной проверки (и, возможно, обновления) каждой существующей реализацииOutputIndicator, чтобы убедиться, что новый статус обрабатывается осознанно (даже если решение — «этот конкретный индикатор не реагирует на этот статус»), а не просачивается вswitchбез обработкиdefault-случая, что для компилируемогоenum classобычно явно подсвечивается предупреждением компилятора о неполном покрытииswitch, но это предупреждение легко проигнорировать.