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

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

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

Поскольку триггеры Cloud Firestore основаны на обновлениях для каждого документа, я закончил тем, что скопировал один и тот же код для каждой коллекции при каждом конкретном триггере. Например, у меня остались функции profiles-onCreate, profiles-onUpdate и profiles-onDelete, и в каждой функции присутствовал один и тот же или очень похожий код для синхронизации с Algolia. Затем это повторилось для других коллекций, для которых требовалась аналогичная функциональность: posts-onCreate, posts-onUpdate и posts-onDelete.

По мере того, как я продолжал создавать эти приложения для добавления дополнительных функций, они становились все более сложными и трудными в обслуживании: копирование и вставка кода в различные триггерные функции вызвали быстрый рост кодовой базы, а выполнение каждой отдельной функции нескольких задач сделало их более сложными и склонными к отказ. Ошибка в коде для одной задачи может привести к сбою всей функции, не позволяя запускать другие задачи, что потенциально может привести к потере данных. И пытаетесь исправить эту ошибку, когда вы копировали этот код в разные функции? Повеселись.

Решение

Чтобы решить эту проблему, я сначала сделал шаг назад и посмотрел на шаблоны в написанном мной коде. Каждая облачная функция выполняла несколько задач и запускала один и тот же код для разных коллекций. Но это противоречит самой идее функции. Википедия определяет ее как:

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

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

Кроме того, в любом случае не было веских причин для такого частого копирования и вставки кода, поэтому логика, разделяемая различными облачными функциями, должна быть написана в отдельной функции. Например, каждая -onUpdate облачная функция должна вызывать обобщенную syncFieldsToAlgolia функцию, в которую можно передать аргумент, чтобы указать поля для синхронизации.

Хотя эти два шага повышают надежность результирующих облачных функций, они мало что решают для решения плохого опыта разработчиков, связанного с необходимостью явного написания облачной функции для каждой комбинации коллекции, триггера Firestore и задачи. Чтобы повторно реализовать три упомянутые выше задачи с помощью этого решения, мне нужно было бы явно написать 3 облачных функции для каждого из 3 триггеров Firestore для 2 упомянутых коллекций - всего 18 облачных функций!

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

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

Фактически, это тот же подход, который делает React настолько популярным: он позволяет разработчикам объявлять, что должно отображаться на экране, не беспокоясь о том, как манипулировать и обновлять базовый DOM для достижения этого.

Пример

Бизнес-требования: нам нужно отобразить список новых пользователей, которые еще не прошли проверку, с сортировкой по времени регистрации.

Проблема: Firestore не позволяет запрашивать документы, в которых нет определенного поля, т.е. поле не определено.

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

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

Давайте создадим новую функцию, которая возвращает облачную функцию, запускаемую событием onCreate. Когда документ создается, он обновляет документ с указанными начальными значениями. Этой функции не нужно знать, какую коллекцию она будет прослушивать или какими должны быть начальные значения.

Затем мы можем сохранить конфигурации отдельно, где мы можем просто объявить какие документы должны иметь какие начальные значения. В этом случае мы хотим убедиться, что все документы в коллекции users имеют значение поля verified по умолчанию false и поле createdAt для отметки времени сервера.

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

Затем этот код добавляется в индексный файл репозитория облачных функций. Интерфейс командной строки Firebase создает облачные функции из экспорта файла. И я намеренно выбрал экспорт функций таким образом, чтобы группировать функции: интерфейс командной строки автоматически добавит к функциям префикс initialize-.

Затем мы можем развернуть все эти функции с помощью одной простой команды:

firebase deploy --only functions:initialize

А вот и облачная функция в действии!

Выгоды

Поскольку код был перемещен в одну обобщенную функцию, мы полностью исключили повторяющийся код. Это дает ряд преимуществ:

  • Требуется меньше кода. Мы можем легко добавлять функции или исправлять ошибки в одном месте, где написан код. 🐛 🔫
  • Отсутствие ошибок при копировании и вставке кода. Больше никаких забытых неизмененных имен переменных, которые приводят к сбою вашей функции. 🚧
  • Тестирование можно упростить. Его нужно запускать только для обобщенной функции, что упрощает улучшение покрытия кода.

А поскольку мы выделили бизнес-логику, сделав ее декларативной, у нас есть:

  • Повышенная читаемость бизнес-логики. Значительно легче понять, какая бизнес-логика реализована и каков ожидаемый результат.
  • Намного более простой способ добавить новую бизнес-логику. Мы могли бы даже создать графический интерфейс для создания этих облачных функций и попросить нетехнических пользователей добавлять к нему - все, что для этого нужно, - это вывести правильный объект конфигурации.

Недостатки

  • Более медленное развертывание. Поскольку отдельные облачные функции создаются во время развертывания, интерфейс командной строки Firebase не распознает их сразу, поэтому всю функциональную группу необходимо развертывать каждый раз.
  • Более высокие затраты. Поскольку исходные мега -облачные функции были разделены на облачные функции для одной операции, мы несем дополнительные расходы из-за увеличения времени вычислений (в результате большего числа загружаемых функций) и из-за большего количества загружаемых функций. вызовы (если в проекте используется больше свободного лимита).

Недавно я использовал этот шаблон во многих наших облачных функциях в проекте с открытым исходным кодом Firetable, графическом интерфейсе для Firestore, похожем на электронную таблицу. Он используется для интеграции с Algolia, синхронизации данных между разными коллекциями, ведения базовой истории версий документов и многого другого.

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