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-запроса до ответа:

  1. Web-фреймворк (Frameworks & Drivers) принимает POST /orders, разбирает HTTP, передаёт управление контроллеру.
  2. Controller (Interface Adapters) проверяет транспортную корректность, собирает PlaceOrderRequest(customerId, itemIds) и вызывает PlaceOrder.Execute(request) — интерактор.
  3. Interactor (Use Cases) через IOrderGateway загружает Customer и товары; обращается к методу customer.PlaceOrder(...) — бизнес-правила (лимит кредита, доступность товара, расчёт скидки) выполняются в Entities.
  4. Интерактор фиксирует результат: gateway.Save(order). Вызов уходит наружу — в SqlOrderGateway — но зависимость (интерфейс IOrderGateway) остаётся внутренней.
  5. Интерактор возвращает PlaceOrderResponse(true, null, 42) — глухую структуру без знания о HTTP.
  6. Presenter (Interface Adapters) превращает response в view model — например, в JSON-ответ {"orderId": 42}.
  7. 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-функция.