Практическая ситуация: модули формально не импортируют друг друга, но оба используют изменяемое глобальное состояние. Какой механизм нарушает их архитектурную границу?
shared Настройки {
скидка = 0
}
module Заказ {
function итог(сумма) {
return сумма * (1 - Настройки.скидка)
}
}
module Кампания {
function включить() {
Настройки.скидка = 0.2
}
}
Границу нарушает скрытая связанность через общее изменяемое состояние. Модуль Заказ зависит не только от своих входных параметров, но и от текущего значения Настройки.скидка, которым управляет другой модуль. Поэтому изменение или порядок вызова Кампания меняет поведение Заказ без изменения его интерфейса.
Модульность появилась как способ ограничить влияние изменений в сложных программных системах. Идея сокрытия информации предполагает, что модуль скрывает свои изменяемые решения, а взаимодействие с ним происходит через явный контракт.
Глобальное состояние противоречит этому принципу: оно выносит важные данные за пределы владельца и делает их доступными через неявный канал. Такой код может быть удобен в небольшой программе, но по мере роста системы усложняет тестирование, сопровождение и независимую эволюцию частей.
Заказ.итог выглядит как функция от сумма, однако фактически результат зависит ещё и от глобальной скидки. Потребитель не видит эту зависимость в сигнатуре и не обязан учитывать, кто и когда изменил настройку.
Возникают несколько рисков: результат зависит от порядка выполнения, параллельные операции могут наблюдать разные значения, а тесты начинают влиять друг на друга. Кроме того, изменение формата или жизненного цикла настройки требует согласованных изменений у всех использующих её модулей.
Нужно сделать зависимость явной и определить владельца политики скидки. Например, Заказ может получать рассчитанную скидку параметром или зависеть от явно объявленного интерфейса политики, переданного при создании модуля.
Теперь источник скидки виден в контракте итог. Модуль можно тестировать с детерминированной политикой, а реализацию политики — менять независимо от расчёта заказа.
Передача состояния параметром подходит для локальной операции и короткой цепочки вызовов. Явная зависимость через интерфейс удобнее, если политика имеет собственную бизнес-логику, несколько реализаций или должна подменяться при тестировании.
Полностью устранить общее состояние не всегда возможно: например, кэш, конфигурация или хранилище могут быть технически общими. В таком случае важно скрыть их за владельцем или интерфейсом, запретить произвольную запись и определить правила согласованности. Сам факт совместного доступа не является проблемой; архитектурный риск создаёт неконтролируемая запись, от которой зависят другие модули.
В интернет-магазине модуль расчёта доставки читал глобальный объект НастройкиРегионов, а административный модуль обновлял его при изменении тарифов. После обновления часть заказов в параллельных запросах рассчитывалась по старым тарифам, часть — по новым; тесты также зависели от порядка запуска.
Рассматривались три варианта. Сохранить глобальный объект было проще всего, но это оставляло скрытую связанность. Передавать весь объект настроек в каждый вызов повышало прозрачность, однако создавало слишком широкий контракт. Выделить интерфейс ТарифыДоставки и передавать его в сервис расчёта требовало небольшого рефакторинга, но ограничивало поверхность зависимости.
Выбрали третий вариант: административный модуль стал владельцем публикации версии тарифов, а расчёт доставки получал неизменяемый снимок через интерфейс. Это устранило произвольное изменение состояния во время расчёта, упростило тестирование и позволило менять способ хранения тарифов без изменений в заказах.
Нет. Если любой модуль по-прежнему может изменить объект, скрытая связанность сохраняется. Инкапсуляция требует контролировать владельца состояния и разрешённые операции, а не только спрятать переменную за синтаксической оболочкой.
Она устраняет неявность зависимости, но не гарантирует корректность архитектуры. Если в параметр передаётся большой изменяемый объект, вызывающий код всё ещё может менять чужое состояние и создавать связанность. Предпочтительнее передавать минимальный интерфейс или неизменяемый снимок данных.
Потому что результат операции может зависеть от момента чтения относительно записи другого потока или запроса. Даже при потокобезопасной записи логика может быть неверной: один расчёт прочитает значение до обновления, другой — после. Для предсказуемости нужны явные правила версии, транзакционности или передачи согласованного снимка.