Entity core и его стоимость
Кольцо Entities — наиболее стабильный код системы: чистые бизнес-правила без зависимостей. Реальная стоимость Clean Architecture — налог на маппинг и косвенное обращение на каждой границе кольца. Он оправдывается только в конкретных обстоятельствах.
Спустя восемнадцать месяцев после принятия Clean Architecture для B2B-платформы заказов команда провела аудит. Хорошие новости: платформа была расширена четырьмя новыми delivery mechanisms — мобильное приложение, партнёрский API, батч-сервис сверки и ops-CLI — все подключились к существующим interactors без изменений бизнес-логики. Новые инженеры могли найти каждое бизнес-правило в одном месте. Плохие новости: кодовая база раздулась. Теперь для «заказа» существовало пять отдельных структур данных — entity Order в кольце Entities, ORM-класс OrderRecord в адаптере базы данных, OrderResponseModel в кольце Use Cases, OrderDTO в REST-адаптере и OrderGraphQLType в GraphQL-адаптере. Каждая граница кольца порождала код маппинга. Простое изменение «добавить поле в заказ» затрагивало все пять классов и три функции маппинга. Команда задала честный вопрос: окупается ли архитектура? Ответ: для этой платформы — да. Но они видели, как коллеги применяли тот же паттерн к двадцатитабличному CRUD-приложению для внутренней отчётности и производили тот же объём кода маппинга при нулевой структурной пользе. Clean Architecture не универсально правильна. У неё есть конкретная стоимость и конкретные условия, при которых эта стоимость оправдана.
Entity core — что там живёт и почему
Кольцо Entities — самое внутреннее кольцо в Clean Architecture. Оно содержит общеприкладные бизнес-правила: логику, которая была бы верна и валидна в любом приложении, построенном на этом бизнес-домене, независимо от delivery mechanism или используемой инфраструктуры.
На B2B-платформе заказов Entity core содержит:
- Класс
Orderс инвариантами жизненного цикла (pending-заказ должен иметь хотя бы одну позицию перед подтверждением; отменённый заказ не может перейти в confirmed; итог должен равняться сумме позиций) - Класс
Invoiceсо структурными инвариантами (позиции должны суммироваться в итог; оплаченный счёт не может быть изменён) - Value object
Moneyс правилами арифметики с учётом валюты - Перечисления
OrderStatus,InvoiceStatus, представляющие валидные состояния - Domain events (
OrderConfirmed,InvoiceGenerated) как чистые структуры данных
Что отсутствует в кольце Entities: всё, что касается инфраструктуры, прикладного потока или фреймворка. Никаких вызовов репозиториев. Никакого async/await-обращения к базе данных. Никаких ORM-аннотаций @Column или @Entity. Никаких HTTP-статус-кодов. Никаких импортов фреймворка логирования. Классы entity — чистые объекты, которые могут работать в полностью изолированном тесте без внешних зависимостей.
Аргумент стабильности
Аргумент в пользу сохранения Entity core чистым — это аргумент стабильности: entities — последний код, который нужно трогать при технологических изменениях. Базы данных мигрируют. Веб-фреймворки обновляются. Появляются мобильные клиенты. Ни одно из этих событий не должно изменить правило «Итог Invoice должен равняться сумме его позиций». Это правило постоянно, пока держится бизнес-модель.
Сохраняя entities свободными от инфраструктурных импортов, вы гарантируете: обновление ORM не может вызвать изменения entity. Переход с REST на GraphQL не может потребовать касания бизнес-логики Order. Новое требование облачного провайдера не может пройти в domain-правила. Самое внутреннее кольцо меняется только при изменении бизнес-модели — что и является единственным изменением, которое должно правомерно затрагивать его.
Эта стабильность также даёт выигрыш в тестировании. Unit-тесты entity — самые дешёвые тесты в системе: инстанцировать domain object, вызвать метод, проверить результат. Никакой базы данных, HTTP-сервера, mock-фреймворка. Entity инкапсулирует свои инварианты — для их тестирования нужен только класс entity и любые value types, которые он использует.
▸Почему это работает
Почему именно «общеприкладные» правила для entities? Терминология Мартина различает правила, которые были бы валидны в нескольких приложениях одного предприятия (инварианты Order применимы к клиентскому порталу, операционной консоли и инструменту финансовой отчётности), и правила, специфичные для рабочего процесса одного приложения (конкретные шаги для «разместить заказ через партнёрский API»). Entities принадлежат самому внутреннему кольцу, потому что они — наиболее переиспользуемые, наиболее стабильные и наименее подверженные изменениям бизнес-знания системы.
Честная стоимость — маппинг и косвенное обращение
Структурные преимущества Clean Architecture реальны. Стоимость не менее реальна, и её необходимо признать перед выбором паттерна.
Налог на маппинг. Каждая граница кольца требует кода трансляции. Поле entity Order не появляется автоматически в OrderResponseModel (кольцо Use Cases), OrderDTO (REST-адаптер) или ORM-классе OrderRecord (адаптер базы данных). Код маппинга на каждой границе пишется явно. Поле, начинающееся на entity, обычно появляется в четырёх-пяти отдельных классах и требует трёх-четырёх функций маппинга.
В аудите B2B-платформы: добавление deliveryDeadline к entity Order означало:
- Добавить поле в
Order(кольцо Entities) с инвариантом (deadline должен быть после даты создания) - Добавить в
GenerateInvoiceRequestиGenerateInvoiceResponseтам, где нужно (кольцо Use Cases) - Добавить в
OrderRecordи функцию ORM-маппинга (кольцо Infrastructure) - Добавить в
OrderDTOи код сериализации (REST-адаптер) - Добавить в
OrderGraphQLTypeи resolver (GraphQL-адаптер)
Это налог на косвенное обращение. В layered architecture можно затронуть два файла. В Clean Architecture затрагиваешь пять — в обмен на гарантию, что каждое кольцо не знает форматов других.
Церемония определений интерфейсов. Каждый репозиторий, каждый сервис, каждый output port требует определения интерфейса в кольце Use Cases и конкретной реализации в другом месте. Для системы с тридцатью use cases это тридцать интерфейсов input port, тридцать классов interactor, потенциально тридцать интерфейсов output port, тридцать request models и тридцать response models. Прежде чем написать единственную строку бизнес-логики, вы строите значительный объём структуры.
Когнитивная нагрузка. Новые инженеры, приходящие в команду, должны понять кольцевую модель, прежде чем смогут найти любой кусок логики. Вопрос онбординга «где живёт проверка billing hold?» имеет точный ответ (кольцо Use Cases, внутри interactor) — но только если понимаешь архитектуру.
Когда Clean Architecture избыточна
Clean Architecture окупается в конкретных обстоятельствах. За их пределами она генерирует церемонию без структурной отдачи.
Избыточна для: CRUD-приложений. Система, которая создаёт, читает, обновляет и удаляет записи без сложных инвариантов, state machines жизненного цикла или нескольких delivery mechanisms, не имеет бизнес-логики для защиты. Entities без реальных инвариантов (просто getters и setters над строкой базы данных) не выигрывают от нахождения в чистом самом внутреннем кольце.
Избыточна для: небольших или краткосрочных приложений. Внутренний дашборд отчётности для десяти человек, скрипт миграции данных, throwaway proof-of-concept — они будут заменены до того, как долгосрочные преимущества архитектуры начнут накапливаться.
Избыточна для: команд без дисциплины поддержания границ. Преимущества Clean Architecture исчезают в момент, когда разработчик из удобства добавляет импорт фреймворка в entity. Налог на маппинг постоянен; структурные гарантии постоянны только при соблюдении dependency rule. Если команда не может или не будет соблюдать границы колец, стоимость платится, но выгода не получается.
Стоит того для: долгоживущих приложений со сложными domain-правилами. Когда бизнес-логика действительно сложна — несколько состояний жизненного цикла, инварианты, требующие соблюдения, правила, растущие со временем, — entity core обеспечивает стабильный фундамент, переживающий годы инфраструктурных изменений.
Стоит того для: нескольких delivery mechanisms над общим доменом. Четыре delivery mechanisms B2B-платформы (веб, мобильный, партнёрский API, CLI) переиспользовали одни и те же interactors. Добавление четвёртого механизма обошлось команде только одним адаптером. Без Clean Architecture у них было бы четыре копии логики генерации счёта.
Стоит того для: команд, которые будут расти и меняться. Когда команда, строящая систему, не та, что её поддерживает, явные контракты (request models, port interfaces) служат документацией, которую компилятор принудительно соблюдает.
▸lesson.inset.note
Практическая эвристика: посчитай количество delivery mechanisms, разделяющих одну бизнес-логику. Если ответ один — только HTTP API, и так будет всегда — симметричное преимущество Clean Architecture гипотетично. Если ответ три или больше — или есть достоверный roadmap к трём или более — паттерн interactor начинает окупаться. Вторая эвристика: посчитай domain-инварианты. Если можешь описать все бизнес-правила домена пятью предложениями, entity core мало что защищает. Если описание правил занимает сессию у доски, core стоит налога.
Команда платформы добавляет новое поле `billingRegion` в entity Order. В полностью реализованной кодовой базе Clean Architecture сколько файлов минимально должно измениться, и почему?
Стартап строит B2B-систему управления заказами как монолит для одного клиента, ожидаемый срок работы — восемнадцать месяцев с последующей перестройкой. CTO предлагает Clean Architecture. Каков честный архитектурный совет?
Команда строит B2B-платформу заказов, сейчас имеющую один HTTP API. Product roadmap показывает мобильное приложение, партнёрскую EDI-интеграцию и ops-CLI, запланированные на следующие два года. CTO спрашивает: стоит ли принимать Clean Architecture сейчас или «подождать и посмотреть»? Каков структурный аргумент за принятие в начале?
- 01Что принадлежит Entity core, и каков аргумент стабильности за сохранение его чистым?
- 02Что такое налог на маппинг, и что делает его приемлемым?
- 03Назови три признака того, что Clean Architecture избыточна для данного проекта.
Entity core — самое внутреннее кольцо в Clean Architecture — наиболее стабильный, наиболее абстрактный, наиболее независимо тестируемый код системы. Он содержит общеприкладные бизнес-правила: инварианты, верные в любом приложении на этом бизнес-домене, независимо от delivery mechanism или инфраструктурной технологии. Что отсутствует: ORM-аннотации, типы базы данных, HTTP-концепции, импорты фреймворка. Entity меняется только при изменении бизнес-модели.
Честная стоимость Clean Architecture — налог на маппинг. Каждая граница кольца его берёт: domain-поле появляется в нескольких структурах данных, формат каждого кольца подходит для его цели, каждая граница требует явного кода маппинга. Это не артефакт плохой реализации — это прямое структурное следствие разделения колец. Аудит платформы обнаружил пять отдельных классов для «заказа» и три функции маппинга на поле. Вот счёт.
Счёт стоит оплатить при конкретных условиях: долгоживущие приложения, где инфраструктурные изменения опережают изменения бизнес-модели; системы с несколькими delivery mechanisms, переиспользующими одну бизнес-логику; сложные домены, где entity core действительно имеет инварианты для защиты; команды, которые будут расти и меняться. Для CRUD-приложений, throwaway-систем, бэкендов с единственным delivery mechanism и доменов с тривиальными инвариантами счёт приходит без соответствующей отдачи.
На этом завершается unit по Clean и Onion Architecture. Следующий unit вводит Domain-Driven Design — который идёт дальше концепции entity core и спрашивает, как моделировать бизнес-домен так, чтобы модель сама стала архитектурой.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.