Анти-паттерны: `Function` там, где хватило бы `Change` и `Switch`
Введение: Проблема "черных ящиков" в Node-RED
Ключевая сила платформы Node-RED и причина её популярности — в наглядности. Визуальное программирование позволяет инженерам и даже не-программистам понимать логику работы системы, просто взглянув на поток (flow). Линии, соединяющие узлы, показывают путь движения данных, а сами узлы декларативно описывают, какая операция выполняется на каждом шаге. Вы видите узел `mqtt in`, за ним `Switch`, потом `Change` и `DALI out` — и вы моментально понимаете: "Система получает сообщение из MQTT, проверяет некое условие, изменяет данные и отправляет команду на светильник".
В этом декларативном, наглядном мире нода `Function` стоит особняком. Это — императивный "черный ящик". В отличие от других узлов, её иконка не говорит нам ничего о том, что происходит внутри. Там может быть одна строчка кода, изменяющая `msg.topic`, а может быть сложный алгоритм на 500 строк с циклами, API-запросами и комплексными вычислениями. Чтобы понять логику, зашитую в `Function`, вам необходимо открыть её редактор и прочитать (а часто — и расшифровать) написанный кем-то JavaScript-код.
Избыточное и неоправданное использование ноды `Function` превращает изящный, читаемый поток в минное поле.
- Усложняется отладка: Если поток ведет себя некорректно, вам приходится последовательно открывать каждую ноду `Function` и анализировать её код. Визуальная диагностика, являющаяся преимуществом Node-RED, становится невозможной.
- Снижается поддерживаемость: Инженер, который не писал исходный поток, потратит в разы больше времени на его понимание и внесение изменений, если логика скрыта внутри множества "черных ящиков".
- Страдает производительность: Хотя это и не всегда очевидно, выполнение кастомного кода в изолированной среде (sandbox) ноды `Function` может приводить к дополнительным накладным расходам на CPU. Внутренний механизм Node-RED, на котором работают нативные ноды (`Change`, `Switch` и др.), часто оптимизирован лучше, чем универсальная среда для выполнения произвольного JavaScript.
Цель этого урока — научиться распознавать анти-паттерны, связанные с неоправданным использованием `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 ];
}
Почему это плохо?
Правильное решение с нодой `Switch`
Та же самая логика реализуется с помощью одной ноды `Switch` за несколько кликов мышью. Как мы уже рассматривали в уроке про `Switch`, эта нода специально создана для маршрутизации сообщений.
Настройка ноды `Switch`:* Правило 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`
Та же самая трансформация элегантно выполняется с помощью ноды `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"
}
Задача потока:
Поток "До" рефакторинга
Поток состоит из одной ноды `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`:- Property: `msg.payload.values`
- Правило 1: `co2` (`msg.payload.values.co2`) > `1000` (число)
- Правило 2: `voc` (`msg.payload.values.voc`) > `300` (число)
- В опциях ноды выставляем "checking all rules". Это важно для правильной логики.
Шаг 2: Трансформируем данные в каждой ветви
Теперь после каждого выхода `Switch` мы ставим ноду, отвечающую за подготовку `msg`.
- Ветвь 1 (CO2 > 1000):
* Настройка `Change`: одно правило `Set msg.payload` в значение (тип JSON):
{
"command": "set_speed",
"value": 75
}
- Ветвь 2 (VOC > 300):
* Настройка `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 |
+---------------------------+ +----------------+
Сравнение результатов
Поток "После" может выглядеть немного более громоздким, но его преимущества неоспоримы:
- Прозрачность: С одного взгляда на поток понятны и условия ветвления (`Switch`), и то, как именно преобразуются данные на каждом шаге (`Change`, `Template`).
- Принцип единственной ответственности: Каждый узел выполняет одну простую задачу. `Switch` — маршрутизирует. `Change` — изменяет структуру. `Template` — форматирует строку. Это упрощает отладку. Если неверно формируется строка тревоги, мы точно знаем, что проблема в ноде `Template`.
- Гибкость: Нужно изменить скорость вентилятора с `75` на `80`? Открываем ноду `Change` и меняем одно число в JSON, не рискуя сломать остальную логику.
Рефакторинг превратил непрозрачный "черный ящик" в читаемую, поддерживаемую и надежную визуальную схему.
---
Выводы: Когда нода Function все-таки необходима?
Рассмотрев ключевые анти-паттерны, важно не сделать ложный вывод, что нода `Function` — это зло, которого следует избегать любой ценой. Это мощный инструмент, но, как и любой мощный инструмент, он требует осознанного применения.
Золотое правило:> Используйте ноду `Function` только тогда, когда стоящую перед вами задачу действительно невозможно решить комбинацией стандартных нод (`Switch`, `Change`, `Template`, `Split`, `Join` и др.).
🔗 Связанный материал: Подробный разбор легитимных сценариев использования ноды `Function` был в предыдущем уроке: `COURSE-06-M03-L07: Нода Function: когда без JavaScript не обойтись`.
Кратко перечислим оправданные сценарии для использования `Function`:
Цель этого урока — не в том, чтобы вы отказались от `Function`, а в том, чтобы перед каждым её использованием вы задавали себе вопрос: "А нет ли более простого, более 'нативного' для Node-RED способа решить эту задачу?". В большинстве случаев он есть. Осознанное применение `Function` только там, где она действительно нужна, является признаком профессионализма и ключом к созданию по-настоящему читаемых и надежных систем автоматизации.
Что дальше
В следующем уроке мы углубимся в управление контекстом и хранение состояния между перезагрузками контроллера, что позволит создавать еще более надежные и отказоустойчивые сценарии автоматизации.