Чистая архитектура: что от неё остаётся на практике
Что такое чистая архитектура, какое у неё единственное несущее правило и почему четыре кольца со схемы не выживают в реальном проекте. Разбор с границей применимости.
Коротко. От чистой архитектуры на дистанции выживает одно правило — зависимости направлены внутрь. Четыре кольца, четыре слоя объектов переноса и класс на каждый сценарий — это налог, который окупается редко.
Чистую архитектуру обычно показывают схемой из четырёх концентрических
колец. Схему запоминают, реализуют буквально, через полгода проект
состоит из папок 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. Это окупается почти всегда и стоит один день настройки.
Слои переноса, классы сценариев и интерфейсы про запас добавляйте по факту боли, а не заранее. Каждый из них решает конкретную проблему; пока проблемы нет, вы платите за решение вперёд.
Где это не нужно вовсе. Скрипт, утилита, прототип с горизонтом в три месяца. Налог на косвенность платится сразу, а окупается на дистанции, которой у них нет. Признак, что пора, — момент, когда кодовую базу перестаёт держать в голове один человек.