АрхитектураАрхитектура ПОАрхитектор программного обеспечения

Практическая ситуация: доменный модуль напрямую зависит от инфраструктурного. Как изменить границу между ни...

Практическая ситуация: доменный модуль напрямую зависит от инфраструктурного. Как изменить границу между ними, чтобы сохранить независимость домена?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

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

Так применяется инверсия зависимостей: направление зависимости исходного кода меняется, хотя во время выполнения домен по-прежнему может вызывать инфраструктурную реализацию.

Исторический контекст

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

Модульность и архитектурные подходы с явными границами возникли как способы локализовать изменения. Инверсия зависимостей уточняет важный принцип: границу должен контролировать тот модуль, чьи правила наиболее стабильны и значимы для системы.

Постановка проблемы

Если доменный код напрямую использует конкретное хранилище, брокер сообщений или внешний клиент, он знает детали инфраструктуры. Это приводит к нескольким рискам:

  • изменение инфраструктуры затрагивает бизнес-логику;
  • тесты домена требуют реальных внешних ресурсов или сложных замен;
  • инфраструктурные типы начинают распространяться через границу модуля;
  • переиспользование доменных правил в другом сценарии становится дороже.

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

Подробное решение

Доменный модуль должен объявить узкий порт — абстракцию операции, которая нужна бизнес-правилу. Например, не «универсальный клиент базы данных», а «получить активный заказ» или «сохранить результат расчёта». Такая абстракция выражает язык домена, а не особенности конкретной технологии.

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

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

Абстракция должна быть минимальной и принадлежать модулю, который использует поведение. Если контракт моделирует детали базы данных, транспорта или конкретного поставщика, зависимость лишь маскируется. Если один порт обслуживает слишком много сценариев, он превращается в нестабильный общий интерфейс, который вынуждает менять домен при изменении инфраструктурных потребностей.

Преимущества подхода — локализация изменений, более изолированное тестирование и ясное владение контрактом. Компромисс состоит в появлении дополнительных абстракций, адаптеров и кода связывания; для простой системы это может быть неоправданной сложностью.

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

Ситуация из практики

В системе расчёта тарифов бизнес-правила напрямую обращались к клиенту внешнего сервиса профилей. Из-за этого изменение протокола требовало правок в расчётах, а тесты зависели от доступности удалённого сервиса.

Рассматривались три варианта. Первый — оставить прямую зависимость и стабилизировать внешний клиент: это дало бы быстрый результат, но сохранило бы связанность. Второй — вынести весь расчёт в инфраструктурный слой: это уменьшило бы видимую зависимость домена, но смешало бы транспортные детали с бизнес-правилами. Третий — ввести доменный порт получения необходимых для расчёта атрибутов и создать адаптер внешнего сервиса.

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

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

Что кандидаты часто упускают

  1. Кто должен владеть абстракцией: домен или инфраструктура?

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

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

  1. Достаточно ли заменить конкретный класс интерфейсом?

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

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

  1. Когда инверсия зависимостей может ухудшить архитектуру?

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

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