N-tier, Clean Architecture и Vertical Slice: практическое сравнение для .NET

—

от автора

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

Фото обложки: Anirban Sengupta, Unsplash

Фото обложки: Anirban Sengupta, Unsplash

Большинство .NET-разработчиков знакомятся с этими тремя архитектурами в одном и том же порядке: N-tier — в первых проектах, Clean Architecture — когда её приносит команда или шаблон, и Vertical Slice — когда о ней упоминают на код-ревью или в докладе на конференции. Каждую из них подают как улучшение предыдущей. На практике они отвечают на несколько разные вопросы, и правильный выбор зависит скорее от проекта, чем от того, какая из них новее.

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

Зачем вообще нужна архитектура

Архитектура — это набор правил о трёх вещах: где лежит код, какие части от каких могут зависеть и насколько дорого будет что-то изменить в будущем.

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

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

Многоуровневая архитектура (N-tier)

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

  • Presentation / API — контроллеры, модели запросов и ответов

  • Business Logic Layer (BLL) — сервисы, реализующие правила предметной области

  • Data Access Layer (DAL) — контекст базы данных, сущности и репозитории

Каждый слой зависит только от нижележащего. API вызывает BLL, BLL вызывает DAL, а DAL обращается к базе данных.

MyApp.sln├── MyApp.API│   └── Controllers/│       └── OrdersController.cs├── MyApp.BLL│   ├── Interfaces/│   │   └── IOrderService.cs│   └── Services/│       └── OrderService.cs└── MyApp.DAL    ├── AppDbContext.cs    ├── Entities/    │   └── Order.cs    ├── Interfaces/    │   ├── IOrderRepository.cs    │   └── IUnitOfWork.cs    ├── Repositories/    │   └── OrderRepository.cs    └── UnitOfWork.cs

В .NET такой подход обычно сочетают с Entity Framework, паттерном Repository и классом Unit of Work. Некоторые разработчики считают два последних избыточными, поскольку DbContext уже работает как Unit of Work, а DbSet<T> — как репозиторий. Тем не менее команды часто сохраняют эти обёртки — ради единообразия между проектами и чтобы слой данных было проще подменять в тестах.

Замечание о терминологии: строго говоря, tier — это физическая граница развёртывания, а layer — логическая. По этой причине в документации Microsoft используется термин «N-Layer». В повседневной .NET-практике эти термины взаимозаменяемы, и статья следует этому соглашению.

Clean Architecture

Clean Architecture сохраняет идею слоёв, но меняет направление зависимостей на противоположное. Вместо того чтобы бизнес-логика зависела от доступа к данным, всё зависит от бизнес-логики.

  • Domain — сущности и ключевые бизнес-правила, без внешних зависимостей

  • Application — сценарии использования (use cases), интерфейсы и DTO; зависит только от Domain

  • Infrastructure — база данных, файловое хранилище, внешние сервисы; реализует интерфейсы, объявленные в Application

  • API / Presentation — точка входа; связывает всё воедино через внедрение зависимостей

Главное правило — зависимости направлены внутрь. Проект Domain ничего не знает об Entity Framework, ASP.NET Core или какой-либо базе данных.

У этого подхода было несколько названий. Алистер Кокберн описал гексагональную архитектуру, также известную как Ports and Adapters, в 2005 году. Джеффри Палермо описал луковую архитектуру (Onion Architecture) в 2008 году. Роберт Мартин популяризировал название Clean Architecture в 2012 году. Руководство по архитектуре от Microsoft рассматривает все эти варианты как одну и ту же идею под разными именами, и различаются они в основном тем, как их рисуют на схемах.

В .NET Clean Architecture очень часто используют вместе с CQRS (Command Query Responsibility Segregation) и библиотекой MediatR. Каждая операция становится либо командой, которая изменяет состояние, либо запросом, который его читает, и у каждой есть собственный обработчик.

MyApp.sln├── MyApp.Domain│   └── Orders/│       └── Order.cs├── MyApp.Application│   ├── Common/│   │   └── Interfaces/│   │       └── IAppDbContext.cs│   └── Orders/│       ├── Commands/│       │   └── CreateOrder/│       │       ├── CreateOrderCommand.cs│       │       ├── CreateOrderCommandHandler.cs│       │       └── CreateOrderCommandValidator.cs│       └── Queries/│           └── GetOrderById/│               ├── GetOrderByIdQuery.cs│               ├── GetOrderByIdQueryHandler.cs│               └── OrderDto.cs├── MyApp.Infrastructure│   └── Persistence/│       └── AppDbContext.cs└── MyApp.API    └── Controllers/        └── OrdersController.cs

Одна команда и её обработчик выглядят так:

public record CreateOrderCommand(int CustomerId, decimal Amount) : IRequest<int>;public class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand, int>{    private readonly IAppDbContext _context;    public CreateOrderCommandHandler(IAppDbContext context) => _context = context;    public async Task<int> Handle(CreateOrderCommand request, CancellationToken cancellationToken)    {        var order = new Order(request.CustomerId, request.Amount);        _context.Orders.Add(order);        await _context.SaveChangesAsync(cancellationToken);        return order.Id;    }}

Контроллер отправляет команду, не зная, кто её обработает:

[HttpPost]public async Task<int> Create(CreateOrderCommand command) => await _mediator.Send(command);

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

Vertical Slice

Архитектура вертикальных срезов (Vertical Slice), популяризированная Джимми Богардом, меняет не направление зависимостей, а ось организации кода.

И в N-tier, и в Clean Architecture верхний уровень структуры — это набор слоёв, а каждая фича распределена между ними. Создание заказа затрагивает контроллер в одном проекте, обработчик или сервис в другом и сущность в третьем. Vertical Slice транспонирует эту структуру: верхний уровень — это набор фич, и каждая содержит всё, что ей нужно. Для тех, кто работает с базами данных, это тот же приём, что и PIVOT: строки становятся столбцами.

MyApp.sln└── MyApp    ├── Features/    │   └── Orders/    │       ├── CreateOrder.cs    │       └── GetOrderById.cs    ├── Data/    │   └── AppDbContext.cs    └── Program.cs

Срез часто хранит запрос, обработчик, валидацию и эндпоинт в одном файле:

public static class CreateOrder{    public record Command(int CustomerId, decimal Amount);    public static void MapEndpoint(IEndpointRouteBuilder app) =>        app.MapPost("/orders", Handle);    private static async Task<int> Handle(Command command, AppDbContext db)    {        var order = new Order(command.CustomerId, command.Amount);        db.Orders.Add(order);        await db.SaveChangesAsync();        return order.Id;    }}

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

По духу Vertical Slice близка к тому, как CQRS обычно применяется внутри Clean Architecture, где каждая команда или запрос уже лежит в отдельной папке. Разница в том, что Vertical Slice делает основной единицей всего проекта фичу, а не слой.

Что их объединяет и чем они различаются

Все три разделяют ответственность, все три работают с внедрением зависимостей и все три можно использовать с Entity Framework. Различия — в том, что считается основной единицей структуры и насколько строго соблюдаются границы.

N-tier

Clean Architecture

Vertical Slice

Основная единица организации

Технический слой

Технический слой вокруг ядра предметной области

Фича

Направление зависимостей

Сверху вниз

Внутрь, к домену

Внутри каждого среза

Типичное число проектов

3

4 и больше

1–2

Где живёт одна фича

Во всех слоях

Во всех слоях, в большем числе папок

В одной папке или файле

Бизнес-логика зависит от БД

Да

Нет

Зависит от среза

Объём кода на фичу

Низкий–средний

Высокий

Низкий–средний

Порог входа

Низкий

Средний–высокий

Низкий–средний

Типичное сочетание

EF + Repository + Unit of Work

CQRS + MediatR

Обработчики в стиле CQRS, Minimal API

Плюсы и минусы

N-tier

Плюсы

  • Прост для понимания и быстрого старта

  • Знаком практически каждому .NET-разработчику

  • Хорошо подходит для CRUD-приложений и проектов со сжатыми сроками

  • Мало проектов и папок для навигации

Минусы

  • Бизнес-логика зависит от деталей доступа к данным

  • Модульное тестирование бизнес-логики может требовать базы данных или обширных моков

  • По мере роста проекта BLL и DAL становятся тесно связанными

  • Крупные классы сервисов со временем обрастают множеством несвязанных методов

Clean Architecture

Плюсы

  • Домен независим от фреймворков и инфраструктуры

  • Бизнес-логику легко покрывать модульными тестами

  • Инфраструктуру можно заменить с ограниченным влиянием на остальную систему

  • Чёткие, проверяемые правила, которые масштабируются на большие команды

  • Наиболее узнаваемая структура в актуальных .NET-вакансиях

Минусы

  • Значительно больше файлов и проектов на одну фичу

  • Более высокие затраты на начальную настройку

  • Чтобы проследить один запрос по коду, нужно больше переходов

  • Может быть тяжелее необходимого для небольших приложений или быстро меняющихся требований

Vertical Slice

Плюсы

  • Весь код фичи находится в одном месте

  • Добавление или удаление фичи редко затрагивает другие

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

  • Хорошо сочетается с Minimal API

Минусы

  • Менее предписывающая, поэтому единообразие зависит от дисциплины команды

  • Общие бизнес-правила могут дублироваться в разных срезах

  • Меньше устоявшихся шаблонов и соглашений, чем у Clean Architecture

  • Менее знакома многим разработчикам и интервьюерам

Проекты, которым не подходит ни одна из них

Не у каждого приложения есть предметная область, которую стоит защищать. Интеграционный сервис, который получает результаты от внешнего API, преобразует их и передаёт дальше, ближе к паттерну «Адаптер», чем к любой из этих архитектур. Его основная задача — преобразование между двумя форматами, и разнесение этого преобразования по трём-четырём проектам мало что даёт.

То же касается консольных инструментов, задач по расписанию и небольших внутренних утилит. Разумный вариант по умолчанию для них — простая структура, а архитектура вводится только тогда, когда проект до неё дорастает.

Практические заметки

Несколько наблюдений, которые редко попадают на архитектурные схемы, но важны в повседневной работе.

Навигация действительно стоит времени. В решении на Clean Architecture одна фича может быть разнесена по четырём проектам и нескольким вложенным папкам. Файлы легко найти поиском, но переходить между ними медленнее. В Visual Studio опция Track Active Item in Solution Explorer («Отслеживать активный элемент в обозревателе решений»; Tools → Options → Projects and Solutions → General) заставляет Solution Explorer следовать за файлом, открытым в редакторе. Для разового перехода то же самое делает Sync with Active Document (Ctrl + [, S).

Шаблонный код стал дешевле. Самым частым практическим возражением против CQRS в связке с Clean Architecture был объём повторяющегося кода: класс команды, затем обработчик, затем валидатор, затем DTO — часто создаваемые копированием существующего набора с последующим переименованием. Ассистенты на базе ИИ теперь надёжно генерируют большую часть этого кода, что снимает значительную долю издержек, из-за которых подход ещё несколько лет назад казался медленным.

Выигрыш в тестировании неравномерен. Поскольку фичи и бизнес-правила изолированы, модульные тесты в Clean Architecture и Vertical Slice писать проще, чем в тесно связанном N-tier-проекте. Интеграционные тесты требуют примерно одинаковых усилий во всех трёх, поскольку в любом случае проходят через API, как бы ни был организован код за ним.

Частота изменений имеет значение. Проекты со сжатыми сроками и часто меняющимися требованиями сильнее всего ощущают дополнительную структуру Clean Architecture, поскольку каждое изменение затрагивает больше файлов. Проекты со стабильной сложной предметной областью и долгим ожидаемым сроком жизни выигрывают от неё больше всего.

Рынок

Каковы бы ни были технические компромиссы, Clean Architecture сегодня — вариант по умолчанию в .NET-вакансиях, на собеседованиях и в шаблонах проектов. Разработчики, приходящие в профессию сейчас, часто изучают её первой, и многие команды используют её как стандартную структуру для новых сервисов.

Ситуация похожа на фронтенд-разработку, где React — далеко не единственный хороший вариант, но именно его чаще всего требуют работодатели. Хорошее понимание Clean Architecture полезно для карьеры .NET-разработчика независимо от того, какую структуру в итоге использует конкретный проект.

Как выбирать

Универсально правильного ответа нет, но несколько вопросов сужают выбор:

  • Сколько проживёт этот код? Недолговечные или экспериментальные проекты тяготеют к N-tier или Vertical Slice. Долгоживущие системы со сложными правилами — к Clean Architecture.

  • Как часто будут меняться требования? Частые изменения вознаграждают меньший объём церемоний на фичу.

  • Насколько велика команда? Большим командам полезны строгие и общеизвестные границы Clean Architecture.

  • Что команда уже знает? Знакомая архитектура, применяемая последовательно, обычно лучше незнакомой, применяемой частично.

Все три — рабочие способы построить .NET-приложение. Самое важное решение — выбрать одну осознанно и применять её последовательно.


Это перевод моей статьи, впервые опубликованной на Medium.

Какую архитектуру использует ваша команда — и был ли это осознанный выбор или шаблон, который пришёл вместе с проектом? Интересно узнать, как это сработало на практике. 👇

ссылка на оригинал статьи https://habr.com/ru/articles/1089808/