- PVSM.RU - https://www.pvsm.ru -
Простой вопрос: делая задачу, касающуюся API - вы чаще работаете с одним эндпоинтом, или пишите, условные, репозитории, которые используются сразу в нескольких эндпоинтах? Скорее всего, первое, тогда почему мы разбиваем проект по слоям, а не по фичам (эндпоинтам)?
Это видно в часто используемых нынче архитектурных подходах: Layered, Clean Architecture, Onion, и так далее. Не буду выделять что-то конкретное и объясню общую разницу в подходах:
Vertical Slice Architecture (VSA) строится вокруг каждого отдельного feature-слайса (эндпоинта, как самый простой пример), а не на вокруг слоев.
То есть, если код относится к конкретному эндпоинту, мы не размазываем его по всему проекту в папках Commands/Services/Repositories/DTOs и т.п., а кладем в одно место, там где и будет находиться эндпоинт. На картинке это выглядит так:
Вопрос, ответ на который я вижу довольно редко в статьях об архитектурах. Не будем плодить карго-культ "потому что так все делают" и ответим:
Главное преимущество VSA - продуктивность и минимум оверхэда, выраженного в бесконечной навигации по коду: все в одном месте, без беготни по проекту, лишних абстракций (а они вам вот все действительно нужны?), тысяч файлов, с минимальным пересечением логики.
Принцип KISS [1] хорошо выполняется и позволяет быстро разобраться - не обязательно понимать все тонкости архитектуры проекта, прежде чем начинать писать код.
От огромных сервисов то мы ушли, только вот теперь скроллим не тысячи строк когда, а сотни папок.
При этом, основные преимущества более привычных подходов сохраняются: модульность (которая, на мой взгляд, в VSA выражена даже лучше), отсутствие огромных сервисов, легкость тестирования и т.п.
Данный подход довольно новый (опустим про хорошо забытое старое - я уверен многие сами его давно используют неосознанно), даже на хабре сложно найти авторскую статью. В 2018 году, например, о нем написал Jimmy Bogard [2], автор всем нам знакомого MediatR и AutoMapper, а теперь уже и тренинги по VSA проводит.
Как знать, может через несколько лет и очередной eShop [3] попробует данную архитектуру.
Поскольку архитектура довольно новая, устоявшихся названий и понятий еще нет, поэтому код ниже не стоит воспринимать как единственно-верный вариант
Также буду использовать Minimal API (но никто не мешает пользоваться контроллерами)
Кто еще не успел познакомится этим нововведением dotnet - не помешает. Это не просто желание "мы тоже хотим писать на c# API в 4 строчки кода", а действительно хороший инструмент, который призван заменить устаревшие контроллеры. На хабре мало инфы, если данная статья зайдет, напишу о том, как использовать Minimal API в реальном проекте с кучей инфраструктуры, а не просто Hello World
Структура будет выглядеть следующим образом:
Program.cs
Endpoints (или, как часто называют, Features)
| User
| GetUserInfo.cs
| UserInfoResponse.cs
GetUserInfo.cs:
public class GetUserInfo : IEndpoint
{
public void Register(IEndpointRouteBuilder endpointsBuilder)
{
// Регистрация эндпоинта. В случае контроллера это был бы стандартный метод с атрибутом [HttpGet("user/info")].
endpointsBuilder.MapGet("/user/info", HandleAsync)
.RequireAuthorization()
.WithDescription("Get user basic info")
.WithTags("User");
}
// Параметры этого метода почти аналогичны параметрам метода контроллера, DI зарезолвит все необходимые зависимости.
public static Task<UserInfoResponse> HandleAsync(ExampleDbContext db, UserContext userContext, CancellationToken ct)
{
return db.Users.Where(x => x.Id == userContext.UserId)
.Select(x => new UserInfoResponse
{
UserId = x.Id,
Username = x.Username,
})
.SingleAsync(ct);
}
}
Где в HandleAsync() - вся необходимая логика запроса. Этот метод сделан публичным для возможности тестирования, об этом чуть ниже.
Такая наглядность позволит подпускать к проекту не только сеньоров с солидным архитектурным бэкграундом, но и разработчиков с опытом поменьше. Да и для вас самих GitHub Copilot, или чем вы пользуетесь, сможет банально лучше анализировать весь нужный код т.к. будет иметь более полный контекст.
IEndpoint - просто интерфейс для удобства регистрации эндпоинта Minimal API с единственным методом Register(): в отличии от контроллеров, магической рефлексии из коробки нет. Подробнее, как я говорил, стоит разобрать в отдельной статье.
Можно заметить, что в описанной мной структуре UserInfoResponse.cs находится внутри GetUserInfo.cs: если что, да, так можно делать, просто перетащив файл в другой в IDE. В .csproj для этого появится соответствующая запись с DependentUpon. Парой абзацев ниже скриншот для примера.
Но если не нравится - можно создать папку GetUserInfo и сложить в нее все файлы, относящиеся к данному эндпоинту (и сам эндпоинт)
Кто не увидел MediatR - не беспокойтесь - ничто не мешает использовать его с паттерном CQRS в данной схеме. Все необходимые файлы просто лягут под EndpointName.cs, а не размажутся тонким слоем по проекту.
Ниже я оставлю ссылки на репозитории, делают именно так. Или же, как можно заметить в этих репозиториях, кто-то вообще создает эти классы прямо внутри файла EndpointName.cs. А почему бы и нет? Несколько классов в одном файле - это, конечно, вполне себе антиппатерн. Но как и всегда в таких случаях, это не значит что это автоматически "плохо". Это плохо только если не понимаешь, почему обычно так не делают.
Однако, я бы задумался: действительно ли нужен MediatR с таким подходом, особенно учитывая что он становится платным [4]? Проблему сервисов на тысячи строк мы решаем и без него. Это просто еще один аргумент [5] против его бездумного использования.
Я хоть и намеренно противопоставил VSA другим подходам, на практике же они совместимы во многих аспектах: хоть немного сложный проект быстро выйдет за пределы набора API эндпоинтов. В конце статьи я привел примеры, которые вполне сочетают в себе эти подходы.
Вообще, принципы DDD хорошо работают с VSA: у нас все еще остается большое количество кода, прежде всего инфраструктурного, который хорошо ложится на эту модель: разумеется, не надо тащить в каждый эндпоинт общую для проекта обработку ошибок/транзакции/валидацию и т.п.: это все так и продолжит жить в своей области.
Тут все просто - вызываем наш HandleAsync, передаем нужные параметры и моки сервисов.
Может, не так красиво, как с использованием DI и MediatR, но зато при изменении эндпоинта больше вероятность отловить ошибку в тесте на этапе компиляции.
Если хочется красиво и с вызовами API - можно так [6].
Конечно, большинство проектов не состоит из одних только эндпоинтов: у нас есть очереди, job'ы и т.п. Но ничто не мешает использовать там такой же подход. Более того: я думаю многие заметят, что они сами, осознанно или нет, уже так делают. А если нет - время задуматься.
Ну и в целом, такой подход легко выходит не только за пределы не только бэкенда (Feature-Sliced Design [7] - пример из мира фронтэнда), но и кода вообще: мы же, например, не разбиваем команды в проекте по принципу "эта команда пишет контроллеры, а эта - хэндлеры".
Вполне логично, что VSA может работать и так и так. И такая архитектура проще разбивается на микросервисы: ведь мы выделяем их не по слоям "это у нас микросервис Domain", а по фичам. Может, с таким подходом "у нас монолит, но мы переходим на микросервисы" в каждой второй компании было бы чуть меньше.
На заглавной картинке видно, как слайс проходит прямо по базе данных. Кто-то может сделать вывод, что Entity/модели, представляющие таблицы в БД, тоже нужно ограничивать областью слайса.
Если у вас на проекте подход Code-first [8] (или просто полная репрезентация БД в коде), не соглашусь: это все еще должен быть отдельный слой. Если же вы пишите DTO для данных из базы под конкретный эндпоинт - этот DTO прекрасно ляжет в слайс.
Аналогично, если используете паттерн репозитория, тут, конечно, зависит от вашей реализации: если метод репозитория используется в одном-единственном эндпоинте/группе эндпоинтов, и его можно выделить не в ущерб тестированию, то, вероятно, так и стоит сделать: общий принцип - не держать код конкретной фичи вне ее пределов.
На хабре уже была статья-перевод [9], вдохновленная VSA, только с группированием больше по целым модулям, а не конкретным эндпоинтам. Для наглядности, приведу пример структуры оттуда:
WebApplication
│ appsettings.json
│ Program.cs
│ WebApplication.csproj
│
├───Modules
│ └───Orders
│ │ OrdersModule.cs
│ ├───Models
│ │ Order.cs
│ └───Endpoints
│ GetOrders.cs
│ PostOrder.cs
Слайс тут - не отдельный эндпоинт, а группа. Принципиально подход не меняется. Такой подход предлагал небезызвестный Роберт Мартин и назвал это Screaming Architecture [10]. Главная идея тут - что архитектура "кричит": по структуре проекта мы можем понять не просто, какой архитектурный подход использовался, а и то, какой функционал выполняет данный проект.
Что если у двух эндпоинтов какой-то общий между собой код? Использовать сервисы/хэлперы никто не запрещает. Поместить их можно рядом с эндпоинтами: зачем выносить уникальный для них код куда-то за их пределы. Ну или, вот [11] статья на эту тему для вдохновения.
Или, если эндпоинты почти идентичны (например, из-за версионирования), может совсем объединить их в один файл.
Однако, не забываем, что принцип DRY [12], конечно, важен, но 3 строчки одинакового кода - еще не повод пересмотреть архитектуру проекта - все равно она не будет идеальной. Даже если кажется что будет.
В целом, если в проекте большое количество внутренних связей, хитрых транзакций и т.п., которые тяжело разбить по фичам, такая архитектура может выглядеть уже не так красиво. Впрочем, как и любая другая архитектура в неподходящем для нее контексте.
Главное, что хочется отметить: при проектировании нужно думать не о том, как написали вон в той авторитетной статье/книге, а головой.
Напоследок, поскольку я привел только весьма упрощенный пример, а не инструкцию к действию, несколько проектов VSA на GitHub:
ContosoUniversity [13] от Jimmy Bogard. Давненько не обновлялся, но как реф подойдет.
Еще пример VSA [14] со своей статьей [15]
Food Delivery Microservices [16] - work in progress, но общие концепции можно почерпнуть.
Автор: Espleth
Источник [17]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/c-2/422041
Ссылки в тексте:
[1] KISS: https://en.wikipedia.org/wiki/KISS_principle
[2] Jimmy Bogard: https://www.jimmybogard.com/vertical-slice-architecture/
[3] eShop: https://github.com/orgs/dotnet-architecture/repositories?q=eshop
[4] становится платным: https://www.jimmybogard.com/automapper-and-mediatr-going-commercial/
[5] еще один аргумент: https://habr.com/ru/articles/686278/
[6] так: https://timdeschryver.dev/blog/how-to-test-your-csharp-web-api
[7] Feature-Sliced Design: https://feature-sliced.github.io/documentation/
[8] Code-first: https://builtin.com/articles/code-first-vs-database-first-approach
[9] статья-перевод: https://habr.com/ru/companies/skbkontur/articles/723840/
[10] Screaming Architecture: https://blog.cleancoder.com/uncle-bob/2011/09/30/Screaming-Architecture.html
[11] вот: https://habr.com/ru/articles/547746/
[12] DRY: https://en.wikipedia.org/wiki/Don%27t_repeat_yourself
[13] ContosoUniversity: https://github.com/jbogard/ContosoUniversityDotNetCore-Pages?ref=jimmybogard.com
[14] Еще пример VSA: https://github.com/nadirbad/VerticalSliceArchitecture
[15] статьей: https://nadirbad.dev/vertical-slice-architecture-dotnet
[16] Food Delivery Microservices: https://github.com/mehdihadeli/food-delivery-microservices/tree/main
[17] Источник: https://habr.com/ru/articles/916704/?utm_source=habrahabr&utm_medium=rss&utm_campaign=916704
Нажмите здесь для печати.