ГлавнаяАкадемияNode-RED: установка, flows, msg/JSON, отладка → Анти-паттерны: как не надо работать с `msg`

Анти-паттерны: как не надо работать с `msg`

Урок 6 · Node-RED: установка, flows, msg/JSON, отладка · 30 мин · theory

Введение в анти-паттерны работы с `msg`

В предыдущих уроках мы детально разобрали анатомию объекта `msg`, его ключевые свойства `payload` и `topic`, а также установили фундаментальные правила его использования, такие как «Контракт сообщения» и валидация. Теперь мы перейдем к изучению обратной стороны медали — анти-паттернов.

> 📋 Ключевые понятия:

> Анти-паттерн — это распространенный, но неэффективный или контрпродуктивный способ решения часто встречающейся задачи. В отличие от ошибки, которая просто не работает, анти-паттерн создает иллюзию работающего решения, но в долгосрочной перспективе приводит к серьезным проблемам.

Многие инженеры, особенно в начале работы с Node-RED, придерживаются принципа «если это работает, не трогай». Поток запускается, данные передаются, реле щелкает — кажется, что задача решена. Однако такой подход в корне неверен для профессиональной разработки систем автоматизации. «Работающий» код — далеко не всегда «хороший» код.

Хороший код (или в нашем случае, хороший поток) должен быть не только функциональным, но и:

Анти-паттерны — это прямой путь к созданию хрупких (brittle) систем. Хрупкая система ломается от малейшего, даже, казалось бы, не связанного изменения. Обновление прошивки датчика, изменение формата ответа от облачного API, добавление нового сценария — все это может привести к полному отказу потока, который был спроектирован с использованием анти-паттернов. Отладка таких систем превращается в кошмар, поскольку логические связи неочевидны, а данные непредсказуемы.

В этом уроке мы детально разберем четыре самых опасных анти-паттерна в работе с объектом `msg`:

  • `msg` как «кухонная раковина»: Перегрузка одного сообщения несвязанными данными.
  • Злоупотребление `global context`: Использование глобальных переменных для транзита данных.
  • Неявные «контракты»: Игнорирование валидации структуры сообщения.
  • Модификация `msg` с потерей данных: Перезапись полезной информации на промежуточных шагах.
  • Изучение этих анти-паттернов позволит вам не только избегать их в своих проектах, но и научиться распознавать их в чужом коде, что является критически важным навыком для любого системного интегратора.

    ---

    Анти-паттерн №1: `msg` как «кухонная раковина» (The Kitchen Sink)

    Это один из самых соблазнительных и в то же время разрушительных анти-паттернов. Он заключается в том, чтобы складывать в один-единственный объект `msg` абсолютно все данные, которые могут понадобиться «когда-нибудь потом» в потоке. Объект `msg` раздувается и начинает напоминать кухонную раковину, куда бросают все подряд.

    > ⚠️ Внимание:

    > Перегруженный `msg` — прямой путь к созданию «потоков-спагетти». Такой код практически невозможно поддерживать и масштабировать. Каждый узел в потоке должен «знать» о `msg` как можно меньше.

    Описание проблемы

    Представьте сценарий: мы считываем данные с датчика температуры, затем по этим данным запрашиваем прогноз погоды через API, и на основе обоих значений принимаем решение об управлении климатической установкой.

    Новичок может построить поток, где `msg` будет мутировать следующим образом:

  • Начало потока: `msg` содержит данные с датчика.
  •     {

    "payload": {

    "value": 23.5,

    "source": "UI-01",

    "unit": "°C"

    },

    "topic": "telemetry/living_room/temperature"

    }

  • После узла `HTTP Request`: В `msg` добавляются данные от погодного API.
  •     {

    "payload": {

    "value": 23.5,

    "source": "UI-01",

    "unit": "°C"

    },

    "topic": "telemetry/living_room/temperature",

    "weather": {

    "forecast": "sunny",

    "cloud_cover": 15,

    "api_ts": 1678890000000

    }

    }

  • После узла `Function` с логикой: В тот же `msg` добавляется команда для устройства.
  •     {

    "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`-объект, который несет в себе и телеметрию, и внешние данные, и команду на исполнение. Это и есть анти-паттерн «Кухонная раковина».

    Последствия

    Рекомендация

    Сохраняйте `msg` сфокусированным на одной задаче. Вместо того чтобы складывать все в один объект, используйте ветвление потока и узел `Join` для последующего объединения.

    Правильный подход:
  • Поток начинается с получения данных от датчика.
  • Далее поток разветвляется.
  • * Ветка 1: `msg` с данными датчика идет в узел, который запрашивает погоду. Перед этим исходный `msg.payload` сохраняется, например, в `msg.originalPayload`.

    * Ветка 2: Оригинальный `msg` с данными датчика отправляется напрямую в узел `Join`.

  • После того как ветка 1 получила ответ от API, она также приходит в узел `Join`.
  • Узел `Join` настроен на объединение двух сообщений в одно (например, по `msg.topic`). На его выходе мы получаем структурированный объект, где четко разделены исходные данные и обогащенные.
  •     {

    "payload": {

    "sensor": { "value": 23.5, "source": "UI-01", "unit": "°C" },

    "weather": { "forecast": "sunny", "cloud_cover": 15 }

    },

    "topic": "telemetry/living_room/temperature"

    }

  • И только после этого объединенное сообщение идет в узел `Function`, который генерирует новый, чистый `msg` с командой, не содержащий ничего лишнего.
  • Этот подход сохраняет потоки чистыми, а компоненты — переиспользуемыми.

    ---

    Анти-паттерн №2: Злоупотребление `global context` вместо `msg`

    Контекст (`flow` и `global`) — мощный инструмент для хранения состояния. Например, в нем можно хранить текущий режим работы системы («День», «Ночь», «Отпуск») или последнее известное значение датчика для сравнения. Однако его никогда не следует использовать для передачи основных, транзакционных данных между узлами.

    > ⚠️ Внимание:

    > Потоки, активно использующие `global context` для передачи данных, являются крайне хрупкими. Изменение в одном месте может непредсказуемо «сломать» логику в совершенно другой части проекта.

    Описание проблемы

    Этот анти-паттерн возникает, когда инженер, вместо того чтобы передавать данные через `msg` по «проводам», решает «срезать путь» и положить данные в глобальную переменную в одном месте, а затем забрать их в другом.

    Пример плохого потока: `[Modbus Read]` -> `[Function "Сохранить в global"]`

    Код в `Function`-узле:

        // Считываем значение из msg.payload

    let temp = msg.payload.data[0] / 10;

    // Сохраняем в глобальный контекст

    global.set("living_room_temp", temp);

    // Останавливаем поток, т.к. "данные доставлены"

    return null;

    `[Inject "Каждые 5 мин"]` -> `[Function "Проверить температуру"]` -> `[Управление HVAC]`

    Код в `Function`-узле:

        // Берем значение не из msg, а из глобального контекста

    let current_temp = global.get("living_room_temp");

    if (current_temp > 25) {

    msg.payload = "START_COOLING";

    return msg;

    }

    return null;

    На первый взгляд, это работает. Но мы создали невидимую связь между двумя абсолютно несвязанными потоками.

    Последствия

    Рекомендация

    Объект `msg` должен быть самодостаточным. Он должен нести в себе все данные, необходимые для обработки в следующем узле.

    * `flow context` — для хранения состояния, общего для всех узлов на одной вкладке (например, статус конечного автомата FSM).

    * `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-брокере на вход придет что-то другое?

    Попытка выполнить `msg.payload.values` приведет к ошибке `TypeError: Cannot read property 'values' of undefined`, так как у числа нет свойства `values`. Поток остановится. Та же самая ошибка, только теперь `msg.payload` — это строка. `msg.payload.values` будет `undefined`. Попытка обратиться к свойству `length` от `undefined` вызовет `TypeError: Cannot read property 'length' of undefined`. Поток остановится.

    Практический пример: внедрение валидации

    «Противоядием» от этого анти-паттерна является защитное программирование (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` в другое свойство перед вызовом узлов, которые его перезаписывают.

    Описание проблемы

    Рассмотрим типичный сценарий обогащения данных.

  • Универсальный вход `UI-05` контроллера подключен к датчику протечки. При срабатывании он генерирует `msg`.
  • В `msg` мы добавляем метаданные: ID зоны (`zone_id: "kitchen"`) и уровень критичности (`severity: "critical"`).
  • Далее мы хотим отправить Push-уведомление через сервис Pushover с помощью узла `node-red-node-pushover`. Этот узел берет текст сообщения из `msg.payload`.
  • После отправки уведомления мы хотим записать инцидент в базу данных `MySQL`, указав `zone_id` и `severity`.
  • Поток с анти-паттерном:

    `[GPIO In]` -> `[Change: add metadata]` -> `[Function: prepare text]` -> `[Pushover]` -> `[Function: prepare SQL]` -> `[MySQL]`

    Проблема возникает на шаге 3. Узел `Pushover` (как и многие другие) после успешной отправки заменяет весь объект `msg` на результат ответа от API Pushover.

        {
    

    "payload": "Внимание! Протечка на кухне!",

    "topic": "pushover",

    "zone_id": "kitchen",

    "severity": "critical"

    }

        {
    

    "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]`:

    Теперь, после узла `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` разнородными данными. Решение — ветвление, фокусировка и чистота сообщений.
  • Злоупотребление `global context`: Использование глобальных переменных для транзита данных. Решение — передача данных через `msg`, а в контексте хранить только состояние.
  • Неявные «контракты»: Отсутствие проверки структуры входящего `msg`. Решение — строгая валидация на входе в каждый логический блок.
  • Потеря исходных данных: Перезапись `msg` узлами ввода-вывода. Решение — сохранение важных данных в промежуточные свойства `msg` перед вызовом таких узлов.
  • «Золотые правила» работы с `msg`

    Можно сформулировать четыре основных принципа, которые являются противоядием от всех рассмотренных проблем:

    Чек-лист для самопроверки потока

    Перед тем как считать работу над потоком завершенной, пройдитесь по этому короткому чек-листу:

    Что дальше

    Мы завершили ключевой модуль, посвященный сердцу Node-RED — объекту `msg`. Вы получили теоретические знания и практические навыки для создания надежных и профессиональных коммуникационных потоков. В следующем модуле мы перейдем от этой основы к реальному миру: начнем подключать к нашему контроллеру HI настоящие промышленные и бытовые устройства, используя протоколы Modbus, MQTT и другие, и применять все изученные паттерны и анти-паттерны на практике.