📐 Раздел 18 · Вопрос #17

Что такое Law of Demeter (принцип наименьшего знания)

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


🟢 Junior Level

Закон Деметры (Law of Demeter, LoD), также известный как Принцип наименьшего знания (Principle of Least Knowledge) — это базовое правило объектно-ориентированного проектирования, сформулированное в 1987 году Яном Холландом:

«Разговаривай только с ближайшими друзьями и не разговаривай с незнакомцами.»

Что это означает простыми словами

Объект не должен знать о внутреннем устройстве других объектов, с которыми он взаимодействует. Вызывающий код должен обращаться только к своему непосредственному соседу и не должен путешествовать по цепочке чужих связей вглубь («через замочную скважину»).

Житейская аналогия

Покупка газеты в киоске:

  • Нарушение Закона Деметры: Покупатель подходит к продавцу, продавец молча залезает рукой в задний карман покупателя, достает оттуда бумажник, открывает отделение для купюр, берет 100 рублей и кладет бумажник обратно.
  • Соблюдение Закона Деметры: Покупатель отдает 100 рублей продавцу (customer.pay(amount)). Продавец не знает и не должен знать, есть ли у покупателя бумажник, карман или кредитная карта.

Антипаттерн «Крушение поезда» (Train Wreck)

Когда Закон Деметры нарушается, в кодовой базе появляются длинные цепочки вызовов через точку:

// Плохо: "Крушение поезда" (Train Wreck)
// Сервис знает структуру заказа, клиента, адреса и города!
String cityName = order.getCustomer().getAddress().getCity().getName().toUpperCase();

Чем опасен такой код:

  1. Катастрофическая хрупкость: Если в классе Customer поле address переименуют или вынесут в список addresses, сломаются десятки сервисов по всей кодовой базе.
  2. Риск NullPointerException на каждом звене: Если хотя бы один метод в цепочке вернет null (customer == null или address == null), всё приложение упадет с ошибкой.
  3. Невозможность мокирования: Чтобы написать юнит-тест, разработчик вынужден писать 4 уровня вложенных моков: when(order.getCustomer()).thenReturn(customer); when(customer.getAddress()).thenReturn(address)....

Исправление по закону Деметры: «Tell, Don’t Ask»

Вместо того чтобы выспрашивать внутренности объекта и принимать решение за него, мы поручаем действие объекту:

// Хорошо: сервис обращается ТОЛЬКО к прямому соседу (Order)
String cityName = order.getDeliveryCityName();

Внутри класса Order запрос делегируется:

public class Order {
    private Customer customer;

    public String getDeliveryCityName() {
        return (customer != null) ? customer.getCityName() : "Unknown";
    }
}

🟡 Middle Level

Формальные 4 правила Либерхерра и Холланда

Формально метод M объекта A имеет право вызывать методы только следующих 5 категорий сущностей:

# Категория разрешенного объекта Пример в Java
1 Самого объекта A (this) this.calculateSubtotal()
2 Объектов, переданных в параметрах метода M public void process(PaymentRequest req) { req.validate(); }
3 Экземпляров, созданных непосредственно внутри метода M StringBuilder sb = new StringBuilder(); sb.append("...");
4 Прямых полей экземпляра объекта A (компонентов композиции) this.orderRepository.save(order);
5 Глобальных констант или статических переменных Math.max(a, b);

❌ Строго запрещено: Вызывать методы объекта, который был возвращен в результате вызова другого метода: a.getB().getC().doAction().


Закон Деметры против Fluent API и Stream API

Один из главных вопросов на собеседовании: «Нарушает ли Stream API или StringBuilder закон Деметры?»

// Нарушает ли это LoD?
List<String> names = users.stream()
    .filter(User::isActive)
    .map(User::getName)
    .toList();

Ответ: НЕТ, не нарушает! Закон Деметры запрещает навигацию по чужой объектной структуре (Structural Navigation). В цепочках Stream API, StringBuilder, Optional или UserBuilder мы работаем в рамках единого контекста конвейера трансформации данных (Pipeline / Fluent Interface). Каждый промежуточный шаг возвращает объект того же концептуального типа или обертку, не раскрывая внутренних приватных графов зависимостей сторонних сервисов.


LoD и DTO (Data Transfer Objects)

Применим ли Закон Деметры к объектам DTO или структурам данных? Роберт Мартин в книге «Clean Code» проводит строгую границу:

  • Объекты (Objects): Скрывают свои данные за абстракциями и выставляют наружу поведение. К ним Закон Деметры применяется на 100%.
  • Структуры данных (Data Structures / DTO / Java Records): Не имеют поведения, их цель — прозрачно переносить сырые данные. К ним Закон Деметры не применяется, так как у DTO нет инкапсуляции поведения, которую можно было бы нарушить. Цепочка orderDto.customer().address().city() абсолютно легитимна.

🔴 Senior Level

Влияние нарушения LoD на производительность JVM и JIT-компилятор

Каждый вызов метода в цепочке a.getB().getC().getD() транслируется в байткод invokevirtual или invokeinterface:

INVOKEVIRTUAL Order.getCustomer()LCustomer;
INVOKEVIRTUAL Customer.getAddress()LAddress;
INVOKEVIRTUAL Address.getCity()LCity;
INVOKEVIRTUAL City.getName()Ljava/lang/String;

Проблемы машинного уровня:

  1. Срыв Method Inlining компилятором C2: JIT-компилятор HotSpot имеет лимиты глубины инлайнинга (MaxInlineLevel = 9) и максимального размера байткода (InlineSmallCode = 1000). Длинные цепочки вызовов через интерфейсы вызывают девиртуализационный паралич, заставляя CPU выполнять 4 последовательных поиска по таблицам VTable/ITable.
  2. Pointer Chasing и Cache Misses в CPU: Каждое разыменование ссылки в длинной цепочке — это прыжок в случайную область оперативной памяти кучи (Heap). Если объекты Order, Customer, Address и City были созданы в разное время и разнесены по страницам памяти, процессор получает до 4 последовательных промахов кэша (L1/L2 Cache Misses), останавливая выполнение конвейера на сотни тактов.

Многопоточные риски: Состояние гонки (Race Condition) в цепочках

В конкурентной многопоточной среде нарушение Закона Деметры порождает скрытые фатальные дефекты:

// Поток 1:
if (account.getProfile().getSettings().isTwoFactorEnabled()) {
    // В этот момент Поток 2 заменяет Settings на новый объект!
    account.getProfile().getSettings().verifyCode(code); // NPE или проверка старого объекта!
}

Между вызовами getProfile() и getSettings() состояние внешнего объекта может измениться другим потоком. Соблюдение LoD устраняет проблему: Вызывается атомарный метод account.verifyTwoFactor(code), внутри которого объект синхронизирует доступ к своим внутренним полям в рамках одной критической секции.


4 Tricky Questions

1. Почему Закон Деметры называют «Законом», хотя формально это эвристический принцип проектирования?

Ответ: Термин возник в 1987 году в Северо-Восточном университете (США) в рамках исследовательского проекта The Demeter Project, названного в честь Деметры — древнегреческой богини земледелия и порядка. Исследователи назвали открытое правило «Законом» из-за строгости его соблюдения в создаваемой системе: компилятор Demeter автоматически запрещал вызовы, нарушающие границы непосредственного соседа. В современной индустрии это рекомендация (эвристика), а не непреложный закон.

2. Приводит ли строгое следование Закону Деметры к антипаттерну Middle Man (Посредник)?

Ответ: Да, это главный архитектурный trade-off Закона Деметры. Если слепо бороться с каждой цепочкой вызовов, промежуточные классы начинают обрастать сотнями методов-транзитеров (методов-прокладок), которые просто перенаправляют вызовы дальше (order.getCityName(), order.getStreetName(), order.getHouseNumber(), order.getPostalCode()). Класс превращается в антипаттерн Middle Man. Решение: Если клиенту требуется вся группа данных адреса целиком, следует передавать сам объект-значение (Value Object) Address как единый параметр, либо использовать DTO-проекции.

3. Нарушает ли Spring Framework Закон Деметры при вызове context.getBean(Service.class).doSomething()?

Ответ: Да, вызов applicationContext.getBean(...).method() нарушает Закон Деметры (и одновременно является антипаттерном Service Locator). Класс берет зависимость у контекста и сразу вызывает у нее методы. Правильный подход в духе Spring — использование Dependency Injection (DI) через конструктор. В этом случае контейнер внедряет зависимость напрямую в поле класса, и метод сервиса общается со своим прямым полем (правило #4 Закона Деметры), не обращаясь к контейнеру.

4. Нарушает ли паттерн Builder Закон Деметры?

Ответ: Нет. При конструировании объекта через Builder:

User user = User.builder().name("Ivan").age(30).build();

Все вызовы .name() и .age() выполняются над одним и тем же объектом билдера, который возвращает сам себя (return this). Здесь нет навигации по вложенным внутренностям чужих независимых бизнес-сущностей, это пошаговая конфигурация одного объекта перед его созданием.


🎯 Шпаргалка для интервью

Главные тезисы (30 секунд на ответ):

  • Суть: Объект должен взаимодействовать только со своими прямыми соседями (полями, аргументами, локально созданными объектами) и не лезть во внутренности чужих структур.
  • Главный антипаттерн: «Крушение поезда» (Train Wreck) вида order.getCustomer().getAddress().getCity().
  • Следствие нарушения: Хрупкость (изменение в одном классе ломает 10 сервисов), засилье вложенных моков в тестах, высокий риск NullPointerException.
  • Принцип «Tell, Don’t Ask»: Вместо запроса данных наружу отдайте команду объекту выполнить действие самому.
  • Исключения: Fluent API, Stream API, Builder и структуры данных (DTO / Records).

Чего избегать (Red Flags):

  • ❌ Называть вызовы Stream API или StringBuilder нарушением Закона Деметры (это конвейеры данных, а не структурная навигация).
  • ❌ Доводить LoD до фанатизма, создавая 50 методов-делегатов в классе на каждое чих (риск антипаттерна Middle Man).
  • ❌ Применять LoD к анемичным DTO-объектам, лишенным поведения.

Связанные темы: