Алексей Меленчук
вся записка
дата
объём 5 мин чтения

Чистая архитектура: что от неё остаётся на практике

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

Коротко. От чистой архитектуры на дистанции выживает одно правило — зависимости направлены внутрь. Четыре кольца, четыре слоя объектов переноса и класс на каждый сценарий — это налог, который окупается редко.

Чистую архитектуру обычно показывают схемой из четырёх концентрических колец. Схему запоминают, реализуют буквально, через полгода проект состоит из папок Application, Domain, Infrastructure, Presentation, а изменение одного поля правит восемь файлов.

Потом схему объявляют оверинжинирингом. Обе крайности мимо, и разобрать стоит по частям: что там несущее, а что декоративное.

Единственное правило, ради которого всё затевалось

Зависимости направлены внутрь. Код, выражающий правила предметной области, не знает о базе, фреймворке, транспорте и формате хранения. Знание идёт только в одну сторону.

Проверяется механически: есть ли в модуле с правилами импорт ORM, HTTP-клиента, фреймворка. Если есть — правила зависят от инструмента, и замена инструмента будет их переписыванием.

Почему это ценно: инструменты меняются быстрее правил. Переезд с одной ORM на другую — работа на неделю, если правила её не видят, и переписывание половины проекта, если видят.

Всё остальное в чистой архитектуре — способы это правило соблюсти. Способы можно выбирать; правило — нет.

Механика: как зависимость разворачивают

Правило кажется невозможным: правила предметной области должны сохранять данные, а сохранение — это база.

Разворот делается объявлением интерфейса внутри, а реализацией снаружи.

domain/
  Order.php              — сущность, ничего не знает
  OrderRepository.php    — интерфейс, объявлен здесь
infrastructure/
  PgOrderRepository.php  — реализация, знает про PostgreSQL

Интерфейс принадлежит внутреннему слою: он выражен в его терминах и меняется вместе с ним. Внешний слой реализует чужой интерфейс — и потому зависит от внутреннего, а не наоборот.

Это и есть весь механизм. Дальше начинается то, что можно не делать.

Что обычно не окупается

Четыре слоя объектов переноса. Запрос превращается в объект команды, тот в доменную сущность, та в объект сохранения, тот в строку таблицы — и обратно. Каждое поле описано четырежды, добавление поля правит восемь файлов.

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

Класс на каждый сценарий использования. CreateOrderUseCase, CancelOrderUseCase, UpdateOrderUseCase — каждый с одним методом execute. Это функция, записанная как класс. Иногда полезно — когда у сценария есть состояние или зависимостей много. Обычно нет.

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

Что это даёт тестам

Самый измеримый эффект правила, и обычно единственный, ради которого его стоит вводить в существующем проекте.

Когда правила предметной области не знают о базе, их можно проверить без базы. Не «поднять контейнер с PostgreSQL, накатить схему, залить фикстуры» — а вызвать функцию и сравнить результат.

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

Это меняет то, как пишется код, гораздо сильнее, чем расположение папок.

Гексагональная, луковичная и чистая

Три названия, которые ищут отдельно и которые описывают одно правило.

Гексагональная (Кокбёрн, 2005) вводит порты и адаптеры: приложение общается с миром через порты — интерфейсы, объявленные им самим, — а адаптеры их реализуют. Шестиугольник на схеме означает только то, что портов много и они равноправны: база, HTTP, очередь, файловая система — всё это адаптеры одного порядка.

Луковичная (Палермо, 2008) рисует те же зависимости кольцами и подчёркивает, что доменная модель в центре.

Чистая (Мартин, 2012) обобщает обе и добавляет словарь: сущности, сценарии использования, адаптеры интерфейсов, фреймворки.

Практическая разница между ними близка к нулю. Все три говорят «зависимости направлены внутрь, инструменты подключаются снаружи» и отличаются терминами и числом колец на картинке. Выбирать между ними как между технологиями не нужно; спор об этом — спор о словах.

Как это проверяется

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

В экосистеме каждого языка есть инструмент архитектурных тестов: ArchUnit в Java, Deptrac в PHP, import-linter в Python, раздельные цели сборки в C++. Правило пишется один раз, нарушение падает в CI.

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

Где резать модули

Отдельный вопрос, на котором чистую архитектуру чаще всего понимают наоборот.

Схема с кольцами провоцирует техническое деление: слой контроллеров, слой сервисов, слой репозиториев. Выглядит опрятно и плохо работает на правках: задача звучит «поменять правила расчёта доставки», а трогает все три слоя.

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

Вердикт

Берите правило, оставьте церемонию. Домен не импортирует инфраструктуру, интерфейс объявлен внутри, реализация снаружи, проверка в CI. Это окупается почти всегда и стоит один день настройки.

Слои переноса, классы сценариев и интерфейсы про запас добавляйте по факту боли, а не заранее. Каждый из них решает конкретную проблему; пока проблемы нет, вы платите за решение вперёд.

Где это не нужно вовсе. Скрипт, утилита, прототип с горизонтом в три месяца. Налог на косвенность платится сразу, а окупается на дистанции, которой у них нет. Признак, что пора, — момент, когда кодовую базу перестаёт держать в голове один человек.