Анти-паттерны: как не надо работать с `msg`
Введение в анти-паттерны работы с `msg`
В предыдущих уроках мы детально разобрали анатомию объекта `msg`, его ключевые свойства `payload` и `topic`, а также установили фундаментальные правила его использования, такие как «Контракт сообщения» и валидация. Теперь мы перейдем к изучению обратной стороны медали — анти-паттернов.
> 📋 Ключевые понятия:
> Анти-паттерн — это распространенный, но неэффективный или контрпродуктивный способ решения часто встречающейся задачи. В отличие от ошибки, которая просто не работает, анти-паттерн создает иллюзию работающего решения, но в долгосрочной перспективе приводит к серьезным проблемам.
Многие инженеры, особенно в начале работы с Node-RED, придерживаются принципа «если это работает, не трогай». Поток запускается, данные передаются, реле щелкает — кажется, что задача решена. Однако такой подход в корне неверен для профессиональной разработки систем автоматизации. «Работающий» код — далеко не всегда «хороший» код.
Хороший код (или в нашем случае, хороший поток) должен быть не только функциональным, но и:
- Читаемым (Readable): Другой инженер (или вы сами через полгода) должен суметь быстро понять логику потока.
- Поддерживаемым (Maintainable): Внесение изменений или исправление ошибок не должно превращаться в многочасовую эпопею с риском сломать всю систему.
- Масштабируемым (Scalable): Поток должен легко адаптироваться к увеличению количества устройств, усложнению логики или интеграции с новыми системами.
- Надежным (Robust): Поток должен корректно обрабатывать нештатные ситуации: неверный формат данных, сбои связи, ошибки в логике.
Анти-паттерны — это прямой путь к созданию хрупких (brittle) систем. Хрупкая система ломается от малейшего, даже, казалось бы, не связанного изменения. Обновление прошивки датчика, изменение формата ответа от облачного API, добавление нового сценария — все это может привести к полному отказу потока, который был спроектирован с использованием анти-паттернов. Отладка таких систем превращается в кошмар, поскольку логические связи неочевидны, а данные непредсказуемы.
В этом уроке мы детально разберем четыре самых опасных анти-паттерна в работе с объектом `msg`:
Изучение этих анти-паттернов позволит вам не только избегать их в своих проектах, но и научиться распознавать их в чужом коде, что является критически важным навыком для любого системного интегратора.
---
Анти-паттерн №1: `msg` как «кухонная раковина» (The Kitchen Sink)
Это один из самых соблазнительных и в то же время разрушительных анти-паттернов. Он заключается в том, чтобы складывать в один-единственный объект `msg` абсолютно все данные, которые могут понадобиться «когда-нибудь потом» в потоке. Объект `msg` раздувается и начинает напоминать кухонную раковину, куда бросают все подряд.
> ⚠️ Внимание:
> Перегруженный `msg` — прямой путь к созданию «потоков-спагетти». Такой код практически невозможно поддерживать и масштабировать. Каждый узел в потоке должен «знать» о `msg` как можно меньше.
Описание проблемы
Представьте сценарий: мы считываем данные с датчика температуры, затем по этим данным запрашиваем прогноз погоды через API, и на основе обоих значений принимаем решение об управлении климатической установкой.
Новичок может построить поток, где `msg` будет мутировать следующим образом:
{
"payload": {
"value": 23.5,
"source": "UI-01",
"unit": "°C"
},
"topic": "telemetry/living_room/temperature"
}
{
"payload": {
"value": 23.5,
"source": "UI-01",
"unit": "°C"
},
"topic": "telemetry/living_room/temperature",
"weather": {
"forecast": "sunny",
"cloud_cover": 15,
"api_ts": 1678890000000
}
}
{
"payload": {
"value": 23.5,
"source": "UI-01",
"unit": "°C"
},
"topic": "telemetry/living_room/temperature",
"weather": {
"forecast": "sunny",
"cloud_cover": 15,
"api_ts": 1678890000000
},
"command": {
"device": "hvac-01",
"action": "SET_MODE",
"value": "COOLING"
}
}
В итоге мы получили один `msg`-объект, который несет в себе и телеметрию, и внешние данные, и команду на исполнение. Это и есть анти-паттерн «Кухонная раковина».
Последствия
- Нарушение принципа единственной ответственности (Single Responsibility Principle): Сообщение больше не отвечает за что-то одно. Оно стало гибридом телеметрии, данных и команды.
- Сложность отладки: При отладке в узле `Debug` выводится огромный JSON, в котором трудно найти нужную информацию. Если что-то пошло не так, непонятно, какая часть этого «монстра» содержит ошибку.
- Невозможность переиспользования: Допустим, вам понадобился отдельный поток, который просто логирует температуру. Вы не можете просто так взять и направить этот `msg` на выход в базу данных `MySQL`, так как он содержит лишние поля `weather` и `command`, которые засорят вашу таблицу телеметрии. Точно так же, вы не можете переиспользовать узел-обработчик команды, потому что он будет получать на вход лишние данные.
Рекомендация
Сохраняйте `msg` сфокусированным на одной задаче. Вместо того чтобы складывать все в один объект, используйте ветвление потока и узел `Join` для последующего объединения.
Правильный подход:* Ветка 1: `msg` с данными датчика идет в узел, который запрашивает погоду. Перед этим исходный `msg.payload` сохраняется, например, в `msg.originalPayload`.
* Ветка 2: Оригинальный `msg` с данными датчика отправляется напрямую в узел `Join`.
{
"payload": {
"sensor": { "value": 23.5, "source": "UI-01", "unit": "°C" },
"weather": { "forecast": "sunny", "cloud_cover": 15 }
},
"topic": "telemetry/living_room/temperature"
}
Этот подход сохраняет потоки чистыми, а компоненты — переиспользуемыми.
---
Анти-паттерн №2: Злоупотребление `global context` вместо `msg`
Контекст (`flow` и `global`) — мощный инструмент для хранения состояния. Например, в нем можно хранить текущий режим работы системы («День», «Ночь», «Отпуск») или последнее известное значение датчика для сравнения. Однако его никогда не следует использовать для передачи основных, транзакционных данных между узлами.
> ⚠️ Внимание:
> Потоки, активно использующие `global context` для передачи данных, являются крайне хрупкими. Изменение в одном месте может непредсказуемо «сломать» логику в совершенно другой части проекта.
Описание проблемы
Этот анти-паттерн возникает, когда инженер, вместо того чтобы передавать данные через `msg` по «проводам», решает «срезать путь» и положить данные в глобальную переменную в одном месте, а затем забрать их в другом.
Пример плохого потока:- Вкладка 1: "Датчики"
Код в `Function`-узле:
// Считываем значение из msg.payload
let temp = msg.payload.data[0] / 10;
// Сохраняем в глобальный контекст
global.set("living_room_temp", temp);
// Останавливаем поток, т.к. "данные доставлены"
return null;
- Вкладка 2: "Логика климата" (никак не связана проводами с первой)
Код в `Function`-узле:
// Берем значение не из msg, а из глобального контекста
let current_temp = global.get("living_room_temp");
if (current_temp > 25) {
msg.payload = "START_COOLING";
return msg;
}
return null;
На первый взгляд, это работает. Но мы создали невидимую связь между двумя абсолютно несвязанными потоками.
Последствия
- Скрытые зависимости: Глядя на холст Node-RED, невозможно понять, откуда узел `Function "Проверить температуру"` берет данные. Это делает поток совершенно нечитаемым и непредсказуемым.
- Состояние гонки (Race Conditions): Что если у вас два датчика температуры, и оба пишут в ту же глобальную переменную `living_room_temp`? Или один поток читает значение, в то время как другой его перезаписывает? Результат работы системы становится недетерминированным и зависит от случайного тайминга выполнения операций.
- Сложности при отладке: Если логика климата работает неправильно, куда смотреть? В поток датчиков? В поток логики? Непонятно, в какой момент времени и каким узлом было установлено некорректное значение в `global.get()`.
- Невозможность изолированного тестирования: Вы не можете протестировать поток "Логика климата" отдельно, потому что он зависит от глобального состояния, которое должен установить другой поток.
Рекомендация
Объект `msg` должен быть самодостаточным. Он должен нести в себе все данные, необходимые для обработки в следующем узле.
- Используйте `msg` для транзита данных: Если узлу `Б` нужны данные от узла `А`, соедините их проводом и передайте данные в `msg`.
- Используйте контекст для хранения состояния:
* `global context` — для хранения глобальных, редко изменяемых настроек или констант (например, API-ключи, координаты объекта), но не для оперативных данных.
Правильное решение для примера выше — соединить выход узла `Modbus Read` (после форматирования) со входом узла `Function "Проверить температуру"`. Просто и предсказуемо.
---
Анти-паттерн №3: Неявные «контракты» и игнорирование валидации
Этот анти-паттерн является прямым нарушением одного из ключевых принципов надежного проектирования, который мы уже обсуждали. Он возникает, когда разработчик подразумевает, что `msg` будет иметь определенную структуру, но никак это не проверяет.
> 🔗 Связанный материал:
> Этот анти-паттерн является прямым следствием несоблюдения практик, описанных в уроке `COURSE-06-M02-L06` «Паттерн: Валидация структуры `msg` на входе». Всегда валидируйте входящие сообщения в начале ключевых логических блоков.
Проблема и демонстрация сбоя
Предположим, у нас есть `Function`-узел, который должен вычислять среднюю температуру из массива значений, пришедшего в `msg.payload.values`.
Код узла с анти-паттерном:// Ожидается, что msg.payload.values - это массив чисел
let sum = 0;
for (let i = 0; i < msg.payload.values.length; i++) {
sum += msg.payload.values[i];
}
msg.payload = { "average": sum / msg.payload.values.length };
return msg;
Этот код будет прекрасно работать, пока на вход приходит ожидаемая структура:
{
"payload": {
"values": [22.1, 22.5, 22.3]
}
}
Но что произойдет, если из-за ошибки в предыдущем узле или из-за сбоя в MQTT-брокере на вход придет что-то другое?
- Сценарий 1: `msg.payload` — это просто число `22.1`
- Сценарий 2: `msg` пришел от кнопки и `msg.payload` — это строка `"ON"`
- Сценарий 3: На вход пришел пустой объект `msg.payload = {}`
Практический пример: внедрение валидации
«Противоядием» от этого анти-паттерна является защитное программирование (defensive programming) и явная валидация контракта на входе.
Код узла, исправленный по всем правилам:// 1. ВАЛИДАЦИЯ КОНТРАКТА
// Проверяем, что payload - это объект, и у него есть свойство 'values', которое является массивом
if (
typeof msg.payload !== 'object' || msg.payload === null ||
!Array.isArray(msg.payload.values)
) {
// Если контракт нарушен, генерируем информативную ошибку
node.error("Invalid message structure. Expected msg.payload.values to be an array.", msg);
// Обновляем статус узла для визуальной диагностики
node.status({ fill: "red", shape: "dot", text: "Invalid input" });
// Останавливаем дальнейшее выполнение потока для этой ветки.
return null;
}
// Дополнительная проверка: массив не должен быть пустым, чтобы избежать деления на ноль
if (msg.payload.values.length === 0) {
node.warn("Input array 'values' is empty. Cannot calculate average.", msg);
node.status({ fill: "yellow", shape: "ring", text: "Empty input" });
return null;
}
// 2. ОСНОВНАЯ ЛОГИКА (выполняется, только если валидация пройдена)
let sum = 0;
for (let i = 0; i < msg.payload.values.length; i++) {
// Дополнительная проверка на число внутри цикла
if (typeof msg.payload.values[i] === 'number') {
sum += msg.payload.values[i];
}
}
// 3. ФОРМИРОВАНИЕ ВЫХОДНОГО СООБЩЕНИЯ
msg.payload = { "average": sum / msg.payload.values.length };
node.status({ fill: "green", shape: "dot", text: "Avg: " + msg.payload.average.toFixed(2) });
return msg;
Такой узел становится надежным. Он не «упадет» от неожиданных данных, а вместо этого сообщит об ошибке понятным образом и безопасно остановит обработку невалидного сообщения.
---
Анти-паттерн №4: Модификация `msg` с потерей исходных данных
Многие стандартные узлы в Node-RED (особенно те, что выполняют операции ввода-вывода) по умолчанию перезаписывают `msg.payload` результатом своей работы. Если не принять меры предосторожности, это может привести к безвозвратной потере ценной информации, которая была в сообщении до этого.
> 💡 Подсказка:
> Узел `Change` — ваш лучший инструмент для управления структурой `msg`. Используйте его, чтобы перемещать (`move`) или копировать (`set`) `msg.payload` в другое свойство перед вызовом узлов, которые его перезаписывают.
Описание проблемы
Рассмотрим типичный сценарий обогащения данных.
`[GPIO In]` -> `[Change: add metadata]` -> `[Function: prepare text]` -> `[Pushover]` -> `[Function: prepare SQL]` -> `[MySQL]`
Проблема возникает на шаге 3. Узел `Pushover` (как и многие другие) после успешной отправки заменяет весь объект `msg` на результат ответа от API Pushover.
- До узла `Pushover` `msg` выглядел так:
{
"payload": "Внимание! Протечка на кухне!",
"topic": "pushover",
"zone_id": "kitchen",
"severity": "critical"
}
- После узла `Pushover` `msg` будет выглядеть так:
{
"payload": { "status": 1, "request": "a1b2c3d4-..." },
"topic": "pushover",
"_msgid": "..."
// Свойства zone_id и severity БЕЗВОЗВРАТНО ПОТЕРЯНЫ!
}
В итоге, когда сообщение дойдет до узла `[Function: prepare SQL]`, он не найдет свойств `zone_id` и `severity` и не сможет сформировать корректный SQL-запрос.
Решение
Решение простое и элегантное: перед вызовом «опасного» узла необходимо сохранить нужные части `msg` в другом свойстве.
Правильный поток:`[GPIO In]` -> `[Change: add metadata]` -> `[Change: save context]` -> `[Function: prepare text]` -> `[Pushover]` -> `[Function: prepare SQL]` -> `[MySQL]`
Настройка узла `[Change: save context]`:
- Правило 1: `move` `msg.payload` `to` `msg.pushover_payload`
- Правило 2: `move` `msg.zone_id` `to` `msg.context_zone_id`
- Правило 3: `move` `msg.severity` `to` `msg.context_severity`
Теперь, после узла `Pushover`, наш `msg` будет выглядеть так:
{
"payload": { "status": 1, "request": "a1b2c3d4-..." }, // Результат от Pushover
"topic": "pushover",
"context_zone_id": "kitchen", // Наши данные сохранены!
"context_severity": "critical",// Наши данные сохранены!
"_msgid": "..."
}
Узел `[Function: prepare SQL]` теперь сможет взять нужные данные из свойств `msg.context_zone_id` и `msg.context_severity` и успешно выполнить свою задачу.
---
Итоги и свод лучших практик
В этом уроке мы рассмотрели четыре ключевых анти-паттерна, которые превращают элегантные и мощные потоки Node-RED в хрупкие, запутанные и неподдерживаемые конструкции. Научившись распознавать и избегать их, вы делаете огромный шаг на пути от простого «сборщика потоков» к профессиональному архитектору систем автоматизации.
Краткий повтор рассмотренных анти-паттернов:«Золотые правила» работы с `msg`
Можно сформулировать четыре основных принципа, которые являются противоядием от всех рассмотренных проблем:
- Держи его чистым: Объект `msg` должен нести данные, относящиеся к одной, четко определенной задаче.
- Держи его самодостаточным: `msg` должен содержать всю информацию, необходимую следующему узлу для работы. Никаких скрытых зависимостей от контекста.
- Держи его предсказуемым: Структура `msg` должна следовать заранее определенному "контракту". Поток данных должен быть явно виден на холсте.
- Держи его валидированным: Никогда не доверяйте входящим данным. Всегда проверяйте их на соответствие контракту.
Чек-лист для самопроверки потока
Перед тем как считать работу над потоком завершенной, пройдитесь по этому короткому чек-листу:
- [ ] [Чистота] Мой `msg` не содержит несвязанных данных из разных доменов (например, телеметрии и команд одновременно)?
- [ ] [Предсказуемость] Могу ли я, глядя только на холст, проследить весь путь данных от источника до потребителя? Нет ли у меня «невидимых проводов» через `global context`?
- [ ] [Надежность] Каждый узел `Function`, принимающий данные извне (MQTT, HTTP) или от других сложных потоков, имеет блок валидации на входе?
- [ ] [Сохранность] Перед каждым узлом, который может перезаписать `msg` (`HTTP Request`, `MySQL` и т.д.), я сохраняю важные данные с помощью узла `Change`?
- [ ] [Читаемость] Если другой инженер откроет этот поток, сможет ли он понять его логику за 5 минут?
Что дальше
Мы завершили ключевой модуль, посвященный сердцу Node-RED — объекту `msg`. Вы получили теоретические знания и практические навыки для создания надежных и профессиональных коммуникационных потоков. В следующем модуле мы перейдем от этой основы к реальному миру: начнем подключать к нашему контроллеру HI настоящие промышленные и бытовые устройства, используя протоколы Modbus, MQTT и другие, и применять все изученные паттерны и анти-паттерны на практике.