Clean Architecture
Общее
Clean Architecture («чистая архитектура») — архитектурный стиль, канонически описанный Робертом Мартином (Robert C. Martin, известным как Uncle Bob): система разделяется на концентрические слои, в центре которых находится бизнес-логика, а всё остальное — фреймворки, пользовательский интерфейс, базы данных, внешние сервисы — выносится на периферию и объявляется «деталями». Единственное и главное правило стиля — правило зависимостей (The Dependency Rule): все зависимости исходного кода направлены строго внутрь, от внешних слоёв к внутренним. Внутренние слои не знают ничего о внешних — ни имён классов, ни функций, ни типов. Это правило превращает «чистую архитектуру» из набора диаграмм в проверяемое свойство кодовой базы: направление зависимостей можно измерить статическим анализом и сломать сборку при нарушении.
Стиль был представлен в блог-посте Мартина The Clean Architecture (2012), а затем развёрнут в книгу Clean Architecture: A Craftsman’s Guide to Software Structure and Design (2017). Важно понимать, что Мартин не претендовал на новизну идеи — наоборот, пост начинается с перечисления предшественников и признания, что все они «суть вариации одной темы». Ценность вклада Мартина в другом: он собрал разрозненные подходы под единым словарём, дал самой идее продающее имя и связал её с системой принципов SOLID, показав, что архитектура приложения — это те же принципы проектирования, применённые не к классу, а к системе целиком.
Исторические предшественники стиля хорошо документированы:
| Подход | Автор | Год | Ключевая идея |
|---|---|---|---|
| BCE (Boundary-Control-Entity) | Ивар Якобсон (Ivar Jacobson) | 1992 | Разделение на границы (интерфейсные объекты), управления и сущностей; из книги Object-Oriented Software Engineering |
| Hexagonal Architecture (Ports & Adapters) | Алистер Коубёрн (Alistair Cockburn) | 2005 | Приложение как гексагон с «портами»; всё внешнее подключается через «адаптеры» |
| Onion Architecture | Джеффри Палермо (Jeffrey Palermo) | 2008 | Слои «луковицы»: домен в центре, зависимости направлены внутрь |
| DCI (Data-Context-Interaction) | Джеймс Коплиен и Трюгве Реенскоуг | 2009 | Разделение системной логики и доменной модели |
| Clean Architecture | Роберт Мартин | 2012 / 2017 | Синтез перечисленных: четыре кольца и правило зависимостей |
Общая для всех предшественников мысль восходит ещё к Ивару Якобсону: в любой системе есть ядро, реализующее бизнес-правила, и периферия, доставляющая эти правила пользователю, — и периферия не должна просачиваться в ядро. Коубёрн в эссе о гексагональной архитектуре (идея зрела у него с начала 2000-х, опубликована в 2005 году) сформулировал мотивацию предельно практично: бизнес-логика не должна зависеть от способа доставки — её нужно уметь вызывать одинаково из HTTP-запроса, из консольной утилиты и из автоматического теста. «Порты» — это интерфейсы приложения, «адаптеры» — конкретные технологии их реализации. Палермо в 2008 году переупаковал ту же идею в виде концентрических слоёв «луковицы», сфокусировавшись на объектно-ориентированной разработке корпоративных приложений и впервые явно запретив зависимость домена от инфраструктурных библиотек. Подход DCI добавил к этому спектру акцент на разделении «что система есть» (доменные объекты) и «что система делает» (сценарии взаимодействия).
Мартин в посте 2012 года честно признал родство всех этих подходов и предложил свой синтез: схему из четырёх концентрических колец (Entities → Use Cases → Interface Adapters → Frameworks & Drivers), единое правило зависимостей и развёрнутое обоснование, почему именно такое разделение делает систему тестируемой, независимой от фреймворков и долговечной. Книга 2017 года закрепила синтез, добавив главы-манифесты «The Database Is a Detail» («база данных — это деталь») и «The Web Is a Detail» и связав архитектуру с ранее описанными Мартином принципами компонентов и SOLID.
В эволюции архитектурной мысли Clean Architecture занимает место прямого «наследника» SOLID на уровне приложения: если принципы SOLID управляют зависимостями между классами, а принципы компонентов — между модулями, то Clean Architecture применяет те же механизмы (прежде всего инверсию зависимостей) к границам всей системы. Отсюда и место стиля в этой базе знаний: это самая цитируемая и самая спорная рамка для организации кода приложения — цитаемая, потому что даёт конкретный ответ на вопрос «как структурировать код», и спорная, потому что цена этого ответа заметна не всем и не всегда оправдана.
Отдельно стоит сказать о популярности стиля. В 2010-е Clean Architecture из книги превратилась в индустриальный стандарт de facto: для Android она в связке с MVP/MVVM стала, пожалуй, самым тиражируемым шаблоном мобильной разработки; для бэкенда на Java/Kotlin/C# появились канонические структуры проекта, генераторы и курсы, а вопрос «расскажите про слои Clean Architecture» закрепился в собесах. Такой масштаб внедрения — одновременно триумф и проблема идеи: стиль применяют и там, где он меняет жизнь проекта к лучшему, и там, где он превращается в карго-культ из папок с именами domain, data и presentation без понимания, зачем они. Мартин — фигура полемичная, и книга написана характерным проповедническим тоном; зрелое чтение отделяет механизм (правило зависимостей — точный, проверяемый и полезный) от риторики, а решение о глубине применения принимает инженер применительно к своей системе, а не доктрина.
Слои и правило зависимостей
Clean Architecture описывается схемой из четырёх концентрических колец. Внутренность каждого кольца ничего не знает о внешних кольцах; чем дальше от центра, тем более изменчивым и «технологичным» становится код.
┌────────────────────────────────────────────────────────┐
│ Frameworks & Drivers │
│ (база данных, Web, UI, внешние API) │
│ │
│ ┌────────────────────────────────────────────────┐ │
│ │ Interface Adapters │ │
│ │ (Controllers, Presenters, Gateways) │ │
│ │ │ │
│ │ ┌────────────────────────────────────────┐ │ │
│ │ │ Use Cases │ │ │
│ │ │ (Interactors, сценарии приложения) │ │ │
│ │ │ │ │ │
│ │ │ ┌────────────────────────────────┐ │ │ │
│ │ │ │ Entities │ │ │ │
│ │ │ │ (бизнес-правила предприятия) │ │ │ │
│ │ │ └────────────────────────────────┘ │ │ │
│ │ │ │ │ │
│ │ └────────────────────────────────────────┘ │ │
│ │ │ │
│ └────────────────────────────────────────────────┘ │
│ │
└────────────────────────────────────────────────────────┘
Назначение слоёв по Мартину:
| Слой | Официальное название в книге | Содержимое | Типичные представители |
|---|---|---|---|
| Entities | Enterprise business rules | Правила, общие для всего предприятия | Доменные сущности, бизнес-объекты с инвариантами |
| Use Cases | Application business rules | Правила конкретного приложения: сценарии использования | Interactors, request/response-модели, оркестрация сущностей |
| Interface Adapters | — | Преобразование данных между форматами use cases и внешнего мира | Controllers, Presenters, Gateways, DTO, модели представлений |
| Frameworks & Drivers | — | Детали доставки и хранения | Web-фреймворк, СУБД, UI, драйверы внешних сервисов, точка сборки Main |
Правило зависимостей (The Dependency Rule) Мартин формулирует коротко: зависимости исходного кода направлены только внутрь, к политикам более высокого уровня. Внутреннее кольцо не может знать ничего о внешнем: ни имени класса, ни функции, ни переменной, ни формата данных, определённых во внешнем кольце. Всё, что находится во внешних кольцах, — изменчивые детали: выбор СУБД, веб-фреймворка, протокола, устройства вывода. Всё, что во внутренних, — стабильные политики: бизнес-правила, меняющиеся по совершенно другим причинам и с совершенно другой частотой, чем технологии. Правило зависимостей выстраивает код вдоль этого градиента изменчивости: стабильное защищено от изменчивого, а не наоборот.
Здесь важно различить две вещи, которые новички в Clean Architecture часто путают: поток управления (flow of control) и поток зависимостей. Поток управления в рантайме вполне может идти наружу: интерактор (слой Use Cases) вызывает сохранение в базу данных (слой Frameworks & Drivers). Но зависимость в исходном коде при этом направлена внутрь: интерактор объявляет у себя интерфейс шлюза OrderGateway, ничего не зная о SQL, а конкретный SqlOrderGateway из внешнего слоя реализует этот интерфейс и, следовательно, зависит от внутреннего слоя. Вызов идёт наружу, зависимость — внутрь. Этот разрыв между направлением вызова и направлением зависимости и есть механизм инверсии зависимостей, применённый на архитектурном масштабе: граница между слоями проходит по интерфейсу, который принадлежит внутренней стороне.
Два уточнения, которые Мартин делает сам и которые снимают половину споров о стиле:
Количество колец — не догма. Четыре слоя — это удобная схема, а не предписание; в реальной системе их может быть больше или меньше. Имеет значение только правило зависимостей: где бы ни проходили границы, все зависимости пересекают их внутрь. Схема колец — просто наглядный способ объяснить, где границы обычно уместны.
Кольца — это не физические слои развёртывания. Clean Architecture ничего не говорит о том, монолитна система или распределена: это стиль организации кода, который одинаково применяется внутри монолита и внутри каждого отдельного микросервиса. Путаница с трёхуровневой архитектурой (presentation–business–data tiers) здесь вредна: N-tier описывает физическое разнесение по машинам и почти всегда вырождается в «UI → логика → SQL», где бизнес-слой зависит от схемы базы; Clean Architecture описывает направление зависимостей в коде и целенаправленно переворачивает эту зависимость.
И последнее уточнение — о цене. Границы в Clean Architecture не бесплатны: каждая полная граница — это интерфейс, две модели данных, маппинг между ними и точка компоновки. Мартин прямо пишет, что архитектура — это искусство проводить границы там, где они окупаются: там, где по разные стороны действительно разные причины изменения и разная частота изменений. Граница между доменом и доставкой данных почти всегда окупается; граница между двумя CRUD-экранами — почти никогда. Хороший архитектор управляет не только тем, где провести границы, но и тем, насколько полными их делать: полную (интерфейс + DTO + маппинг), частичную (общий интерфейс без раздельных моделей) или отложенную (просто отдельные пакеты с контролем зависимостей). Стиль диктует направление зависимостей, но глубину изоляции выбирает инженер.
Элементы архитектуры
Каждое кольцо — это не просто «папка для кода», а роль с чёткой зоной ответственности. Разберём роли по слоям.
Entities (сущности)
Entities — воплощение enterprise business rules: бизнес-правил, которые остаются верными для предприятия независимо от того, каким приложением они используются. Это ядро системы, самый стабильный и самый ценный код: Order, инвариант «заказ нельзя оплатить дважды», Loan с правилом расчёта процентов, Schedule с логикой пересечения интервалов. Сущность — не обязательно класс с поведением в духе ООП: Мартин допускает, что это может быть набор структур данных с функциями; важно лишь, что сущность инкапсулирует правила самого предприятия и не знает о существовании ни приложений, ни баз, ни интерфейсов.
Практический критерий: если правило переживёт смену приложения (вместо интернет-магазина появится B2B-портал), оно — кандидат в Entities; если умрёт вместе с приложением — это уровень Use Cases.
Use Cases (Interactors)
Use Cases — application business rules: правила, специфичные для конкретного приложения, его сценариев использования. Классическое название элемента — Interactor: класс, оркестрирующий один сценарий, названный по намерению пользователя — PlaceOrder, CancelSubscription, TransferMoney. Интерактор получает на вход простую request-модель, загружает нужные сущности через шлюзы, поручает им выполнить бизнес-правила, фиксирует результат и возвращает response-модель. Именно интеракторы — та точка, где Clean Architecture отвечает на вопрос «что делает система»: Мартин в посте Screaming Architecture (2011) формулирует принцип — при взгляде на структуру проекта должно быть видно, что он делает (варианты использования, домен), а не на чём он написан (фреймворк).
Ключевое свойство интерактора: он владеет границами. Интерфейс доступа к данным (OrderGateway), интерфейс уведомлений (Notifier) — интерактор (или соседний с ним модуль слоя Use Cases) объявляет их сам, в терминах своих потребностей, а внешние слои лишь реализуют их. Слой сценариев не зависит от инфраструктуры в принципе — она подставляется снаружи.
Interface Adapters (адаптеры интерфейсов)
Interface Adapters — слой перевода данных между форматом, удобным для use cases, и форматами внешнего мира. Роли внутри слоя:
- Controller — принимает ввод из внешнего мира (HTTP-запрос, событие очереди, нажатие кнопки), валидирует его на уровне транспорта и преобразует в request-модель интерактора, затем запускает сценарий. Контроллер не принимает бизнес-решений: вся его логика — перевод форматов.
- Presenter — принимает результат интерактора (response-модель) и готовит его к отображению: view model для шаблона, JSON для API, строку для консоли. Presenter отделяет формат вывода от содержания результата — благодаря ему сменить REST на gRPC или шаблон на SPA можно, не тронув сценарий.
- Gateway — реализация интерфейсов, объявленных внутренними слоями:
SqlOrderGatewayповерх ORM и СУБД,SmtpNotifierповерх почтового сервера,StripePaymentGatewayповерх платёжного API. Шлюз — это адаптер Коубёрна в чистом виде; в DDD-терминологии ему соответствует реализация репозитория.
Здесь же живут DTO, модели представлений и всё остальное «переводное» хозяйство. Классический MVC Мартин относит именно к этому слою: контроллер и представление — адаптеры, а не носители логики.
Frameworks & Drivers (фреймворки и драйверы)
Frameworks & Drivers — самый внешний слой: веб-фреймворк (роутеры, middleware), драйвер СУБД, UI-фреймворк, клиенты внешних API, устройства. Код здесь — по возможности «склеивающий»: тонкие обёртки над библиотеками, не содержащие бизнес-знаний. Отдельного упоминания заслуживает компонент Main — точка сборки приложения: именно здесь создаётся дерево объектов, интерфейсы связываются с реализациями (dependency injection), читается конфигурация и передаётся управление первому сценарию. Main — самый «грязный», детальный модуль системы, и Clean Architecture честно выносит его на самую периферию, оставляя право ничего не знать о нём всем остальным слоям.
Философия слоя выражается лозунгами Мартина «база данных — это деталь» и «веб — это деталь»: PostgreSQL или MongoDB, REST или gRPC — это решения, которые обязаны быть отложенными, заменимыми и не имеющими голоса в устройстве бизнес-логики.
Передача данных через границы
Границы слоёв пересекают не только вызовы, но и данные, и здесь Clean Architecture требует дисциплины, которую чаще всего нарушают:
- Request/Response-модели. Каждый интерактор определяет собственные входные и выходные структуры — простые, «глухие» объекты данных, описанные в терминах сценария (
PlaceOrderRequest,PlaceOrderResponse). Мартин прямо предостерегает от «жульничества»: через границу нельзя передавать ни доменные сущности, ни строки базы данных — иначе внутренний слой неявно привязывается к внешнему (к структуре entity или к схеме таблицы), и граница становится фикцией. - DTO (Data Transfer Object). Простые структуры данных, единственная обязанность которых — переносить данные через границу без поведения. Отдельный DTO на каждую сторону границы — тот самый источник маппинга, за который стиль ругают (см. «Минусы») и который является осознанной платой за изоляцию: если entity меняет внутреннее устройство, никакого потребителя за пределами домена это не касается.
- Boundary-интерфейсы. Каждая граница — это интерфейс, принадлежащий внутренней стороне: его объявление живёт рядом с интерактором, реализация — снаружи. Не «домен реализует интерфейс инфраструктуры», а наоборот. Множество таких интерфейсов — фактический публичный контракт внутреннего круга.
Вопрос, который возникает у каждой команды на второй неделе применения: можно ли переиспользовать request/response-модели между интеракторами? Соблазн велик — структура-то похожая. Правильный по умолчанию ответ: лучше дублировать, чем связывать. Общая модель двух сценариев означает, что любое изменение одного из них (добавить поле, поменять тип) невидимо для автора распространяется на другой — сценарии сращиваются через данные, хотя должны быть независимы. Дублирование «глухих» структур данных — самая дешёвая форма связности в системе; его не нужно бояться. Если третьему сценарию действительно нужен общий тип — это сигнал, что появилась сущность или правило предприятия, и его место во внутреннем кольце Entities.
Схема на компактном примере (C#, в духе книги):
// ── Слой Use Cases ────────────────────────────────────────────
// Request/Response-модели: простые структуры, термины сценария
public record PlaceOrderRequest(int CustomerId, IReadOnlyList<int> ItemIds);
public record PlaceOrderResponse(bool Success, string? Error, int? OrderId);
// Boundary-интерфейс объявлен здесь, в терминах сценария
public interface IOrderGateway
{
Order? Find(int orderId);
Customer? FindCustomer(int customerId);
void Save(Order order);
}
// Interactor: оркестрирует сценарий, ничего не знает о HTTP и SQL
public class PlaceOrder
{
private readonly IOrderGateway _gateway;
public PlaceOrder(IOrderGateway gateway) => _gateway = gateway;
public PlaceOrderResponse Execute(PlaceOrderRequest request)
{
var customer = _gateway.FindCustomer(request.CustomerId);
if (customer is null)
return new PlaceOrderResponse(false, "customer not found", null);
var order = customer.PlaceOrder(request.ItemIds); // бизнес-правила — в Entity
_gateway.Save(order);
return new PlaceOrderResponse(true, null, order.Id);
}
}
// ── Слой Interface Adapters / Frameworks ──────────────────────
// Реализация границы: зависит от use case, а не наоборот
public class SqlOrderGateway : IOrderGateway
{
private readonly AppDbContext _db; // ORM, соединения, SQL — всё здесь
public SqlOrderGateway(AppDbContext db) => _db = db;
public Order? Find(int orderId) => _db.Orders.Find(orderId);
public Customer? FindCustomer(int id) => _db.Customers.Find(id);
public void Save(Order order) => _db.Orders.Add(order);
}
// Controller: HTTP → request-модель, запуск сценария
[HttpPost("/orders")]
public IActionResult PlaceOrder([FromBody] PlaceOrderDto dto)
{
var result = _placeOrder.Execute(
new PlaceOrderRequest(dto.CustomerId, dto.ItemIds));
return result.Success
? Ok(new { orderId = result.OrderId })
: BadRequest(new { error = result.Error });
}
Обратите внимание на направление зависимостей в примере: SqlOrderGateway и контроллер импортируют типы слоя Use Cases; интерактор не импортирует ничего внешнего. PlaceOrder тестируется с заглушкой IOrderGateway за доли секунды — без сервера баз данных и веб-сервера.
Пример: путь запроса через слои
Чтобы кольца перестали быть абстракцией, проследим типичный сценарий «оформление заказа» от HTTP-запроса до ответа:
- Web-фреймворк (Frameworks & Drivers) принимает
POST /orders, разбирает HTTP, передаёт управление контроллеру. - Controller (Interface Adapters) проверяет транспортную корректность, собирает
PlaceOrderRequest(customerId, itemIds)и вызываетPlaceOrder.Execute(request)— интерактор. - Interactor (Use Cases) через
IOrderGatewayзагружаетCustomerи товары; обращается к методуcustomer.PlaceOrder(...)— бизнес-правила (лимит кредита, доступность товара, расчёт скидки) выполняются в Entities. - Интерактор фиксирует результат:
gateway.Save(order). Вызов уходит наружу — вSqlOrderGateway— но зависимость (интерфейсIOrderGateway) остаётся внутренней. - Интерактор возвращает
PlaceOrderResponse(true, null, 42)— глухую структуру без знания о HTTP. - Presenter (Interface Adapters) превращает response в view model — например, в JSON-ответ
{"orderId": 42}. - Web-фреймворк сериализует и отправляет ответ.
Тот же интерактор без единого изменения запускается из консольной команды (другой контроллер, другой презентер), из сообщения очереди и из юнит-теста — ровно это Коубёрн считал главным признаком здоровой архитектуры. Тест заслуживает отдельного взгляда: благодаря boundary-интерфейсу он не требует ни базы, ни веб-сервера, ни поднятия окружения:
// Юнит-тест интерактора: только бизнес-правила, доли миллисекунды
[Fact]
public void PlaceOrder_saves_order_for_valid_customer()
{
var gateway = new FakeOrderGateway(); // заглушка на IOrderGateway
var useCase = new PlaceOrder(gateway);
var result = useCase.Execute(new PlaceOrderRequest(42, new[] { 1, 2, 3 }));
Assert.True(result.Success);
Assert.Single(gateway.Saved); // заказ передан на сохранение
}
Такой тест проверяет ровно одно — поведение сценария, — выполняется за микросекунды и не ломается от смены версии СУБД. Именно на таких тестах строится «пирамида тестирования» в Clean Architecture: плотный быстрый слой юнит-тестов на интеракторы и сущности, умеренный слой интеграционных на шлюзы и тонкий слой сквозных на доставки.
Связь с SOLID, DDD и родственными стилями
SOLID как механизм
Clean Architecture — это не «ещё один набор правил» рядом с SOLID, а его прямое продолжение на масштабе приложения, и связь здесь не декоративная, а механическая:
- Dependency Inversion Principle — несущая конструкция. Само правило зависимостей реализуется исключительно DIP: внутренний слой объявляет абстракцию, внешний её реализует. Без инверсии зависимостей «зависимости направлены внутрь» невозможно технически — бизнес-логика неизбежно импортирует драйвер БД. Мартин в книге подчёркивает: DIP — это про защиту стабильных абстракций от изменчивых деталей; абстракции принадлежат внутренним слоям, детали — внешним.
- Single Responsibility Principle определяет, где проводить границы. Границы слоёв проводятся по причинам изменения: правила предприятия меняются при изменении бизнеса, сценарии — при изменении продукта, адаптеры — при смене технологий. SRP в масштабе архитектуры — это и есть разрезание системы по осям изменчивости.
- Open/Closed Principle — цель всей конструкции. Замена СУБД, веб-фреймворка или UI не должна требовать изменений домена: система открыта для расширения новыми деталями и закрыта для их влияния на ядро.
- Interface Segregation Principle виден в шлюзах: интерактор объявляет узкий интерфейс
IOrderGatewayс двумя нужными ему методами, а не принимает «большой» репозиторий с тридцатью.
Родство с DDD
Clean Architecture отлично стыкуется с Domain-Driven Design Эрика Эванса (Domain-Driven Design, 2003), хотя решает другую задачу: DDD отвечает на вопрос «как найти правильную модель предметной области», Clean Architecture — «как изолировать её от инфраструктуры». Соответствие ролей почти взаимное: Entities ≈ слой доменной модели DDD (сущности, агрегаты, объекты-значения); Use Cases ≈ прикладной слой (application services) DDD; Gateway ≈ репозиторий. Комбинация «тактические паттерны DDD внутри кольца Entities + правило зависимостей Clean Architecture» — распространённый и рабочий союз: DDD даёт содержание ядра, Clean Architecture — его защиту.
Отличия от Hexagonal и Onion
На уровне сути Hexagonal, Onion и Clean Architecture — одна и та же идея: домен в центре, детали снаружи, зависимости внутрь. Различия — в словаре и акцентах:
| Hexagonal (2005) | Onion (2008) | Clean (2012/2017) | |
|---|---|---|---|
| Ключевой образ | Гексагон с портами | Слои луковицы | Концентрические кольца |
| Ядро | Приложение (use cases + домен) | Доменные модели и сервисы | Entities (правила предприятия) |
| Сценарии использования | Внутри ядра | Отдельный внутренний слой | Отдельное кольцо Use Cases |
| Терминология границ | Порты и адаптеры | Интерфейсы и реализации | Boundary-интерфейсы, шлюзы |
| Акцент | Тестируемость и заменяемость доставки | Отделение от инфраструктуры (IoC, ORM) | Синтез + связь с SOLID |
Практический вывод: спорить «Hexagonal или Clean» бессмысленно — это вопрос словаря, а не дизайна. Инженер Херберту Граса в известном цикле статей о наведении порядка в архитектурах показал, как все перечисленные подходы складываются в одну схему: DDD для модели, порты и адаптеры для границ, Clean/Onion для организации слоёв. Разумная команда выбирает один словарь и фиксирует его в архитектурных решениях, а не смешивает терминологии.
Типичные ошибки при внедрении
Большинство неудач с Clean Architecture — не недостатки стиля, а ошибки его применения. Они настолько повторяются от проекта к проекту, что заслуживают отдельного списка.
Интерфейс на каждый класс. «Инверсия ради инверсии»: каждой реализации немедленно заводится интерфейс, даже если реализация одна, заменять её никто не собирается и точка изменения не просматривается. Абстракция без осмысленной изменчивости — это чистая цена (файл, косвенность, маппинг) без выгоды. DIP требует инверсии там, где проходит архитектурная граница, а не везде, где существует класс.
Бизнес-логика в контроллере или в ORM-сущности. Два зеркальных нарушения правила зависимостей. В первом случае сценарная логика живёт в слое адаптеров — она недоступна другим доставкам и не тестируется изолированно. Во втором — доменная сущность обвешана атрибутами конкретного ORM и знает о таблицах; смена хранилища мгновенно становится задачей всего домена. Оба варианта сохраняют папки со «слоями», но разрушают саму границу.
Протаскивание доменных сущностей через границы. «Жульничество», о котором предупреждает Мартин: интерактор возвращает entity, контроллер сериализует её в JSON. Экономия одного маппера оборачивается тем, что любое внутреннее изменение сущности ломает внешних потребителей — граница существует номинально, а фактически её нет.
God-use-case. Интерактор на пару тысяч строк, «инкапсулирующий» половину приложения: сценарий — это одно намерение пользователя, а не весь жизненный путь данных. Признак проблемы — название не отражает одно намерение пользователя (ProcessOrder вместо PlaceOrder, CancelOrder, ShipOrder). Лечится SRP: один сценарий — одно намерение.
Кольца как папки, а не как границы. Структура директорий domain/usecases/data/presentation создана, но зависимости никто не контролирует: интерактор импортирует ORM, сущность — HTTP-типы. Без автоматической проверки правила зависимостей (ArchUnit, NetArchTest, dependency-cruiser в CI) папки деградируют в косметику за пару спринтов.
Маппинг-паралич. Цепочки ручных мапперов, копирующих сорок полей из структуры в структуру, и мапперы мапперов поверх них. Изоляция слоёв не требует ручного копирования каждого поля: объектные мапперы (MapStruct, AutoMapper, Kotlin data class copy) и здравный смысл снижают цену там, где граница всё-таки нужна. Если же маппинга больше, чем логики, — это сигнал, что граница проведена зря.
Догматическое ревью. Ветирование любых отступлений от «канонических четырёх слоёв» превращает архитектуру из инструмента в религию: команда проводит недели на споры о том, куда положить класс, вместо доставки ценности. Индикатор здоровой практики — каждое архитектурное правило команды отвечает на вопрос «какую изменчивость мы изолируем», а не «что сказал Uncle Bob».
Плюсы
Тестируемость без инфраструктуры. Главный и самый ощутимый выигрыш. Домен и сценарии тестируются чистыми юнит-тестами: шлюзы подменяются заглушками, тесты не требуют СУБД, веб-сервера и сети — они быстрые, детерминированные и не «мигают» из-за окружения. Тесты становятся регрессионной сеткой именно для бизнес-правил — самого ценного и хрупкого кода, — а не для склеивающего кода фреймворка.
Независимость от фреймворков. Фреймворк используется как инструмент, а не как каркас, в который вписывается код: Spring, ASP.NET или Django — подключённая библиотека на периферии, а не центр системы. Мартин формулирует это жёстко: «архитектура не должна говорить о фреймворке». Это устраняет vendor lock-in на уровне кода и защищает от главного риска экосистемы фреймворков — вынужденных миграций из-за окончания поддержки.
Замена деталей без хирургии. Замена СУБД, протокола API или UI-фреймворка затрагивает внешние слои и точку сборки, не трогая ядро. На практике полная замена базы случается реже, чем обещают лекции, но частичные операции того же класса — смена ORM, разделение чтения и записи, вынос операции в очередь, версия API v2 рядом с v1 — происходят постоянно, и именно их стоимость Clean Architecture радикально снижает.
Долгоживущий домен. Бизнес-правила переживают поколения технологий: ядро, изолированное от деталей, переносится между платформами, переиспользуется в нескольких приложениях предприятия (внутренний портал и клиентское приложение делят одни Entities) и десятилетиями накапливает ценность вместо того, чтобы растворяться в Rails-магии конкретного года.
Параллельная работа команд. Границы слоёв и boundary-интерфейсы — это точки стыковки: команда домена развивает интеракторы и сущности, команда инфраструктуры — шлюзы и контроллеры, договорившись об интерфейсах заранее. Это применение закона Конвея в свою пользу: структура кода поддерживает структуру команды, а не конфликтует с ней.
Отложенные решения. Систему можно начать строить, ещё не выбрав СУБД и UI-фреймворк: внутренние кольца не зависят от этих решений, их можно принять позже, когда появится информация. Мартин считает откладывание решений одной из главных задач архитектора: архитектура — это то, что позволяет отложить необратимые выборы максимально долго и дёшево.
Исполнимость и проверяемость правила. В отличие от расплывчатых призывов к «слабой связности», правило зависимостей конкретно и машинно проверяемо инструментами вроде ArchUnit, NetArchTest или dependency-cruiser — оно становится fitness-функцией в CI, и деградация архитектуры блокируется автоматически, а не обнаруживается через три года.
Предсказуемая структура и дешёвый онбординг. Соглашение о четырёх кольцах интернационально: разработчик, знакомый с Clean Architecture, открывает любой проект на ней — от Android-приложения до бэкенда на C# — и мгновенно знает, где искать бизнес-правила (кольцо Entities), где сценарии (кольцо Use Cases) и где клеящий код (периферия). Снижается bus-factor, ускоряется вход новичков, а перемещение людей между проектами компании перестаёт требовать перепроектирования в голове. Та же предсказуемость помогает и в разговоре о коде: «это нарушение правила зависимостей» — точный, проверяемый аргумент в ревью, а не вопрос вкуса.
Минусы
Бойлерплейт и маппинг. Самая частая претензия. Одни и те же данные тянутся через цепочку преобразований: HTTP-DTO → request-модель → доменная сущность → response-модель → view model. Для одной операции «создать заказ» пишутся пять структур и три функции маппинга. Сторонники отвечают, что это осознанная плата за изоляцию слоёв, — и это честно, но плата реальна: в простых системах маппинг легко съедает больше кода, чем бизнес-логика, ради которой всё затевалось.
Избыточность для CRUD. Подавляющее большинство приложений — это формы над базой данных без сколько-нибудь сложных бизнес-правил. Для них кольца Clean Architecture — пустой ритуал: интерактор без правил — это транзит данных, сущность без инвариантов — DTO, и вся конструкция лишь добавляет косвенность к тому, что генерирует скаффолдинг фреймворка за минуту. Классический слой controller → service → repository честно и дешевле решает ту же задачу.
Порог входа. Стиль требует зрелости: понимать инверсию зависимостей, отличать правило предприятия от правила сценария, чувствовать границы. Джун, выросший на «фреймворк делает всё сам», в чистой архитектуре теряется: без дисциплины границы быстро замусориваются — в интерактор просачивается SQL, в сущность — DTO, и формально «чистая» структура превращается в распределённый по папкам большой шар грязи, который хуже честного монолита, потому что создаёт иллюзию порядка.
Риск «религиозного» применения. Clean Architecture — самый яркий пример стиля, который применяют из веры, а не из анализа. «Uncle Bob так сказал» — не аргумент; аргумент — характер изменчивости конкретной системы. Симптомы культа: четыре кольца в проекте на две недели; слои use cases в приложении из трёх экранов; DTO-маппинг там, где есть один потребитель; запрет на прямой SQL «по принципам», хотя весь домен — это один запрос. Мартин сам призывает применять стиль частично и по необходимости — но популяризация часто теряет эту оговорку.
Анемичная доменная модель. При механическом применении вся логика уезжает в интеракторы, а Entities остаются мешками данных с геттерами и сеттерами. Мартин Фаулер ещё в 2003 году описал этот антипаттерн как анемичную доменную модель (anemic domain model): объекты-записи плюс процедуры-скрипты — это процедурное программирование на классах, теряющее главную выгоду ООП — инкапсуляцию данных с поведением. Clean Architecture не обречена на анемию (пример выше намеренно держит PlaceOrder внутри сущности), но поощряемое ею разделение «сценарии отдельно, данные отдельно» регулярно в неё скатывается — особенно в паре с ORM, где entity удобно делать записью таблицы.
Известная критика. Помимо фаулеровской, стиль попадает под более общую критику декомпозиционных школ: Джон Аустераут в A Philosophy of Software Design (2018) отстаивает противоположный трейдофф — «глубокие модули» с простым интерфейсом и сложной реализацией против множества мелких частей с перекладыванием данных; Рич Хики в докладе Simple Made Easy (2011) показывает, как «абстрактность» и «многослойность» незаметно усложняют (complect) систему вместо упрощения. Показательна и позиция индустриальных гайдлайнов: официальная архитектурная документация Google для Android объявляет слой domain с use cases необязательным — добавляйте, когда он несёт логику, а не по умолчанию. Наконец, есть взвешенная претензия к самой книге: её примеры (Java/C#-мир 2010-х) плохо переносятся в экосистемы с другим культом — функциональные языки, где изоляция достигается типами и чистыми функциями без класса-интерактора, или фреймворки с генерацией кода.
Не решает системных вопросов. Clean Architecture — стиль организации кода, и только. Она не отвечает, как масштабировать нагрузку, резать систему на сервисы, обеспечивать транзакции между ними, версионировать контракты, разворачивать и наблюдать систему. Команда может получить безупречно чистые кольца внутри каждой части — и провальную архитектуру целого. Обратное тоже верно: стиль не мешает всему этому, просто оставляет за скобками.
Когда применять
Сложная доменная логика. Главный критерий — количество и изменчивость бизнес-правил. Если в системе правила: расчёт тарифов и скидок, лимиты и инварианты, состояния и переходы между ними, законодательные требования, — ядро этой логики заслуживает изоляции. Чем больше правил и чем чаще они меняются, тем быстрее окупается дисциплина границ. Типичные кандидаты: биллинг, страхование, логистика, трейдинг — иными словами, domain-heavy SaaS.
Долгоживущие системы. Горизонт жизни в годы и десятилетия почти гарантирует смену технологий доставки и хранения: UI-фреймворки приходят и уходят каждые несколько лет, ORM и версии платформы — ещё чаще. Если код переживёт эту ротацию (а бизнес-критичные системы почти всегда переживают), изоляция ядра окупается с процентами. Для одноразового кода — демо, hackathon, временный лендинг — ротации не будет, и платить за неё вперёд бессмысленно.
Ожидаемая замена инфраструктуры. Конкретные, уже видимые причины: несколько интерфейсов доступа к одной логике (web + API + CLI + очередь), пилотирование нескольких хранилищ, миграция с легаси, требования регуляторов к смене компонентов. Если такие требования известны заранее, границы нужно проводить с первого дня — перестраивать позже на порядок дороже.
Регрессионная защита бизнес-правил. Если ошибка в бизнес-логике стоит дорого (финансы, медицина, договорные обязательства), быстрый детерминированный юнит-тест на интерактор — самый дешёвый способ получить плотную сетку тестов именно на правила, а не на инфраструктуру вокруг них.
Организационная готовность. Честный критерий, о котором забывают: стиль требует от команды базовых инженерных навыков — понимания инверсии зависимостей и DI, привычки писать тесты с заглушками, владения маппингом и рефакторингом. Команда без этого фундамента получит не «чистую архитектуру», а дорогую имитацию с теми же связями, только разнесёнными по папкам. В таком случае честнее сначала вырастить культуру тестирования и проектирования на простой структуре, а кольца вводить, когда команда к ним готова, — а не наоборот.
Когда избыточна. Честный список зеркален: CRUD-приложения и админки без правил; прототипы и MVP, чья судьба — быть выброшенными или переписанными; системы с горизонтом жизни в месяцы; маленькие команды на простом домене без перспективы роста сложности. Здесь честный controller → service → repository или скаффолдинг фреймворка даст тот же результат в разы дешевле. Правило большого пальца: число слоёв должно соответствовать числу разных причин изменения, а не эстетике диаграммы.
Применять частично — нормальная практика. Менее известный, но важный тезис самой книги: Clean Architecture допускает частичное применение. Можно изолировать только ядро расчётов, оставив CRUD-обвязку в «грязном» стиле; можно применить стиль к одному модулю системы, не трогая остальное; можно начать с двух слоёв (домен + всё остальное) и дорезать границы по мере роста боли. Эволюционный путь «от простого к чистому» почти всегда разумнее стартового максимализма — и лучше всего он описан в рамке эволюционной архитектуры: границы проводятся там, где доказана изменчивость, и удерживаются fitness-функциями.
Частые вопросы
Нужен ли интерактор для каждой операции? Нет. Если операция — чистый CRUD без правил («удалить черновик по идентификатору»), интерактор-транзит лишь добавит слой без содержания: контроллер может обратиться к шлюзу напрямую. Интерактор появляется тогда, когда появляется сценарная логика — проверки, оркестрация нескольких сущностей, бизнес-решения. Количество слоёв следует за сложностью, а не наоборот.
Сколько слоёв должно быть? Столько, сколько различимых причин изменения вы изолируете. Четыре кольца книги — схема для типичного корпоративного приложения; в конкретной системе могут быть уместны три (домен, адаптеры, доставка) или шесть (например, отдельное кольцо для антикоррупционного слоя интеграций). Единственное неизменное требование — направление зависимостей.
Нужен ли DI-контейнер? Нет. Контейнер (Spring DI, ASP.NET DI, Dagger) — удобство, а не требование стиля. Компоновку можно выполнять вручную в компоненте Main: создать шлюз, передать его в интерактор, интерактор — в контроллер. Для небольших систем ручная компоновка даже предпочтительнее: она прозрачна и не добавляет «магии».
Как Clean Architecture сочетается с CQRS? Хорошо и естественно. CQRS разделяет операции на команды и запросы; Clean Architecture описывает, как организовать каждую сторону. На командной стороне — классические интеракторы с бизнес-правилами. На запросовой часто всё проще: у чтения нет инвариантов, поэтому query-обработчик может идти мимо домена — прямо из контроллера через шлюз к оптимизированному представлению данных. Такой «асимметричный» вариант — распространённая и поощряемая книгой практика.
Применима ли Clean Architecture во фронтенде и мобильной разработке? Механизм — применим: изоляция доменной логики от фреймворка состояния и от API одинаково ценна и в SPA, и в Android/iOS. Отличается баланс цены: UI-слои меняются чаще, данных меньше, а фреймворки (React, SwiftUI, Compose) сами навязывают структуру. Рабочий компромисс, пришедший из мобильной практики: изолировать только содержательную логику (валидация, расчёты, конечные автоматы экранов) в ядро, а binding и состояние оставить фреймворку.
Обязательна ли UML-строгая терминология «Interactor/Gateway/Presenter»? Нет: имена вторичны, правило зависимостей первично. В разных экосистемах те же роли называются по-своему (use case service, repository, view model); команда может пользоваться любым словарём, если он зафиксирован и границы выдерживаются.
Связанные статьи
- SOLID — пять принципов проектирования, масштабирующихся от классов до границ слоёв; Dependency Inversion — механизм, делающий правило зависимостей Clean Architecture возможным.
- Введение в архитектуру — общие понятия архитектуры ПО и место Clean Architecture среди архитектурных стилей.
- Монолитная архитектура — deployment-стиль, внутри которого чаще всего применяется Clean Architecture как структура кода.
- Микросервисы — декомпозиция на сервисы; Clean Architecture применима внутри каждого сервиса независимо.
- Эволюционная архитектура — управление изменениями архитектуры; правило зависимостей как fitness-функция.