EXPEDIA GROUP ТЕХНОЛОГИИ — ПРОГРАММНОЕ ОБЕСПЕЧЕНИЕ

Применение принципа единой ответственности к многоуровневой архитектуре FE/BFF— подробности внутренней архитектуры

Бэкэнд как композиция компонентов с одной ответственностью — Часть 3/3

В этом посте подробно рассматриваются детали внутренней архитектуры подхода, описанного в части 1 Применение принципа единой ответственности к многоуровневой архитектуре FE/BFF и в части 2 серии, посвященной Сведениям об архитектуре внешнего интерфейса. Для получения дополнительной информации, пожалуйста, сначала прочитайте эти сообщения.

Вариант использования

В этом обсуждении мы сосредоточимся на одном варианте использования нашего гипотетического приложения:

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

Внутренние слои

Для этого приложения у нас есть бэкенд, реализующий паттерн Backend For Frontends (BFF). Его основная цель — быть посредником между сервисами домена и нашим интерфейсом. Проще говоря, этот бэкэнд организует сообщения между нашим веб-клиентом и нижестоящими доменными службами. На данный момент мы можем предположить, что и веб-клиент, и доменные службы используют HTTP REST. Другими словами, этот бэкэнд будет обмениваться данными через HTTP с обеих сторон: восходящей и нисходящей.

Подобно тому, что мы делали в начале нашего общего архитектурного проектирования, это помогает думать об «обязанностях» или «категориях вещей, которые должны произойти» в коде при разработке этих слоев. То же самоетакжеи в нашем внутреннем дизайне.

Для некоторого контекста, вот высокоуровневая архитектура на основе SRP, к которой мы пришли ранее. В этом обсуждении мы сосредоточимся на разделе BFF API (бэкэнд).

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

  • Контроллер
  • Оказание услуг
  • Картограф

Последовательность внутренних операций

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

Подводя итог, вот что должно произойти:

  1. Контроллер получает HTTP-запрос веб-клиента, чтобы узнать часы работы.
  2. Контроллер вызывает службу для получения и обработки соответствующих нижестоящих служб и объектов домена.
  3. Контроллер перенаправляет ответ службы сопоставителю.
  4. сопоставитель переводит сообщения службы домена в желаемую модель представления.
  5. Контроллер отправляет сопоставленную модель представления веб-клиентам в ответе на запрос.

Размышляя над «композицией».

Теперь давайте сделаем шаг назад. Обычно на этом этапе, если вы знакомы с диаграммами последовательности UML как частью рабочего процесса разработки в рамках своих привычек объектно-ориентированного программирования, вы начинаете думать о классах и/или, возможно, интерфейсах В этом случае давайте не будем идти по этому пути, а вместо этого подумаем о композиции функций. Мы думаем о цепочке функций, которые при объединении и вызове возвращают нам то, что нам нужно. Что-то вроде этого:

request --> controller --> service --> mapper --> view model

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

viewModel = mapper(service(request))

Интерфейс композиции

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

resolve :: a -> b  
/*   
    resolve = our black-box function that resolves `a` onto `b`   
    a = the POJO input, i.e., HTTP payload/params in this case   
    b = the JSON response sent to the client with the `viewModel` 
*/

Мы называем эту функцию черного ящика «разрешением», вдохновленную преобразователями GraphQL.

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

Обратите внимание, что это не обязательно означает внедрение технологии проверки типов (например, TypeScript). Хотите ли вы применить эти интерфейсы каким-то автоматическим образом, это другой разговор. Это не умаляет того факта, что было бы неплохо каким-то образом формализовать этот контракт.

Допустим, у нас есть разные серверные (нисходящие) системы, которые могут разрешить запрос в зависимости от разных условий (например, конфигурации развертывания приложения), и все они имеют разные интерфейсы API. Наша логика черного ящика теперь должна адаптироваться к различным условиям для одного и того же запроса. Пока эти адаптации соответствуют нашему интерфейсу resolve, мы можем подключить любой преобразователь, который захотим, для выполнения запроса; даже во время выполнения.

Адаптеры среды выполнения для различных вышестоящих инфраструктур

Чтобы развить предыдущее понятие, предположим, что нам нужно адаптировать нашу логику черного ящика к двум различным системам: CP и EG (давайте назовем их так для этого примера). Затем мы можем создать «ABC Resolver» и «XYZ Resolver», которые реализуют наш resolve :: a -> b интерфейс. Эти преобразователи действуют как адаптеры в нашей системе для любых нижестоящих серверных служб, которые нам могут понадобиться. Этот подход позволяет нам подключать во время выполнения любой подходящий преобразователь, что делает нашу систему более гибкой.

Подведение итогов

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

  • Контроллер
  • Оказание услуг
  • Картограф

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

Схемы предоставлены автором.