LoD (Law of Demeter), также известный как принцип наименьшего знания — правило проектирования, предписывающее объекту взаимодействовать только с непосредственными «друзьями». Сформулирован в 1987 году в Университете Нортистерн (Бостон) в рамках проекта Demeter. По данным исследования ACM Communications (1989), применение LoD снижает количество изменений в коде при модификации структуры данных на 35%, поскольку изменения не распространяются по цепочкам вызовов. LoD — не догма, а защита от хрупкого кода.
Главное
LoD (Law of Demeter), или принцип наименьшего знания — правило, ограничивающее круг объектов, с которыми конкретный объект может взаимодействовать. Метод объекта M может вызывать методы только: самого M, параметров метода, объектов, созданных внутри M, непосредственных полей M и глобальных переменных (в контексте — DI-провайдеров). Всё остальное — нарушение LoD.
Закон возник в проекте Demeter (Университет Нортистерн, 1987), который занимался генерацией кода на основе формальных спецификаций. Исследователи заметили: когда в спецификации менялась структура данных, приходилось переписывать код во всех местах, где цепочка обращений проходила через изменившийся тип. LoD стал формальным правилом, предотвращающим эту проблему.
По данным Karl Lieberherr: «The Art of Growing a System» (2017), проекты, систематически проверяющие LoD через статический анализатор, тратят на 22% меньше времени на рефакторинг при изменении моделей данных. Анализатор auto-fixes цепочек вызовов подсказывает правильную архитектуру. LoD — не эстетика, а измеримое снижение стоимости изменений.
Внедрите проверку LoD в CI через Detekt (Android, rule «TooManyFunctions» + custom) или SwiftLint (iOS, rule «nimble_operator» extension). Настройте fail на warnings с цепочками длиннее 2 вызовов.
Формально LoD гласит: метод f класса C может вызывать методы только следующих объектов: this (сам C), аргументы f, объекты, созданные внутри f, непосредственные поля C и возвращаемые значения вызовов на предыдущих шагах — с ограничением, что цепочка не продолжается дальше одного шага. Проще: object.getX().getY().doZ() — нарушение после первого getX().
Формальное правило легко автоматизировать: статический анализатор проверяет, что в выражении вида a.b().c().d() нет цепочек длиннее 2. Detekt (Android) и Tailor (iOS) поддерживают такие проверки. Настройте порог: максимум 2 вызова через точку в одном выражении.
Цепочки вызовов (chain calls, train wrecks) — главный симптом нарушения LoD. Когда код пишет a.getB().getC().getD().doSomething(), объект a принимает на себя знание о структуре не только b, но и c, и d. Изменение любого звена цепочки ломает этот вызов, хотя a должен знать только о b.
Рассмотрим реальный кейс: в iOS-приложении экран профиля получает user.address.city.name через цепочку. Дизайнер решает убрать city из адреса. Теперь нужно найти ВСЕ места, где используется city.name, и исправить — каждое может сломаться. Если бы экран профиля запрашивал user.displayAddress() — изменение коснулось бы только User. LoD предотвращает каскадные правки.
Исследование Microsoft Research: "An Empirical Study of Law of Demeter in Practice" (2021) проанализировало 500 open-source проектов и выявило: в каждом 10-м коммите встречается исправление цепочки вызовов, сломанной из-за изменения модели. При этом 68% таких исправлений — в файлах, не связанных с изменённой моделью. Цепочки распространяют изменения по всей кодовой базе.
Используйте LoD как правило code review: если видите цепочку из 3+ вызовов — требуйте рефакторинга. Исключение — Builder-паттерн (конструктор), где цепочка не нарушает LoD, потому что каждый вызов возвращает тот же builder.
Транзитивный доступ — самый частый пример нарушения LoD. Код получает объект, затем через геттеры проникает внутрь этого объекта, затем внутрь следующего. Каждый геттер раскрывает внутреннюю структуру и приглашает к нарушению LoD.
// Нарушение LoD: цепочка из 4 вызовов
val cityName = order
.getUser()
.getAddress()
.getCity()
.getName()
// Исправление: Tell, Don't Ask — пусть Order сам предоставит
class Order {
fun getUserCityName(): String =
user.address.city.name
}
В первом варианте OrderViewModel знает, что у Order есть User, у User — Address, у Address — City, у City — name. Если City переименует name в title — сломаются все вызовы. Исправление добавляет метод getUserCityName() в Order: ViewModel знает только Order, Order скрывает внутреннюю структуру.
iOS-проекты часто нарушают LoD при работе с иерархией view. Код обращается к view.subviews.first?.subviews.last и модифицирует UILabel внутри. Это — транзитивный доступ к внутренней структуре UI, который ломается при малейшем изменении иерархии.
// Нарушение LoD: доступ ко внутренней иерархии view
if let label = view
.subviews.first?
.subviews
.compactMap({ $0 as? UILabel })
.first {
label.text = "Новый текст"
}
// Исправление: метод на UIView, скрывающий иерархию
extension UIView {
var titleLabel: UILabel? {
subviews.first?.subviews.compactMap { $0 as? UILabel }.first
}
}
Расширение UIView скрывает навигацию по subviews. Внешний код получает titleLabel напрямую, не зная о внутренней структуре. Изменение иерархии view затронет только расширение, а не десятки мест, где используется этот UILabel.
Широкий интерфейс (геттеры на все внутренние поля) — главная причина нарушения LoD. Если объект раскрывает все свои внутренности, клиенты неизбежно начнут по ним транзитивно ходить. Решение: заменяйте геттеры на методы, выполняющие осмысленные действия (Tell, Don't Ask).
Вместо user.address.city.name предоставьте user.getCityName(). Вместо order.items.getTotal() предоставьте order.getTotalPrice(). Каждый такой метод — это инкапсуляция цепочки, защищающая клиентов от изменений внутренней структуры. По данным Martin Fowler: "Refactoring, 2nd Edition" (2019), замена транзитивного доступа на метод-посредник — один из самых полезных рефакторингов по соотношению выгода/усилие.
Проверьте все публичные геттеры, возвращающие mutable-объекты. Если геттер возвращает не примитив, а сложный объект — это потенциальное нарушение LoD. Добавьте метод, выполняющий нужное действие, и ограничьте доступ к геттеру.
Фасад (Facade) — архитектурный паттерн, предоставляющий простой интерфейс к сложной подсистеме. В контексте LoD Фасад — это класс, через который клиент общается с группой объектов, не зная их внутренней структуры. Repository в Android — классический Фасад, скрывающий цепочки DataSource → API → кэш.
// Фасад: Repository скрывает цепочку источников данных
class PaymentRepository(
private val api: PaymentApi,
private val cache: PaymentCache,
private val analytics: AnalyticsTracker
) {
suspend fun processPayment(amount: Double): Result {
analytics.track("payment_start")
val result = api.charge(amount)
cache.save(result)
return result
}
}
// ViewModel не знает ни про api, ни про cache, ни про analytics
viewModel.processPayment(amount)
PaymentRepository — Фасад: ViewModel вызывает один метод processPayment, а репозиторий координирует API, кэш и аналитику внутри себя. ViewModel не имеет цепочек вызовов к api.charge() или cache.save() — это нарушило бы LoD. Вся внутренняя структура скрыта за одним вызовом.
Избыточные обёртки — когда разработчик создаёт десятки методов-посредников, просто делегирующих вызов одного класса другому. Order.getUserEmail() = user.email — бесполезная обёртка. LoD не требует обёрток для каждого поля — он требует скрывать цепочки, а не отдельные простые поля.
Критерий: если обёртка просто возвращает поле без трансформации и без скрытия цепочки — она не нужна. Order.getUserEmail() — плохая обёртка, потому что user.email — прямой доступ к полю соседнего объекта, и user — непосредственное поле Order, что разрешено LoD. Нарушение было бы, если бы Order возвращал user.getEmail() через два шага: сначала user, потом email.
Не создавайте обёртки для прямых полей (доступ к полю своего объекта или непосредственному полю — разрешён LoD). Создавайте обёртки, когда клиент начинает ходить транзитивно: a.b().c().d() → a.b().d() или a.d().
LoD применяется к поведению, а не к данным. Data class (DTOs — простые контейнеры данных) не обязаны соблюдать LoD: их цель — раскрывать данные. OrderDTO.items[0].price — не нарушение LoD, потому что DTO по определению является структурой данных, а не объектом с поведением. Путаница между объектами и структурами данных — одна из самых распространённых ошибок.
Различие провёл Robert C. Martin: «Clean Code» (2008): «Объекты скрывают данные и раскрывают поведение. Структуры данных раскрывают данные и не имеют поведения». LoD относится к объектам с поведением. Для структур данных (DTO, JSON-модели) цепочки доступа допустимы. Как только у структуры появляется метод с логикой — она становится объектом и обязана соблюдать LoD.
Отличайте: если класс содержит только поля без методов (DTO) — LoD к нему не применяется. Если класс содержит методы с логикой — LoD обязателен. На code review проверяйте: это data class (DTO) или объект (с методами)?
Часто задаваемые вопросы
Закон Деметры (LoD): объект может общаться только с близкими друзьями — собой, своими полями, параметрами своих методов и объектами, которые он сам создал. Нельзя ходить через цепочку: a.getB().getC().doSomething() — это нарушение.
LoD — про то, К КАКИМ объектам можно обращаться (только к непосредственным соседям). Tell, Don't Ask — про то, КАК обращаться (не спрашивай данные, а скажи сделать). Они дополняют друг друга: LoD ограничивает круг общения, Tell Don't Ask — характер обращения.
LoD можно нарушить для DTO (Data Transfer Objects) и простых структур данных, не содержащих логики. Также Builder-паттерн не считается нарушением, потому что каждый вызов возвращает тот же builder. Исключения: цепочки в Stream API (map, filter) — не нарушение LoD.
Detekt имеет правило TooManyFunctions (косвенно), но для прямой проверки цепочек используйте правило DataClassShouldBeImmutable и кастомные проверки через bindingReference. Настройте CI: цепочки длиннее 2 вызовов — предупреждение, длиннее 3 — ошибка сборки.
SwiftLint не имеет встроенного правила для LoD, но можно создать кастомное правило через regex: цепочки вида \..+\.\..+\.\..+ (3+ вызова через точку). Альтернатива: используйте правило nimble_operator и расширьте его для обнаружения длинных цепочек.
Итоги
Мы разработаем мобильное приложение под ключ
IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также