Стили внутренней организации кода
Модель и масштаб — Основы БД, опорные темы, проектирование БД, пакетная работа. Карта — о разделе.
Стили внутренней организации кода
Выбор архитектурного стиля — это выбор стратегии направления зависимостей. В любой системе возникают зависимости — между классами, модулями, слоями, внешними системами. Ключевой вопрос в том, как сделать зависимости управляемыми. Хороший стиль минимизирует зависимость стабильных, критичных частей системы (бизнес-логики, доменных правил) от нестабильных (интерфейсов, инфраструктуры, внешних API).
Это достигается через инверсию зависимостей — принцип, согласно которому высокоуровневые модули не должны зависеть от низкоуровневых; оба должны зависеть от абстракций. Архитектурные стили — это конкретные реализации этого принципа на уровне приложения.
Слоистая архитектура (Layered Architecture)
Слоистая архитектура — наиболее распространённый и интуитивно понятный стиль. Система разделяется на горизонтальные слои, каждый из которых имеет чёткую зону ответственности, и зависимости допускаются только в одном направлении — от верхних слоёв к нижним.
Типичная трёхслойная структура:
-
Презентационный слой (Presentation) — отвечает за взаимодействие с пользователем или внешней системой — HTTP-контроллеры, GraphQL-ресолверы, UI-компоненты (в случае SSR). Здесь формируются запросы и отдаются ответы. Логика здесь минимальна — преобразование данных, валидация входных параметров, маршрутизация.
-
Слой приложения (Application / Business Logic) — сердце системы. Здесь реализуются сценарии использования — "оформить заказ", "отменить бронирование", "рассчитать налог". Этот слой координирует работу доменных сущностей, транзакций, внешних вызовов. Он не содержит деталей реализации (как именно сохраняются данные, как отправляется email), но знает, когда и в каком порядке это должно происходить.
-
Слой инфраструктуры (Infrastructure / Persistence) — техническая поддержка — доступ к базе данных, работа с внешними API, отправка сообщений, логирование, кэширование. Здесь живут репозитории, HTTP-клиенты, ORM-маппинги.
Ключевой приём — зависимости направлены вниз, но реализация низкоуровневых компонентов внедряется вверх через интерфейсы (Dependency Injection). Например, слой приложения зависит не от конкретного SqlOrderRepository, а от интерфейса IOrderRepository. Конкретная реализация подаётся из слоя инфраструктуры при старте приложения.
Play ITЗагрузка интерактивного демо…
Play ITЗагрузка интерактивного демо…
Такая структура обеспечивает:
- чёткое разделение ответственности — легко понять, где искать нужный код;
- возможность тестирования слоя приложения в изоляции (mock-реализации репозиториев);
- заменяемость инфраструктурных компонентов без перекомпиляции ядра.
Однако у слоистой архитектуры есть ограничение: она хорошо масштабируется по функциональности, но плохо — по доменным контекстам. Когда система охватывает несколько независимых предметных областей (например, "продажи", "склад", "финансы"), слои начинают "размазываться" — в одном слое приложения смешиваются правила из разных доменов, в слое инфраструктуры — репозитории, не связанные логически. Это приводит к росту связанности и снижению возможности автономной эволюции.
Гексагональная архитектура (Ports & Adapters)
Гексагональная архитектура — ответ на проблему внешних зависимостей. Её цель — сделать ядро приложения полностью независимым от того, откуда приходят данные и куда они уходят. Ядро ("гексагон") содержит только бизнес-логику и оперирует через порты — интерфейсы, определяющие, что может делать система (например, IUserRepository, INotificationService, IPaymentGateway).
Внешние адаптеры — это реализации этих портов:
- Первичные (driving) адаптеры — инициируют вызовы в ядро — HTTP-контроллеры, CLI-команды, scheduled-задачи. Они преобразуют внешний запрос в вызов порта.
- Вторичные (driven) адаптеры — реагируют на вызовы из ядра — реализации репозиториев, HTTP-клиенты для внешних систем, отправка email через SMTP.
Суть в том, что ядро ничего не знает о вебе, базах данных, протоколах. Оно зависит только от своих портов — абстракций, определённых внутри ядра. Адаптеры зависят и от портов, и от внешних технологий. Направление зависимостей — внутрь гексагона, а не наружу.
Play ITЗагрузка интерактивного демо…
Преимущества:
- возможность легко менять способ взаимодействия: заменить REST на gRPC — достаточно написать новый первичный адаптер;
- тестирование ядра в полной изоляции — все зависимости заменяются на заглушки;
- чёткое выделение контрактов: порт — это публичный API ядра, его стабильность критична.
Гексагональная архитектура особенно эффективна в системах с множеством интеграций, где внешние интерфейсы часто меняются (например, платёжные шлюзы, CRM, ERP), а бизнес-логика остаётся стабильной. Она также хорошо сочетается с DDD: порты часто соответствуют границам агрегатов или ограниченных контекстов.
Чистая архитектура (Clean Architecture)
Чистая архитектура — развитие идей гексагональной, с акцентом на иерархию стабильности. Она формализует круги зависимости:
-
Entities (Сущности) — ядро ядра. Бизнес-объекты, инкапсулирующие критически важные правила, не зависящие ни от чего внешнего —
Order,Account,Policy. Могут быть чистыми классами или даже просто структурами данных с методами. Эти правила должны выживать даже при полной смене технологий. -
Use Cases (Сценарии использования) — оркестрация: как сущности взаимодействуют для выполнения бизнес-задачи. Например,
PlaceOrderUseCaseполучает данные, создаётOrder, проверяет инвентарь, инициирует платёж. Зависит от Entities, но не от внешних технологий. -
Interface Adapters (Адаптеры интерфейсов) — преобразование данных между форматами — контроллеры, презентеры, репозитории-обёртки, DTO-мапперы. Здесь происходит адаптация к конкретному фреймворку (MVC, gRPC), но без логики.
-
Frameworks & Drivers (Фреймворки и драйверы) — внешние зависимости — базы данных, веб-серверы, UI-фреймворки, внешние API. Эта зона наиболее нестабильна.
Зависимости направлены от внешних кругов к внутренним. Entities ничего не знают о Use Cases; Use Cases не знают о том, как именно реализованы репозитории. Это достигается через интерфейсы и DI.
Play ITЗагрузка интерактивного демо…
Чистая архитектура — принцип стабильности: чем ближе к центру, тем дольше живёт код. Сущности могут оставаться неизменными годами; адаптеры — меняться с новой версией фреймворка.
Этот стиль оправдан там, где долгосрочная поддержка важнее скорости первоначальной разработки — госзаказ, медицинские системы, финансовые ядра. Он снижает стоимость владения, но требует дисциплины — легко "протечь" зависимость из внешнего круга во внутренний (например, добавить атрибут Id типа Guid в доменную сущность только потому, что так требует ORM).
Событийно-ориентированная архитектура (Event-Driven Architecture)
Событийно-ориентированная архитектура — это смещение акцента с запрос-ответ на публикация-подписка. Вместо того чтобы компоненты вызывали друг друга напрямую, они обмениваются событиями — неизменяемыми фактами о том, что произошло — OrderPlaced, PaymentConfirmed, InventoryUpdated.
Ключевые элементы:
- Источник события — компонент, фиксирующий факт и публикующий событие в шину (Kafka, RabbitMQ, AWS SNS/SQS);
- Шина сообщений — инфраструктурный компонент, обеспечивающий доставку;
- Подписчики — компоненты, реагирующие на события и выполняющие свои задачи (обновление склада, отправка уведомления, аналитика).
Событие — констатирует "сделано". Это позволяет строить асинхронные, слабосвязанные системы.
Play ITЗагрузка интерактивного демо…
Преимущества:
- масштабируемость: обработчики событий можно масштабировать независимо;
- отказоустойчивость: если один обработчик упал, событие остаётся в очереди;
- расширяемость: новые функции можно добавлять просто подпиской на существующие события;
- историчность: события можно сохранять как журнал (event log), что даёт полную аудиторскую трассу.
Однако события вносят новую сложность:
- eventual consistency — состояние системы может быть временно несогласованным;
- отладка требует инструментов трассировки по цепочке событий;
- дублирование событий или их потеря требуют идемпотентных обработчиков;
- проектирование событий — нетривиальная задача: слишком грубое событие (например,
OrderUpdated) несёт мало информации; слишком детальное — жёстко связывает издателя и подписчика.
Событийно-ориентированная архитектура особенно эффективна в системах с высокой нагрузкой, где важна асинхронность — электронная коммерция, логистика, финтех, IoT. Часто она используется как дополнение к другим — например, в чистой архитектуре сценарий использования после выполнения публикует событие, а инфраструктурный адаптер его отправляет.
Компонентно-ориентированная архитектура (Component-Based Architecture)
Этот стиль чаще применяется на уровне пользовательского интерфейса, но принципы переносятся и на бэкенд. Основная идея — разбиение системы на повторно используемые, самодостаточные компоненты, каждый из которых инкапсулирует:
- состояние;
- поведение;
- представление (если применимо);
- интерфейс взаимодействия с другими компонентами.
Компонент — это не просто класс или модуль. Это контракт — что он принимает на вход, что выдаёт на выход, какие события генерирует, какие зависимости требует. В вебе это — React-компоненты, Angular-модули, Web Components. В бэкенде — библиотеки с чётким API, микросервисы с открытой спецификацией (OpenAPI), плагины с фиксированным интерфейсом.
Play ITЗагрузка интерактивного демо…
Компонентно-ориентированный подход позволяет:
- собирать сложные интерфейсы из простых блоков;
- переиспользовать функционал без дублирования;
- изолировать изменения: если компонент меняет внутреннюю реализацию, но сохраняет контракт — внешний код не требует правок.
Ключевое условие успеха — строгая дисциплина контрактов. Компонент без документированного API быстро превращается в "чёрный ящик", зависимость от которого становится техническим долгом.
Практика: как внедрять стиль в существующем проекте
Если проект уже живёт в продакшене, переход к более строгой архитектуре лучше делать шагами:
- Зафиксировать текущие зависимости между слоями (кто кого вызывает).
- Выделить 1-2 критичных сценария и описать их через порты/интерфейсы.
- Перенести инфраструктурные детали за адаптеры (БД, HTTP, очереди).
- Добавить архитектурные тесты (например, запрет зависимостей из ядра во внешний слой).
- Повторять цикл на следующих модулях.
Такой подход даёт эффект без полной переписи и снижает риск регрессий.
Что проверить в код-ревью
- Бизнес-логика не "утекает" в контроллеры, репозитории и фреймворковые классы.
- Интерфейсы описывают смысловые операции домена, а не технические детали.
- Зависимости направлены к более стабильным модулям, а не наоборот.
- Новые интеграции входят через адаптеры, а не прямыми вызовами из ядра.
Разборы: как стиль меняет код
Разбор 1: слоистая архитектура
- До: контроллеры напрямую вызывают SQL/ORM, валидация и правила скидок перемешаны.
- После — контроллер только принимает запрос, бизнес-правила уехали в слой приложения, БД — в инфраструктуру.
- Что получили: упрощение тестирования и предсказуемые точки изменений.
Разбор 2: гексагональная архитектура
- До: бизнес-логика зависит от SDK конкретного платёжного провайдера.
- После: введён порт
IPaymentGateway, внешний SDK подключён через адаптер. - Что получили: замена провайдера без переписывания ядра.
Разбор 3: событийная модель как дополнение
- До: синхронные вызовы уведомлений и аналитики в критичном сценарии оформления заказа.
- После: после успешного сценария публикуется событие, подписчики обрабатывают в фоне.
- Что получили: меньше latency пользовательского запроса и проще масштабирование обработчиков.