Как родилась идея шаблона?

Что являлось отличительной особенностью этой параллельной разработки сервисов на проекте? Собственно то, что я бегал по командам и рассказывал, что и как надо делать, потому что документации на этом этапе у нас никакой не было. Точнее была, но не в полном объеме. Но работу то вести надо было. Было ли это тяжело? Думаю, ответ очевиден. Это был “День сурка”. С этим надо было что-то делать, и причем чем быстрее, тем лучше.
И вот на этом этапе появляется у нас, в System Team, идея. Давайте, наверное, хотя бы сделаем отдельный репозиторий на GitHub, где хотя бы соберем какой-то шаблон правильного сервиса. Чтобы команды хотя бы копировали себе его локально, и брали оттуда примеры, что и как делать. Ну собственно под это дело отдельная UserStory, я её даю отдельному BE девелоперу, начинаем делать.

Планировали за день-два ее закрыть, ну потому что по большому счету, было откуда этот шаблон взять - к тому моменту уже был в Production сервис аутентификации. Но в процессе реализации появляется новая идея: хм, так может это дело немножко заимпрувить, а давайте сделаем project template и завернем его вообще в NuGet. То есть это гораздо быстрее будет, нежели какой-то там репозиторий, что-то клонировать и т.д.
Мы в итоге делаем NuGet-пакет под project template. Что в этом темплейте находится? Банально код и структура микросервиса, если бы он имплементился для продакшена. За исключением объема бизнес логики и реальных юзкейзов, которые могли бы быть, но это вроде как логично. Этот микросервис мы назвали SampleMicroservice. Ну тут надеюсь понятно, что этот сервис никуда не деплоился;)

Архитектура шаблонного микросервиса

Структура солюшена в нем построена по заветам Clean Architecture (теорию можно найти у Bob Martin, “желтая книга”, классика). Т.е. несколько проектов внутри, некоторые из них относятся к Core Business (Domain) Layer, некоторые к Application Layer (например Use Cases), тут же проекты которые являются портами для прожектов адаптеров из следующего слоя - Infrastructure (проекты инкапсулирующие логику работы с любыми third party и не только, например SendGrid). Собственно слой Infrastructure изолируется от Application Logic с помощью инверсии зависимостей основанной на понятии портов.
Что такое порты и адаптеры? Ну чтобы не тратить время, это можно загуглить – “Архитектура портов и адаптеров”. Но в двух словах порты это просто проекты с интерфейсами. Есть разные подходы их реализации. Есть подходы, как вот, например, у меня - отдельный порт, отдельный проект. Либо некоторые делают а-ля “материнская плата”, то есть один проект, в котором куча портов. Тогда все адаптеры подключается к этим портам как на материнскую плату какие-то элементы.

У меня как раз-таки первый подход. Например, у нас было понятие репозиториев, Repository pattern. Под каждый репозиторий был интерфейс. Интерфейсы находятся на уровне Application Layer в виде портов. А на уровне Infrastructure Layer находится их реализация, которая называется адаптеры. И там реализация интерфейса репозитория плюс куча кода относящегося к тому функционалу, собственно который мы и скрываем за портом.
Например, Entity Framework как ORM для работы с MS SQL-сервером. То есть это все изолировано и от Application Logic и естественно от Domain abstractions, они все полностью изолированы. Они (Domain и Application Logic) по итогу не зависят ни от чего. Ну и оставшиеся проекты, это в основном один, например Api прожект, а иногда еще и какой-нибудь Worker, в общем непосредственно исполняемые прожекты.
Каждый слой со своими проектами структурированы в отдельные фолдеры с четкими именами. Кстати из вышесказанного можно сделать вид, что мы смешали два вида архитектур Clean Architecture и Ports and Adapters. Отчасти да. Но на самом деле идея Портов и Адаптеров в немного видоизмененном виде присутствует в Чистой архитектуре. Тут мы использовали основные классические пункты при построении архитектуры любого приложения:
  1. Понять сложность своего проекта
  2. Взять соразмерную архитектуру
  3. Выбросить все лишнее (YAGNI)
И YAGNI здесь ключевое ;)
Погружаясь в конкретный проект в солюшене мы увидим целый список из conventions, который потом стали частью документации. От формата имен namespaces (enterprise.domain.subdomain.level/layer) до примеров Entities (в том числе Rich models), ValueObjects, Events, UseCases, Restful Url for endpoints, Command/Query Handlers, Repositories и других типов портов/адаптеров. И естественно это все было на примере кода в этом Sample микросервисе, каждый объект на своем правильном уровне.
Стоит отметить, что еще до работ над шаблоном сервиса и параллельно с ним писался Common функционал для всего проекта. В виде отдельных Common репозиториев на GitHub. Этот функционал паблишился как Nuget пакеты. Ну и т.к. шаблонный сервис это все-таки без 5-ти минут рабочий сервис, то мы естественно в него сразу подключали все нужные Nuget пакеты для жизни новых сервисов, которые будут построены на базе этого шаблона. Это такие пакеты как например, мониторинг, диагностика, health-чеки, JSON сериализация, брокер сообщений, Swagger, необходимый минимум из middlewares, аутентификация (Client SDK нашего сервиса аутентификации), авторизация, подгрузка конфигураций из DB, error handling, разные Common.Tools и т.д. Плюс было расставлено много TODO comments со всевозможными пояснениями. И даже сейчас мы держим шаблон в актуальном состоянии и обновляем в нем соответствующие пакеты.

Особенности SampleMicroservice

У SampleMicroservice была интересная особенность. Что за особенность? Тут стоит сказать, что у нас на проекте (в целом наш проект Azure Cloud based) для хостинга приложений использовались три типа хостов: обычный AppService, т.е. просто WebApi, и две версии хоста для serviceless приложений AzureFunctions V3-V4 и AzureFunctions V4 Isolated, это аналоги например AWS Lambda. Ну и собственно в шаблонном сервисе было три типа реализации слоя исполняемых приложений. И в каждом из них были подключены все соответствующие типу хоста tools-ы, и под каждый хост были примеры подключения всего необходимого, в том числе и соответствующий конкретному типу хоста Client SDK nuget пакет для аутентификации, разработанный уже к тому времени в рамках сервиса аутентификации.
Может возникнуть вопрос, как эти три типа хостов уживаются в одном солюшене, или получается, что ненужные два, при использовании придется удалить? Так вот, почему же мы собственно пришли к понятию nuget-пакета именно для project-темплейта, для IDE, потому что при создании нового микросервиса на основе этого уже предварительно установленного project-темплейта, мы добавили возможность выбрать необходимый тип хоста.

Например, пишете просто в команде в cmd первым параметром isolated, вторым название сервиса, например users. И автоматически, при разворачивании солюшена из темплейта, лишние типы хостов просто будут удалены. В итоге у вас остается исполняемый проект для указанного типа хоста.
Как отработает второй указанный параметр? На значение второго параметра, в нашем примере это users, будут заменены все вхождения строки Sample в темплейте. Например в коде темплейта есть такой тип как SampleEvent, так вот на выходе будет UsersEvent, был SampleRepository, а станет UsersRepository и т.д. Понимаете о чем речь? Т.е. реплейс по всему солюшену будет, даже в комментариях. Мы старались все максимально автоматизировать.
На самом деле это еще не все, что было и есть в этом темплейте. Также там были docker файлы для исполняемых приложений. Были еще yaml-пайплайны для CI:

  • build - который создавал билд-артефакт
  • test - который ранал unit и intergation-тесты
  • nuget - который паблишил в Feed пакеты сервиса, у нас для каждого сервиса есть Client SKD пакеты например.
  • Были еще файлы yaml-пайплайнов для CD для всех энвайронментов вплоть до Production в отдельной папке.
  • Также были еще примеры IaC (Infrastructure as Code) скриптов для нового сервиса. Мы используем на проекте Terraform скрипты.
  • Были два проекта с примерами тестов: проект с unit тестами и проект с integrations тестами, включая api-тесты.

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