LoD (Law of Demeter), cunoscut și ca principiul cunoașterii minime — o regulă de proiectare care prescrie ca un obiect să interacționeze doar cu „prietenii” direcți. Formulat în 1987 la Northeastern University (Boston) în cadrul proiectului Demeter. Potrivit cercetării ACM Communications (1989), aplicarea LoD reduce numărul de modificări în cod la modificarea structurii datelor cu 35%, deoarece modificările nu se propagă de-a lungul lanțurilor de apeluri. LoD — nu o dogmă, ci o protecție împotriva codului fragil.
Principalele
LoD (Law of Demeter), sau principiul cunoașterii minime — o regulă care limitează cercul obiectelor cu care un anumit obiect poate interacționa. Metoda obiectului M poate apela metode doar: ale lui M însuși, parametrilor metodei, obiectelor create în interiorul M, câmpurilor directe ale M și variabilelor globale (în context — furnizorilor DI). Orice altceva — încălcare a LoD.
Legea a apărut în proiectul Demeter (Northeastern University, 1987), care se ocupa de generarea de cod pe baza specificațiilor formale. Cercetătorii au observat: atunci când structura datelor se schimba în specificație, trebuia rescris codul în toate locurile unde lanțul de acces trecea prin tipul modificat. LoD a devenit regula formală care previne această problemă.
Potrivit Karl Lieberherr: „The Art of Growing a System” (2017), proiectele care verifică sistematic LoD printr-un analizor static petrec cu 22% mai puțin timp pe refactorizare la modificarea modelelor de date. Analizorul auto-fix al lanțurilor de apeluri sugerează arhitectura corectă. LoD — nu estetică, ci o reducere măsurabilă a costului modificărilor.
Implementați verificarea LoD în CI prin Detekt (Android, regula „TooManyFunctions” + personalizată) sau SwiftLint (iOS, extensia „nimble_operator”). Configurați fail pe avertismente cu lanțuri mai lungi de 2 apeluri.
Formal LoD spune: metoda f a clasei C poate apela metode doar ale următoarelor obiecte: this (însuși C), argumentele lui f, obiectele create în interiorul lui f, câmpurile directe ale lui C și valorile returnate de apelurile de la pașii anteriori — cu restricția că lanțul nu continuă mai mult de un pas. Mai simplu: object.getX().getY().doZ() — încălcare după primul getX().
Regula formală se automatizează ușor: analizorul static verifică dacă în expresia de forma a.b().c().d() nu există lanțuri mai lungi de 2. Detekt (Android) și Tailor (iOS) suportă astfel de verificări. Setați pragul: maximum 2 apeluri prin punct într-o singură expresie.
Lanțurile de apeluri (chain calls, train wrecks) — principalul simptom al încălcării LoD. Când codul scrie a.getB().getC().getD().doSomething(), obiectul a își asumă cunoașterea structurii nu doar a lui b, ci și a lui c și d. Modificarea oricărei verigi a lanțului strică acest apel, deși a ar trebui să știe doar despre b.
Să luăm un caz real: într-o aplicație iOS ecranul de profil obține user.address.city.name printr-un lanț. Designerul decide să scoată city din adresă. Acum trebuie găsite TOATE locurile unde se folosește city.name și reparate — fiecare se poate strica. Dacă ecranul de profil ar fi cerut user.displayAddress() — modificarea ar fi atins doar User. LoD previne corecturile în cascadă.
Cercetarea Microsoft Research: „An Empirical Study of Law of Demeter in Practice” (2021) a analizat 500 de proiecte open-source și a relevat: în fiecare al 10-lea commit apare o corectură a unui lanț de apeluri stricat din cauza modificării modelului. În același timp, 68% din astfel de corecturi — în fișiere nelegate de modelul modificat. Lanțurile propagă modificările în întreaga bază de cod.
Folosiți LoD ca regulă de code review: dacă vedeți un lanț de 3+ apeluri — cereți refactorizare. Excepție — Builder (constructor), unde lanțul nu încalcă LoD, deoarece fiecare apel returnează același builder.
Accesul tranzitiv — cel mai frecvent exemplu de încălcare a LoD. Codul obține un obiect, apoi prin getter-e pătrunde în interiorul acestui obiect, apoi în interiorul următorului. Fiecare getter dezvăluie structura internă și invită la încălcarea LoD.
// Încălcarea LoD: lanț de 4 apeluri
val cityName = order
.getUser()
.getAddress()
.getCity()
.getName()
// Corectura: Tell, Don't Ask — lăsați Order să furnizeze singur
class Order {
fun getUserCityName(): String =
user.address.city.name
}
În prima variantă, OrderViewModel știe că Order are User, User are Address, Address are City, City are name. Dacă City redenumește name în title — toate apelurile se strică. Corectura adaugă metoda getUserCityName() în Order: ViewModel cunoaște doar Order, Order ascunde structura internă.
Proiectele iOS încalcă adesea LoD la lucrul cu ierarhia de view-uri. Codul accesează view.subviews.first?.subviews.last și modifică UILabel din interior. Aceasta — acces tranzitiv la structura internă a UI, care se strică la cea mai mică modificare a ierarhiei.
// Încălcarea LoD: acces la ierarhia internă a view-ului
if let label = view
.subviews.first?
.subviews
.compactMap({ $0 as? UILabel })
.first {
label.text = "Text nou"
}
// Corectura: metodă pe UIView care ascunde ierarhia
extension UIView {
var titleLabel: UILabel? {
subviews.first?.subviews.compactMap { $0 as? UILabel }.first
}
}
Extensia UIView ascunde navigarea prin subviews. Codul extern primește titleLabel direct, fără a cunoaște structura internă. Modificarea ierarhiei de view va afecta doar extensia, nu și zeci de locuri unde se folosește acest UILabel.
Interfața largă (getter-e pentru toate câmpurile interne) — principala cauză a încălcării LoD. Dacă obiectul dezvăluie toate interioritățile sale, clienții vor începe inevitabil să circule tranzitiv prin ele. Soluția: înlocuiți getter-ele cu metode care execută acțiuni semnificative (Tell, Don't Ask).
În loc de user.address.city.name, oferiți user.getCityName(). În loc de order.items.getTotal(), oferiți order.getTotalPrice(). Fiecare astfel de metodă — este o încapsulare a lanțului care protejează clienții de modificările structurii interne. Potrivit Martin Fowler: „Refactoring, 2nd Edition” (2019), înlocuirea accesului tranzitiv cu o metodă intermediară — unul dintre cele mai utile refactorizări după raportul beneficiu/efort.
Verificați toate getter-ele publice care returnează obiecte mutabile. Dacă un getter returnează nu un tip primitiv, ci un obiect complex — aceasta este o potențială încălcare LoD. Adăugați o metodă care execută acțiunea necesară și limitați accesul la getter.
Fațada (Facade) — model arhitectural care oferă o interfață simplă către un subsistem complex. În contextul LoD, Fațada este o clasă prin care clientul comunică cu un grup de obiecte, fără a cunoaște structura lor internă. Repository în Android — Fațada clasică, care ascunde lanțurile DataSource → API → cache.
// Fațada: Repository ascunde lanțul surselor de date
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 nu știe nici de api, nici de cache, nici de analytics
viewModel.processPayment(amount)
PaymentRepository — Fațada: ViewModel apelează o singură metodă processPayment, iar repository-ul coordonează API, cache și analitica în interiorul său. ViewModel nu are lanțuri de apeluri către api.charge() sau cache.save() — aceasta ar încălca LoD. Întreaga structură internă este ascunsă în spatele unui singur apel.
Wrapper-e excessive — când dezvoltatorul creează zeci de metode intermediare care doar delegă apelul de la o clasă la alta. Order.getUserEmail() = user.email — wrapper inutil. LoD nu cere wrapper-e pentru fiecare câmp — cere ascunderea lanțurilor, nu a câmpurilor simple separate.
Criteriu: dacă wrapper-ul doar returnează un câmp fără transformare și fără ascunderea lanțului — nu este necesar. Order.getUserEmail() — wrapper prost, deoarece user.email este acces direct la câmpul obiectului vecin, iar user este câmp direct al lui Order, ceea ce este permis de LoD. Încălcarea ar fi dacă Order ar returna user.getEmail() prin doi pași: întâi user, apoi email.
Nu creați wrapper-e pentru câmpuri directe (accesul la câmpul propriului obiect sau la câmpul direct — permis de LoD). Creați wrapper-e când clientul începe să circule tranzitiv: a.b().c().d() → a.b().d() sau a.d().
LoD se aplică comportamentului, nu datelor. Data class (DTO — containere simple de date) nu sunt obligate să respecte LoD: scopul lor este de a dezvălui date. OrderDTO.items[0].price — nu este o încălcare LoD, deoarece DTO prin definiție este o structură de date, nu un obiect cu comportament. Confuzia între obiecte și structuri de date — una dintre cele mai frecvente greșeli.
Distincția a fost făcută de Robert C. Martin: „Clean Code” (2008): „Obiectele ascund datele și dezvăluie comportamentul. Structurile de date dezvăluie datele și nu au comportament”. LoD se referă la obiecte cu comportament. Pentru structuri de date (DTO, modele JSON) lanțurile de acces sunt permise. Imediat ce o structură capătă o metodă cu logică — devine obiect și trebuie să respecte LoD.
Distingeți: dacă o clasă conține doar câmpuri fără metode (DTO) — LoD nu i se aplică. Dacă o clasă conține metode cu logică — LoD este obligatoriu. La code review verificați: este data class (DTO) sau obiect (cu metode)?
Întrebări frecvente
Legea lui Demeter (LoD): un obiect poate comunica doar cu prietenii apropiați — sine însuși, câmpurile sale, parametrii metodelor sale și obiectele pe care el însuși le-a creat. Nu se poate merge printr-un lanț: a.getB().getC().doSomething() — aceasta este o încălcare.
LoD — despre CARE obiecte pot fi accesate (doar vecinii direcți). Tell, Don't Ask — despre CUM să accesezi (nu întreba datele, ci spune să facă). Ele se completează reciproc: LoD limitează cercul de comunicare, Tell Don't Ask — caracterul accesului.
LoD poate fi încălcat pentru DTO (Data Transfer Objects) și structuri simple de date fără logică. De asemenea, Builder nu este considerat o încălcare, deoarece fiecare apel returnează același builder. Excepții: lanțurile în Stream API (map, filter) — nu sunt încălcări LoD.
Detekt are regula TooManyFunctions (indirect), dar pentru verificarea directă a lanțurilor folosiți regula DataClassShouldBeImmutable și verificări personalizate prin bindingReference. Configurați CI: lanțuri mai lungi de 2 apeluri — avertizare, mai lungi de 3 — eroare de compilare.
SwiftLint nu are o regulă încorporată pentru LoD, dar se poate crea o regulă personalizată prin regex: lanțuri de forma \..+\.\..+\.\..+ (3+ apeluri prin punct). Alternativă: folosiți regula nimble_operator și extindeți-o pentru detectarea lanțurilor lungi.
Rezumat
Vom dezvolta o aplicație mobilă la cheie
IT Sectr creează aplicații iOS și Android pentru startup-uri și afaceri din 2017. Vă vom consilia și vă vom propune cea mai bună soluție.
Citiți și