АрхитектураАрхитектура ПОРазработчик прикладных систем

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

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

модуль Каталог {
    приватное товары

    функция получитьТовары() {
        вернуть товары
    }
}

список = Каталог.получитьТовары()
список.удалить(0)
список[0].внутренняяСебестоимость = 0
Проходите собеседования с ИИ помощником Hintsage

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

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

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

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

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

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

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

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

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

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

Модуль должен владеть своим состоянием и предоставлять операции, выражающие допустимые намерения: добавитьТовар, изменитьЦену, удалитьТовар. Внутри этих операций выполняются проверки, обновляются связанные данные и поддерживаются инварианты.

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

Например:

модуль Каталог { приватное товары функция найтиТовар(идентификатор) { товар = товары.найти(идентификатор) вернуть DTO(товар.идентификатор, товар.цена) } функция изменитьЦену(идентификатор, новаяЦена) { требовать новаяЦена >= 0 товары.найти(идентификатор).изменитьЦену(новаяЦена) } }

Здесь потребитель не получает ссылку на хранилище и не меняет цену напрямую. Модуль сохраняет свободу заменить структуру товары, а бизнес-правила остаются в одном месте.

Есть компромисс: копирование и преобразование DTO требуют дополнительных ресурсов и кода. Слишком узкий интерфейс может привести к множеству специализированных методов, а слишком широкий — снова превратить модуль во внутреннюю базу данных для всех потребителей. Граница должна публиковать устойчивые бизнес-намерения, а не каждую операцию над внутренней структурой.

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

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

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

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

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

  1. Достаточно ли вернуть неизменяемую коллекцию, чтобы граница стала качественной?

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

  1. Всегда ли DTO лучше доменной сущности на границе модуля?

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

  1. Почему запрет прямой мутации не гарантирует независимую эволюцию модуля?

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