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% менше часу на рефакторинг при зміні моделей даних. Автоматичні виправлення аналізатора для ланцюжків викликів підказують правильну архітектуру. LoD — не естетика, а вимірюване зниження вартості змін.
Впровадьте перевірку LoD в CI через Detekt (Android, правило «TooManyFunctions» + кастомне) або SwiftLint (iOS, правило «nimble_operator» розширення). Налаштуйте на попередження з ланцюжками довшими за 2 виклики.
Формально LoD стверджує: метод f класу C може викликати лише методи наступних об’єктів: this (сам C), аргументи f, об’єкти, створені всередині f, безпосередні поля C та значення, що повертаються з попередніх кроків — з обмеженням, що ланцюжка не продовжується далі одного кроку. Простіше: object.getX().getY().doZ() — порушення після першого getX().
Формальне правило легко автоматизувати: статичний аналізатор перевіряє, що в виразах типу a.b().c().d() немає ланцюжок довшими за 2. Detekt (Android) та Tailor (iOS) підтримують такі перевірки. Налаштуйте поріг: максимум 2 виклики через крапку в одному виразі.
Ланцюжки викликів (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 опен-сорс проектів і виявило: кожний 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 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також