LoD (Law of Demeter), más néven a legkevesebb tudás elve — egy tervezési szabály, amely előírja, hogy egy objektum csak a közvetlen „barátaival” lépjen kapcsolatba. 1987-ben fogalmazták meg a Northeastern University-n (Boston) a Demeter projekt keretében. A ACM Communications (1989) kutatása szerint az LoD alkalmazása 35%-kal csökkenti a kódváltoztatások számát az adatstruktúra módosításakor, mivel a változtatások nem terjednek tovább a hívási láncok mentén. LoD — nem dogma, hanem védelem a törékeny kód ellen.
Főbb pontok
LoD (Law of Demeter), vagy a legkevesebb tudás elve — egy szabály, amely korlátozza azon objektumok körét, amelyekkel egy adott objektum kapcsolatba léphet. Az M objektum metódusa csak a következők metódusait hívhatja meg: maga M, a metódus paraméterei, az M-en belül létrehozott objektumok, M közvetlen mezői és globális változók (kontextusban — DI-szolgáltatók). Minden más — LoD-sértés.
A törvény a Demeter projektben (Northeastern University, 1987) keletkezett, amely formális specifikációk alapján kódgenerálással foglalkozott. A kutatók észrevették: amikor az adatstruktúra megváltozott a specifikációban, át kellett írni a kódot minden olyan helyen, ahol a hozzáférési lánc a megváltozott típuson haladt keresztül. LoD formális szabállyá vált, amely megakadályozza ezt a problémát.
Karl Lieberherr: „The Art of Growing a System” (2017) szerint azok a projektek, amelyek szisztematikusan ellenőrzik az LoD-t statikus elemzővel, 22%-kal kevesebb időt töltenek refactoringra az adatmodellek megváltoztatásakor. A hívási láncok auto-fix elemzője a helyes architektúrát javasolja. LoD — nem esztétika, hanem a változtatások költségének mérhető csökkentése.
Valósítsa meg az LoD ellenőrzését CI-ban Detekt (Android, „TooManyFunctions” szabály + egyéni) vagy SwiftLint (iOS, „nimble_operator” kiterjesztés) segítségével. Állítsa be a fail-t a 2-nél hosszabb hívási láncokra vonatkozó figyelmeztetésekre.
Formálisan az LoD kimondja: a C osztály f metódusa csak a következő objektumok metódusait hívhatja meg: this (maga C), f argumentumai, f-en belül létrehozott objektumok, C közvetlen mezői és az előző lépésekben lévő hívások visszatérési értékei — azzal a korlátozással, hogy a lánc nem folytatódik tovább egy lépésnél. Egyszerűbben: object.getX().getY().doZ() — sértés az első getX() után.
A formális szabály könnyen automatizálható: a statikus elemző ellenőrzi, hogy az a.b().c().d() formájú kifejezésben nincsenek-e 2-nél hosszabb láncok. Detekt (Android) és Tailor (iOS) támogatják az ilyen ellenőrzéseket. Állítsa be a küszöböt: maximum 2 hívás ponton keresztül egy kifejezésben.
Hívási láncok (chain calls, train wrecks) — az LoD-sértés fő tünete. Amikor a kód a.getB().getC().getD().doSomething()-et ír, az a objektum nemcsak b, hanem c és d szerkezetéről is tudomást szerez. Változtatás a lánc bármely szeme megtöri ezt a hívást, holott a-nak csak b-ről kellene tudnia.
Vegyünk egy valós esetet: egy iOS alkalmazásban a profilképernyő user.address.city.name-et kér le egy láncon keresztül. A tervező úgy dönt, hogy eltávolítja a city-t a címből. Most meg kell találni az ÖSSZES helyet, ahol city.name-et használják, és javítani — mindegyik elromolhat. Ha a profilképernyő user.displayAddress()-t kért volna — a változtatás csak User-t érintette volna. LoD megakadályozza a lépcsőzetes javításokat.
A Microsoft Research: „An Empirical Study of Law of Demeter in Practice” (2021) kutatás 500 nyílt forráskódú projektet elemzett, és kimutatta: minden 10. commitban előfordul egy modellváltozás miatt eltört hívási lánc javítása. Ugyanakkor az ilyen javítások 68%-a — a megváltoztatott modellhez nem kapcsolódó fájlokban található. Láncok terjesztik a változtatásokat az egész kódbázisban.
Használja az LoD-t code review szabályként: ha 3+ hívásból álló láncot lát — kérjen refactoringot. Kivétel — Builder (konstruktor), ahol a lánc nem sérti az LoD-t, mivel minden hívás ugyanazt a builder-t adja vissza.
Tranzitív hozzáférés — az LoD-sértés leggyakoribb példája. A kód lekér egy objektumot, majd gettereken keresztül behatol ennek az objektumnak a belsejébe, majd a következőébe. Minden getter feltárja a belső szerkezetet, és LoD-sértésre hív.
// LoD-sértés: 4 hívásból álló lánc
val cityName = order
.getUser()
.getAddress()
.getCity()
.getName()
// Javítás: Tell, Don't Ask — hagyja, hogy Order maga szolgáltassa
class Order {
fun getUserCityName(): String =
user.address.city.name
}
Az első változatban az OrderViewModel tudja, hogy az Order-ben van User, a User-ben Address, az Address-ben City, a City-ben name. Ha a City átnevezi a name-et title-re — minden hívás elromlik. Javítás hozzáadja a getUserCityName() metódust az Order-hez: a ViewModel csak Order-t ismer, az Order elrejti a belső szerkezetet.
Az iOS projektek gyakran megsértik az LoD-t a view hierarchiával való munka során. A kód hozzáfér a view.subviews.first?.subviews.last-hez, és módosítja a benne lévő UILabel-t. Ez — tranzitív hozzáférés a UI belső szerkezetéhez, amely a hierarchia legkisebb változásakor elromlik.
// LoD-sértés: hozzáférés a view belső hierarchiájához
if let label = view
.subviews.first?
.subviews
.compactMap({ $0 as? UILabel })
.first {
label.text = "Új szöveg"
}
// Javítás: metódus az UIView-en, amely elrejti a hierarchiát
extension UIView {
var titleLabel: UILabel? {
subviews.first?.subviews.compactMap { $0 as? UILabel }.first
}
}
Az UIView kiterjesztés elrejti a subviews közötti navigációt. A külső kód közvetlenül kapja meg a titleLabel-t, anélkül hogy ismerné a belső szerkezetet. Változtatás a view hierarchiában csak a kiterjesztést érinti, nem a tucatnyi helyet, ahol ezt a UILabel-t használják.
Széles interfész (getterek az összes belső mezőre) — az LoD-sértés fő oka. Ha az objektum feltárja az összes belső részét, az ügyfelek elkerülhetetlenül tranzitíven kezdenek járni rajtuk. Megoldás: cserélje ki a gettereket jelentéssel bíró műveleteket végrehajtó metódusokra (Tell, Don't Ask).
A user.address.city.name helyett biztosítson user.getCityName()-t. Az order.items.getTotal() helyett biztosítson order.getTotalPrice()-t. Minden ilyen metódus — a lánc beágyazása, amely védi az ügyfeleket a belső szerkezet változásaitól. Martin Fowler: „Refactoring, 2nd Edition” (2019) szerint a tranzitív hozzáférés közvetítő metódussal való helyettesítése — az egyik leghasznosabb refactoring a haszon/erőfeszítés arány alapján.
Ellenőrizze az összes publikus gettert, amely mutable objektumokat ad vissza. Ha egy getter nem primitívet, hanem összetett objektumot ad vissza — ez potenciális LoD-sértés. Adjon hozzá egy metódust, amely végrehajtja a szükséges műveletet, és korlátozza a getterhez való hozzáférést.
Facade — architekturális minta, amely egyszerű interfészt biztosít egy összetett alrendszerhez. Az LoD kontextusában a Facade egy osztály, amelyen keresztül az ügyfél kommunikál egy objektumcsoporttal anélkül, hogy ismerné azok belső szerkezetét. Repository Android-ban — klasszikus Facade, amely elrejti a DataSource → API → cache láncokat.
// Facade: A Repository elrejti az adatforrások láncát
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 nem tud sem api-ról, sem cache-ről, sem analytics-ról
viewModel.processPayment(amount)
PaymentRepository — Facade: a ViewModel egyetlen processPayment metódust hív, a repository pedig koordinálja az API-t, cache-t és analitikát magában. ViewModel-nek nincsenek hívási láncai az api.charge()-hoz vagy cache.save()-hez — ez sértené az LoD-t. A teljes belső szerkezet egyetlen hívás mögé van rejtve.
Túlzott wrapperek — amikor a programozó tucatnyi közvetítő metódust hoz létre, amelyek egyszerűen átirányítják a hívást egyik osztályból a másikba. Order.getUserEmail() = user.email — haszontalan wrapper. LoD nem kér wrappert minden mezőhöz — azt kéri, hogy rejtse el a láncokat, nem az egyes egyszerű mezőket.
Kritérium: ha a wrapper egyszerűen visszaad egy mezőt transzformáció és láncelrejtés nélkül — nem szükséges. Order.getUserEmail() — rossz wrapper, mert a user.email közvetlen hozzáférés a szomszédos objektum mezőjéhez, és a user Order közvetlen mezője, amit az LoD engedélyez. Sértés akkor lenne, ha az Order user.getEmail()-t két lépésben adná vissza: először user, majd email.
Ne hozzon létre wrappereket közvetlen mezőkhöz (hozzáférés a saját objektum mezőjéhez vagy közvetlen mezőhöz — az LoD által engedélyezett). Akkor hozzon létre wrappert, amikor az ügyfél tranzitíven kezd járni: a.b().c().d() → a.b().d() vagy a.d().
LoD a viselkedésre vonatkozik, nem az adatokra. A data class (DTO — egyszerű adattárolók) nem kötelesek betartani az LoD-t: céljuk az adatok feltárása. OrderDTO.items[0].price — nem LoD-sértés, mert a DTO definíció szerint adatstruktúra, nem viselkedéssel rendelkező objektum. Összetévesztés az objektumok és adatstruktúrák között — az egyik leggyakoribb hiba.
A különbséget Robert C. Martin: „Clean Code” (2008) mutatta be: „Az objektumok elrejtik az adatokat és feltárják a viselkedést. Az adatstruktúrák feltárják az adatokat és nem rendelkeznek viselkedéssel.” Az LoD a viselkedéssel rendelkező objektumokra vonatkozik. Az adatstruktúrák (DTO, JSON modellek) esetében a hozzáférési láncok megengedettek. Amint egy struktúra logikával rendelkező metódust kap — objektummá válik, és be kell tartania az LoD-t.
Különböztesse meg: ha egy osztály csak mezőket tartalmaz metódusok nélkül (DTO) — az LoD nem alkalmazandó rá. Ha egy osztály logikával rendelkező metódusokat tartalmaz — az LoD kötelező. Code review során ellenőrizze: ez data class (DTO) vagy objektum (metódusokkal)?
Gyakran Ismételt Kérdések
Démétér törvénye (LoD): egy objektum csak közeli barátaival kommunikálhat — önmagával, mezőivel, metódusai paramétereivel és azokkal az objektumokkal, amelyeket ő maga hozott létre. Nem lehet láncon keresztül menni: a.getB().getC().doSomething() — ez sértés.
LoD — arról szól, hogy MELYIK objektumokhoz lehet fordulni (csak közvetlen szomszédokhoz). Tell, Don't Ask — arról szól, hogy HOGYAN kell fordulni (ne kérdezz adatokat, hanem mondd meg, hogy csinálja). Kiegészítik egymást: az LoD korlátozza a kommunikációs kört, a Tell Don't Ask — a fordulás jellegét.
LoD megsérthető DTO (Data Transfer Objects) és egyszerű, logikát nem tartalmazó adatstruktúrák esetében. Továbbá a Builder nem minősül sértésnek, mert minden hívás ugyanazt a builder-t adja vissza. Kivételek: láncok a Stream API-ban (map, filter) — nem LoD-sértés.
Detekt rendelkezik a TooManyFunctions szabállyal (közvetve), de a láncok közvetlen ellenőrzéséhez használja a DataClassShouldBeImmutable szabályt és egyéni ellenőrzéseket a bindingReference segítségével. Állítsa be a CI-t: 2-nél hosszabb láncok — figyelmeztetés, 3-nál hosszabbak — build hiba.
SwiftLint nem rendelkezik beépített szabállyal az LoD-hez, de létrehozható egyéni szabály regex segítségével: \..+\.\..+\.\..+ formájú láncok (3+ hívás ponton keresztül). Alternatíva: használja a nimble_operator szabályt, és bővítse ki a hosszú láncok észlelésére.
Összegzés
Kulcsrakész mobilalkalmazást fejlesztünk
Az IT Sectr 2017 óta készít iOS és Android alkalmazásokat induló vállalkozásoknak és vállalkozásoknak. Tanácsot adunk, és a legjobb megoldást javasoljuk.
Olvassa el is