Как устроена зависимость слоев в классической трехслойной архитектуре
Что проверяют
Знаете ли вы, куда направлены зависимости в классической схеме и чем она отличается от луковой или чистой архитектуры. Следом обычно спрашивают: «Если поменять базу, какие проекты придётся трогать?»
Как отвечать
Три слоя, обычно три проекта в решении:
- Presentation: контроллеры ASP.NET Core или Minimal API, Blazor, WPF. Принимает ввод, отдаёт ответ;
- Business Logic Layer: сервисы с правилами предметной области, валидация, оркестрация операций;
- Data Access Layer:
DbContextиз EF Core или Dapper, репозитории, работа с базой.
В классике зависимости идут строго сверху вниз: Presentation ссылается на BLL, BLL на DAL. Нижний слой ничего не знает о верхних, а слои не перепрыгиваются: контроллер не ходит в DbContext напрямую. В C# это закрепляется ссылками между проектами, и циклическую ссылку сборка просто не допустит.
Слабое место классики: BLL зависит от DAL, то есть правила предметной области компилируются против деталей хранения. Лечится инверсией зависимостей: интерфейс IOrderRepository объявляется в BLL, реализация живёт в DAL, и стрелка разворачивается. Это уже луковая (Onion) или чистая (Clean) архитектура, её стоит назвать своим именем.
Собирает всё composition root в Program.cs: через builder.Services.AddScoped<IOrderService, OrderService>() регистрируются реализации, и только этот проект знает про все слои. Между слоями данные передают через DTO, чтобы сущность EF не утекла в ответ API.
За направлением следят архитектурные тесты (NetArchTest, ArchUnitNET): они падают, если контроллер начал использовать DAL.
Где спотыкаются
Выдают инверсию зависимостей за свойство классической трёхслойки. Интервьюер спросит, в каком проекте лежит интерфейс репозитория, и путаница вскроется.
Путают слои и уровни (layers и tiers). Слои это логическое деление кода, уровни это физическое размещение: клиент, сервер приложений, СУБД на разных машинах.
Не замечают протечку через транзитивные ссылки: в проектах SDK-стиля Presentation видит типы DAL через BLL, и обращение к DbContext из контроллера компилируется без ошибок. Запрет держится на ревью или архитектурных тестах.