LoD (Law of Demeter), také známý jako princip nejmenších znalostí — pravidlo návrhu, které nařizuje objektu interagovat pouze s bezprostředními „přáteli“. Formulován v roce 1987 na Northeastern University (Boston) v rámci projektu Demeter. Podle výzkumu ACM Communications (1989), použití LoD snižuje počet změn v kódu při modifikaci datové struktury o 35%, protože změny se nešíří podél řetězců volání. LoD — není dogma, ale ochrana před křehkým kódem.
Hlavní
LoD (Law of Demeter), nebo princip nejmenších znalostí — pravidlo omezující okruh objektů, se kterými může daný objekt interagovat. Metoda objektu M může volat metody pouze: samotného M, parametrů metody, objektů vytvořených uvnitř M, přímých polí M a globálních proměnných (v kontextu — DI providerů). Vše ostatní — porušení LoD.
Zákon vznikl v projektu Demeter (Northeastern University, 1987), který se zabýval generováním kódu na základě formálních specifikací. Výzkumníci si všimli: když se ve specifikaci změnila datová struktura, musel se přepisovat kód na všech místech, kde řetězec přístupu procházel změněným typem. LoD se stal formálním pravidlem, které tomuto problému předchází.
Podle Karl Lieberherr: „The Art of Growing a System” (2017), projekty systematicky kontrolující LoD pomocí statického analyzátoru tráví o 22% méně času refaktorováním při změně datových modelů. Analyzátor auto-fix řetězců volání navrhuje správnou architekturu. LoD — není estetika, ale měřitelné snížení nákladů na změny.
Implementujte kontrolu LoD v CI pomocí Detekt (Android, pravidlo „TooManyFunctions” + vlastní) nebo SwiftLint (iOS, rozšíření „nimble_operator”). Nastavte fail na varování s řetězci delšími než 2 volání.
Formálně LoD říká: metoda f třídy C může volat metody pouze následujících objektů: this (samotné C), argumenty f, objekty vytvořené uvnitř f, přímá pole C a návratové hodnoty volání z předchozích kroků — s omezením, že řetězec nepokračuje dále než jeden krok. Jednodušeji: object.getX().getY().doZ() — porušení po prvním getX().
Formální pravidlo lze snadno automatizovat: statický analyzátor kontroluje, zda ve výrazu tvaru a.b().c().d() nejsou řetězce delší než 2. Detekt (Android) a Tailor (iOS) takové kontroly podporují. Nastavte práh: maximálně 2 volání přes tečku v jednom výrazu.
Řetězce volání (chain calls, train wrecks) — hlavní symptom porušení LoD. Když kód píše a.getB().getC().getD().doSomething(), objekt a přebírá znalost struktury nejen b, ale také c a d. Změna libovolného článku řetězce tento volání rozbíjí, přestože a by měl vědět pouze o b.
Zvažme reálný případ: v iOS aplikaci obrazovka profilu získává user.address.city.name přes řetězec. Designér se rozhodne odstranit city z adresy. Nyní je třeba najít VŠECHNA místa, kde se city.name používá, a opravit — každé se může rozbít. Pokud by obrazovka profilu požadovala user.displayAddress() — změna by se dotkla pouze User. LoD zabraňuje kaskádovým opravám.
Výzkum Microsoft Research: „An Empirical Study of Law of Demeter in Practice” (2021) analyzoval 500 open-source projektů a odhalil: v každém 10. commitu se vyskytuje oprava řetězce volání rozbitého kvůli změně modelu. Přitom 68% takových oprav — v souborech nesouvisejících se změněným modelem. Řetězce šíří změny po celé kódové bázi.
Používejte LoD jako pravidlo code review: pokud vidíte řetězec 3+ volání — požadujte refaktorování. Výjimka — Builder (konstruktor), kde řetězec neporušuje LoD, protože každé volání vrací stejný builder.
Tranzitivní přístup — nejčastější příklad porušení LoD. Kód získá objekt, poté přes gettery proniká dovnitř tohoto objektu, poté dovnitř dalšího. Každý getter odhaluje vnitřní strukturu a zve k porušení LoD.
// Porušení LoD: řetězec 4 volání
val cityName = order
.getUser()
.getAddress()
.getCity()
.getName()
// Oprava: Tell, Don't Ask — nechť Order sám poskytne
class Order {
fun getUserCityName(): String =
user.address.city.name
}
V první variantě OrderViewModel ví, že Order má User, User má Address, Address má City, City má name. Pokud City přejmenuje name na title — všechna volání se rozbijí. Oprava přidává metodu getUserCityName() do Order: ViewModel zná pouze Order, Order skrývá vnitřní strukturu.
iOS projekty často porušují LoD při práci s hierarchií view. Kód přistupuje k view.subviews.first?.subviews.last a modifikuje UILabel uvnitř. To — tranzitivní přístup k vnitřní struktuře UI, který se rozbije při sebemenší změně hierarchie.
// Porušení LoD: přístup k vnitřní hierarchii view
if let label = view
.subviews.first?
.subviews
.compactMap({ $0 as? UILabel })
.first {
label.text = "Nový text"
}
// Oprava: metoda na UIView skrývající hierarchii
extension UIView {
var titleLabel: UILabel? {
subviews.first?.subviews.compactMap { $0 as? UILabel }.first
}
}
Rozšíření UIView skrývá navigaci přes subviews. Externí kód získává titleLabel přímo, aniž by znal vnitřní strukturu. Změna hierarchie view se dotkne pouze rozšíření, nikoli desítek míst, kde se tento UILabel používá.
Široké rozhraní (gettery na všechna vnitřní pole) — hlavní příčina porušení LoD. Pokud objekt odhaluje všechny své vnitřnosti, klienti nevyhnutelně začnou tranzitivně chodit po nich. Řešení: nahraďte gettery metodami, které provádějí smysluplné akce (Tell, Don't Ask).
Místo user.address.city.name poskytněte user.getCityName(). Místo order.items.getTotal() poskytněte order.getTotalPrice(). Každá taková metoda — je zapouzdření řetězce, které chrání klienty před změnami vnitřní struktury. Podle Martin Fowler: „Refactoring, 2nd Edition” (2019), nahrazení tranzitivního přístupu zprostředkující metodou — jeden z nejužitečnějších refaktorů z hlediska poměru přínos/úsilí.
Zkontrolujte všechny veřejné gettery vracející měnitelné objekty. Pokud getter vrací ne primitiv, ale komplexní objekt — to je potenciální porušení LoD. Přidejte metodu provádějící potřebnou akci a omezte přístup k getteru.
Fasáda (Facade) — architektonický vzor poskytující jednoduché rozhraní ke komplexnímu subsystému. V kontextu LoD je Fasáda třída, přes kterou klient komunikuje se skupinou objektů, aniž by znal jejich vnitřní strukturu. Repository v Android — klasická Fasáda skrývající řetězce DataSource → API → cache.
// Fasáda: Repository skrývá řetězec zdrojů dat
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 neví ani o api, ani o cache, ani o analytics
viewModel.processPayment(amount)
PaymentRepository — Fasáda: ViewModel volá jednu metodu processPayment a repository koordinuje API, cache a analytiku uvnitř sebe. ViewModel nemá řetězce volání k api.charge() nebo cache.save() — to by porušilo LoD. Celá vnitřní struktura je skryta za jedním voláním.
Nadbytečné obaly — když programátor vytváří desítky zprostředkujících metod, které pouze delegují volání z jedné třídy do druhé. Order.getUserEmail() = user.email — zbytečný obal. LoD nevyžaduje obaly pro každé pole — vyžaduje skrývání řetězců, nikoli jednotlivých jednoduchých polí.
Kritérium: pokud obal pouze vrací pole bez transformace a bez skrytí řetězce — není potřeba. Order.getUserEmail() — špatný obal, protože user.email je přímý přístup k poli sousedního objektu a user je přímé pole Order, což je LoD povoleno. Porušení by bylo, kdyby Order vracel user.getEmail() ve dvou krocích: nejprve user, pak email.
Nevytvářejte obaly pro přímá pole (přístup k poli vlastního objektu nebo přímému poli — povoleno LoD). Vytvářejte obaly, když klient začne chodit tranzitivně: a.b().c().d() → a.b().d() nebo a.d().
LoD se aplikuje na chování, nikoli na data. Data class (DTO — jednoduché kontejnery dat) nejsou povinny dodržovat LoD: jejich účelem je odhalovat data. OrderDTO.items[0].price — není porušení LoD, protože DTO je z definice datová struktura, nikoli objekt s chováním. Záměna mezi objekty a datovými strukturami — jeden z nejčastějších omylů.
Rozdíl uvedl Robert C. Martin: „Clean Code” (2008): „Objekty skrývají data a odhalují chování. Datové struktury odhalují data a nemají chování.” LoD se týká objektů s chováním. Pro datové struktury (DTO, JSON modely) jsou přístupové řetězce povoleny. Jakmile struktura získá metodu s logikou — stává se objektem a musí dodržovat LoD.
Rozlišujte: pokud třída obsahuje pouze pole bez metod (DTO) — LoD se na ni nevztahuje. Pokud třída obsahuje metody s logikou — LoD je povinný. Při code review kontrolujte: je to data class (DTO) nebo objekt (s metodami)?
Často kladené otázky
Demeterův zákon (LoD): objekt může komunikovat pouze s blízkými přáteli — sebou samým, svými poli, parametry svých metod a objekty, které sám vytvořil. Nelze chodit přes řetězec: a.getB().getC().doSomething() — to je porušení.
LoD — o tom, K JAKÝM objektům lze přistupovat (pouze k bezprostředním sousedům). Tell, Don't Ask — o tom, JAK přistupovat (neptej se na data, ale řekni, ať to udělá). Vzájemně se doplňují: LoD omezuje okruh komunikace, Tell Don't Ask — charakter přístupu.
LoD lze porušit pro DTO (Data Transfer Objects) a jednoduché datové struktury bez logiky. Také Builder není považován za porušení, protože každé volání vrací stejný builder. Výjimky: řetězce v Stream API (map, filter) — nejsou porušením LoD.
Detekt má pravidlo TooManyFunctions (nepřímo), ale pro přímou kontrolu řetězců použijte pravidlo DataClassShouldBeImmutable a vlastní kontroly přes bindingReference. Nastavte CI: řetězce delší než 2 volání — varování, delší než 3 — chyba sestavení.
SwiftLint nemá vestavěné pravidlo pro LoD, ale lze vytvořit vlastní pravidlo pomocí regex: řetězce tvaru \..+\.\..+\.\..+ (3+ volání přes tečku). Alternativa: použijte pravidlo nimble_operator a rozšiřte jej pro detekci dlouhých řetězců.
Shrnutí
Vyvineme mobilní aplikaci na klíč
IT Sectr vytváří aplikace pro iOS a Android pro startupy a podniky od roku 2017. Poradíme vám a navrhneme nejlepší řešení.
Přečtěte si také