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

Анти-паттерны: `Function` там, где хватило бы `Change` и `Switch`

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

Введение: Проблема "черных ящиков" в Node-RED

Ключевая сила платформы Node-RED и причина её популярности — в наглядности. Визуальное программирование позволяет инженерам и даже не-программистам понимать логику работы системы, просто взглянув на поток (flow). Линии, соединяющие узлы, показывают путь движения данных, а сами узлы декларативно описывают, какая операция выполняется на каждом шаге. Вы видите узел `mqtt in`, за ним `Switch`, потом `Change` и `DALI out` — и вы моментально понимаете: "Система получает сообщение из MQTT, проверяет некое условие, изменяет данные и отправляет команду на светильник".

В этом декларативном, наглядном мире нода `Function` стоит особняком. Это — императивный "черный ящик". В отличие от других узлов, её иконка не говорит нам ничего о том, что происходит внутри. Там может быть одна строчка кода, изменяющая `msg.topic`, а может быть сложный алгоритм на 500 строк с циклами, API-запросами и комплексными вычислениями. Чтобы понять логику, зашитую в `Function`, вам необходимо открыть её редактор и прочитать (а часто — и расшифровать) написанный кем-то JavaScript-код.

Избыточное и неоправданное использование ноды `Function` превращает изящный, читаемый поток в минное поле.

Цель этого урока — научиться распознавать анти-паттерны, связанные с неоправданным использованием `Function`, и заменять их на более эффективные, наглядные и декларативные решения с помощью стандартных нод, таких как `Switch` и `Change`.

---

Анти-паттерн №1: Маршрутизация через Function вместо Switch

Одним из самых распространенных злоупотреблений нодой `Function` является реализация простой логики ветвления. Инженеры, пришедшие из мира традиционного программирования, часто по привычке пишут конструкции `if-else` или `switch-case` внутри `Function` для направления сообщения по разным путям.

> ⚠️ Внимание: Код внутри `Function`, который просто проверяет свойство `msg` и возвращает массив сообщений для разных выходов (например, `[msg, null]` или `[null, msg]`), является явным признаком того, что нода `Switch` была бы лучшим выбором. Этот анти-паттерн усложняет отладку и затемняет логику потока.

Пример "плохого" решения

Представим задачу: мы получаем с датчика температуру и в зависимости от её значения должны направить поток по одному из трех путей: "Включить охлаждение", "Включить обогрев" или "Ничего не делать".

Входящее сообщение:

{

"payload": {

"value": 23.5,

"source": "temp-sensor-office-1",

"ts": 1678886400000,

"unit": "°C"

},

"topic": "telemetry/sensor/temperature"

}

Неопытный пользователь может реализовать это с помощью ноды `Function` с тремя выходами:

// Код внутри ноды Function с 3 выходами

const temp = msg.payload.value;

if (temp > 25) {

// Выход 1: Охлаждение

return [ msg, null, null ];

} else if (temp < 19) {

// Выход 2: Обогрев

return [ null, msg, null ];

} else {

// Выход 3: Норма

return [ null, null, msg ];

}

Почему это плохо?
  • Ненаглядно: Глядя на поток, вы видите просто иконку `ƒ`. Чтобы понять правила маршрутизации ( `>25`, `<19`), вам нужно открыть редактор кода.
  • Сложно для модификации: Чтобы добавить новое правило (например, "Включить вентиляцию" при `temp > 23`), нужно не просто добавить новый выход, но и аккуратно отредактировать код, изменив массив возвращаемых значений (`[null, msg, null, null]`). Это повышает риск ошибки.
  • Избыточно: Задача не требует гибкости JavaScript, она решается стандартными средствами.
  • Правильное решение с нодой `Switch`

    Та же самая логика реализуется с помощью одной ноды `Switch` за несколько кликов мышью. Как мы уже рассматривали в уроке про `Switch`, эта нода специально создана для маршрутизации сообщений.

    Настройка ноды `Switch`:
  • Добавляем ноду `Switch` и открываем её настройки.
  • В поле "Property" указываем `msg.payload.value`.
  • Создаем три правила:
  • * Правило 1: `is greater than` ( `>` ) `25` (число)

    * Правило 2: `is less than` ( `<` ) `19` (число)

    * Правило 3: `otherwise`

    Результат — нода `Switch` с тремя выходами, каждый из которых соответствует одному из правил.

    | Критерий | Решение с `Function` | Решение с `Switch` |

    | ----------------------- | ---------------------------------------------------- | ------------------------------------------------ |

    | Читаемость | Низкая. Логика скрыта внутри кода. | Высокая. Правила видны прямо под нодой в редакторе. |

    | Поддерживаемость | Низкая. Требует редактирования JS-кода. | Высокая. Правила добавляются и меняются в UI. |

    | Порог входа | Требует знания синтаксиса JavaScript и API Node-RED. | Нулевой. Интуитивно понятный графический интерфейс. |

    | Визуальная диагностика | Невозможна без открытия ноды. | Отличная. Сразу видно, по какому пути прошло сообщение.|

    Используя `Switch`, мы возвращаем потоку его главное преимущество — наглядность. Любой член команды, взглянув на схему, поймет логику климат-контроля без необходимости погружаться в код.

    ---

    Анти-паттерн №2: Манипуляции с `msg` через Function вместо Change

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

    > 💡 Подсказка: Нода `Change` — это ваш "швейцарский нож" для работы с сообщениями. Прежде чем открывать редактор `Function` для простой манипуляции данными, задайте себе вопрос: "А можно ли это сделать через `Change`, возможно, с помощью JSONata?". В 90% случаев ответ будет "да".

    Пример "плохого" решения

    Предположим, мы получили данные от Modbus-устройства, и они находятся в `msg.payload.data`. Нам нужно извлечь значение, переименовать поле и установить новый `msg.topic`.

    Входящее сообщение:

    {
    

    "payload": {

    "data": [ 455 ],

    "buffer": ""

    },

    "topic": "modbus/raw/input-register-30001",

    "qos": 1,

    "retain": false

    }

    Задача: Преобразовать его в сообщение вида:
    {
    

    "payload": {

    "humidity": 45.5

    },

    "topic": "telemetry/greenhouse/humidity"

    }

    Типичный, но неоптимальный путь решения — написать код в `Function`:

    // Код внутри ноды Function
    
    

    // Извлекаем значение и делим на 10

    const humidityValue = msg.payload.data[0] / 10;

    // Формируем новый payload

    msg.payload = {

    humidity: humidityValue

    };

    // Устанавливаем новый topic

    msg.topic = "telemetry/greenhouse/humidity";

    return msg;

    Почему это плохо?
  • Не декларативно: Код описывает как шаг за шагом трансформировать объект. Декларативный подход, который предлагает `Change`, описывает что мы хотим получить в итоге.
  • Скрытая логика: Как и в случае со `Switch`, мы не видим правил трансформации, не открыв редактор.
  • Риск ошибок: Можно случайно удалить весь `msg` (`msg = { payload: ... }`), потеряв важные свойства, или допустить синтаксическую ошибку.
  • Правильное решение с нодой `Change`

    Та же самая трансформация элегантно выполняется с помощью ноды `Change` и её правил. Как мы помним из предыдущих уроков, `Change` позволяет выполнять несколько операций последовательно.

    Настройка ноды `Change`:
  • Добавляем ноду `Change` и открываем её настройки.
  • Добавляем три правила, которые выполнятся сверху вниз:
  • * Правило 1 (Set): Установить `msg.payload` в значение, вычисленное с помощью JSONata.

    * Выражение JSONata: `{"humidity": payload.data[0] / 10}`

    * Это правило за один шаг создает новый `msg.payload` нужной структуры.

    * Правило 2 (Set): Установить `msg.topic` в значение `telemetry/greenhouse/humidity` (строка).

    Всего два правила в графическом интерфейсе полностью заменяют блок кода.

    | Критерий | Решение с `Function` | Решение с `Change` / JSONata |

    | -------------------- | ------------------------------------------------------- | ------------------------------------------------------ |

    | Подход | Императивный ("как делать"). | Декларативный ("что получить"). |

    | Наглядность | Низкая. Что меняется в `msg`? Неизвестно без входа в ноду. | Высокая. Список правил четко описывает трансформацию.|

    | Мощность | Ограничена только знанием JavaScript. | Высокая, благодаря JSONata для сложных трансформаций. |

    | Безопасность | Есть риск случайно повредить или перезаписать `msg`. | Выше. Правила действуют на конкретные свойства. |

    С помощью JSONata в ноде `Change` можно выполнять и более сложные вещи: преобразовывать массивы, фильтровать объекты, конкатенировать строки — и все это без единой строчки JS-кода, сохраняя поток визуально понятным.

    ---

    Практика: Рефакторинг потока от Function к Switch и Change

    Теперь объединим оба анти-паттерна и проведем рефакторинг — процесс улучшения кода/потока без изменения его внешней функциональности.

    Представим поток, который обрабатывает данные от комплексного датчика качества воздуха. Одна нода `Function` с двумя выходами решает, что делать, и форматирует сообщение.

    Входящее сообщение `msg`:
    {
    

    "payload": {

    "deviceId": "air-quality-livingroom-01",

    "values": {

    "temperature": 22.1,

    "co2": 1150,

    "voc": 350

    },

    "status": "online"

    },

    "topic": "raw/airquality/livingroom"

    }

    Задача потока:
  • Если уровень CO2 (`co2`) превышает `1000` ppm, отправить на первый выход команду для системы вентиляции в формате `{ "command": "set_speed", "value": 75 }`.
  • Если уровень летучих органических соединений (`voc`) превышает `300` ppb, отправить на второй выход тревожное уведомление в MQTT топик `alerts/airquality` с текстом: `WARN: High VOC level (350 ppb) in livingroom`.
  • Если оба условия выполнены, сработать должно только первое (приоритет у CO2).
  • Поток "До" рефакторинга

    Поток состоит из одной ноды `Function`, которая является "черным ящиком".

    ASCII-схема "До":
                               +----------------------+
    

    [...источник данных...] -> | | -- (выход 1) --> [Управление вентиляцией]

    | Function: |

    | ProcessAirQuality |

    | | -- (выход 2) --> [Отправка Alert в MQTT]

    +----------------------+

    Код в `Function: ProcessAirQuality`:
    const co2 = msg.payload.values.co2;
    

    const voc = msg.payload.values.voc;

    if (co2 > 1000) {

    // Приоритетное условие: CO2

    msg.payload = {

    "command": "set_speed",

    "value": 75

    };

    return [ msg, null ];

    } else if (voc > 300) {

    // Второе условие: VOC

    const deviceLocation = msg.payload.deviceId.split('-')[2]; // "livingroom"

    msg.payload = `WARN: High VOC level (${voc} ppb) in ${deviceLocation}`;

    msg.topic = "alerts/airquality";

    return [ null, msg ];

    }

    // Если ни одно условие не выполнено, останавливаем поток

    return null;

    Этот поток работает, но он непрозрачен и хрупок.

    Процесс рефакторинга: "После"

    Мы заменим одну ноду `Function` на комбинацию из `Switch`, `Change` и `Template`.

    Шаг 1: Выделяем логику маршрутизации в `Switch`

    Первым делом избавляемся от `if-else`. Добавляем ноду `Switch`, которая будет проверять условия и направлять исходное сообщение по нужной ветке.

    Настройка ноды `Switch`:

    Шаг 2: Трансформируем данные в каждой ветви

    Теперь после каждого выхода `Switch` мы ставим ноду, отвечающую за подготовку `msg`.

    * После первого выхода `Switch` ставим ноду `Change`.

    * Настройка `Change`: одно правило `Set msg.payload` в значение (тип JSON):

          {

    "command": "set_speed",

    "value": 75

    }

    * После второго выхода `Switch` ставим ноду `Template`. Она идеально подходит для форматирования строк.

    * Настройка `Template`:

    * Property: `msg.payload`

    * Format: `Mustache Template`

    * Template: `WARN: High VOC level ({{payload.values.voc}} ppb) in {{payload.deviceId}}`. (Мы можем упростить извлечение локации для наглядности).

    * За нодой `Template` ставим еще одну `Change` для установки топика: `Set msg.topic` to `alerts/airquality`.

    ASCII-схема "После":
                                    +---------------------------+
    

    | Change: |

    | Set payload for ventilator| -- (выход 1) --> [Управление вентиляцией]

    +---------------------------+

    /

    [...источник...] -> [Switch] --

    (CO2 > 1000) (VOC > 300) \

    +---------------------------+ +----------------+

    | Template: |-> | Change: |-> [Отправка Alert в MQTT]

    | Format VOC alert string | | Set MQTT topic |

    +---------------------------+ +----------------+

    Сравнение результатов

    Поток "После" может выглядеть немного более громоздким, но его преимущества неоспоримы:

    Рефакторинг превратил непрозрачный "черный ящик" в читаемую, поддерживаемую и надежную визуальную схему.

    ---

    Выводы: Когда нода Function все-таки необходима?

    Рассмотрев ключевые анти-паттерны, важно не сделать ложный вывод, что нода `Function` — это зло, которого следует избегать любой ценой. Это мощный инструмент, но, как и любой мощный инструмент, он требует осознанного применения.

    Золотое правило:

    > Используйте ноду `Function` только тогда, когда стоящую перед вами задачу действительно невозможно решить комбинацией стандартных нод (`Switch`, `Change`, `Template`, `Split`, `Join` и др.).

    🔗 Связанный материал: Подробный разбор легитимных сценариев использования ноды `Function` был в предыдущем уроке: `COURSE-06-M03-L07: Нода Function: когда без JavaScript не обойтись`.

    Кратко перечислим оправданные сценарии для использования `Function`:

  • Сложные циклы и итерации: Когда вам нужно пройтись по массиву данных и выполнить для каждого элемента нетривиальную операцию, которую не покрывает JSONata (`for`/`while`/`map`/`reduce`).
  • Асинхронные операции: Вызов функций, которые возвращают результат не сразу (например, через callback или Promise), особенно при работе с некоторыми внешними библиотеками.
  • Работа с внешними библиотеками: Если для обработки данных вам требуется специфическая библиотека (например, для криптографии, работы с датами `moment.js` или сложной математики), её можно подключить в `settings.js` и использовать внутри `Function`.
  • Сложное управление состоянием: Хотя простое хранение состояния лучше делать через контекст в ноде `Change`, `Function` дает больше контроля при реализации сложных конечных автоматов (FSM) или при необходимости атомарно прочитать-изменить-записать значение в `flow` или `global` контекст.
  • Работа с бинарными данными: Когда требуется побайтовая работа с буферами данных, которую не предоставляют стандартные узлы.
  • Цель этого урока — не в том, чтобы вы отказались от `Function`, а в том, чтобы перед каждым её использованием вы задавали себе вопрос: "А нет ли более простого, более 'нативного' для Node-RED способа решить эту задачу?". В большинстве случаев он есть. Осознанное применение `Function` только там, где она действительно нужна, является признаком профессионализма и ключом к созданию по-настоящему читаемых и надежных систем автоматизации.

    Что дальше

    В следующем уроке мы углубимся в управление контекстом и хранение состояния между перезагрузками контроллера, что позволит создавать еще более надежные и отказоустойчивые сценарии автоматизации.