LoD (Law of Demeter), noto anche come principio di conoscenza minima — una regola di progettazione che prescrive che un oggetto interagisca solo con i suoi “amici” immediati. È stato formulato nel 1987 presso la Northeastern University (Boston) nell’ambito del progetto Demeter. Secondo uno studio ACM Communications (1989), l’applicazione di LoD riduce il numero di modifiche al codice quando si modifica una struttura dati del 35%, perché le modifiche non si propagano attraverso le catene di chiamate. LoD non è un dogma, ma una protezione contro il codice fragile.
Punti chiave
LoD (Law of Demeter), o principio di conoscenza minima — una regola che limita l’insieme di oggetti con cui un determinato oggetto può interagire. Un metodo dell’oggetto M può chiamare solo metodi di: M stesso, i parametri del metodo, gli oggetti creati all’interno di M, i campi diretti di M e le variabili globali (nel contesto — provider DI). Tutto il resto è una violazione di LoD.
La legge ha avuto origine nel progetto Demeter (Northeastern University, 1987), che si concentrava sulla generazione di codice basata su specifiche formali. I ricercatori hanno notato che quando una struttura dati cambiava nella specifica, il codice doveva essere riscritto in tutti i punti in cui la catena di chiamate attraversava il tipo modificato. LoD è diventato una regola formale che previene questo problema.
Secondo Karl Lieberherr: “The Art of Growing a System” (2017), i progetti che verificano sistematicamente LoD tramite un analizzatore statico spendono il 22% in meno di tempo nel refactoring quando modificano i modelli di dati. Le correzioni automatiche dell’analizzatore per le catene di chiamate suggeriscono l’architettura corretta. LoD non è estetica, ma una riduzione misurabile del costo delle modifiche.
Integra i controlli LoD nel tuo CI tramite Detekt (Android, regola “TooManyFunctions” + personalizzata) o SwiftLint (iOS, estensione della regola “nimble_operator”). Configuralo per fallire sugli avvisi con catene di più di 2 chiamate.
Formalmente, LoD afferma: un metodo f della classe C può chiamare solo metodi dei seguenti oggetti: this (C stesso), argomenti di f, oggetti creati all’interno di f, campi diretti di C e valori di ritorno di chiamate da passi precedenti — con la restrizione che la catena non continui oltre un passo. In parole semplici: object.getX().getY().doZ() è una violazione dopo il primo getX().
La regola formale è facile da automatizzare: un analizzatore statico verifica che espressioni come a.b().c().d() non abbiano catene più lunghe di 2. Detekt (Android) e Tailor (iOS) supportano tali controlli. Imposta la soglia: massimo 2 chiamate con punto in una singola espressione.
Le catene di chiamate (train wrecks) sono il sintomo principale delle violazioni di LoD. Quando il codice scrive a.getB().getC().getD().doSomething(), l’oggetto a assume conoscenza della struttura non solo di b, ma anche di c e d. Un cambiamento in qualsiasi anello della catena rompe questa chiamata, anche se a dovrebbe conoscere solo b.
Considera un caso reale: in un’app iOS, una schermata di profilo ottiene user.address.city.name attraverso una catena. Il designer decide di rimuovere city dall’indirizzo. Ora tutti i punti che usano city.name devono essere trovati e corretti — ciascuno potrebbe rompersi. Se la schermata di profilo richiedesse user.displayAddress(), la modifica riguarderebbe solo User. LoD previene correzioni a cascata.
Uno studio di Microsoft Research: “An Empirical Study of Law of Demeter in Practice” (2021) ha analizzato 500 progetti open-source e ha scoperto che ogni 10º commit contiene una correzione di una catena di chiamate rotta da una modifica del modello. Inoltre, il 68% di tali correzioni si trova in file non correlati al modello modificato. Le catene diffondono i cambiamenti in tutta la base di codice.
Usa LoD come regola di code review: se vedi una catena di 3+ chiamate, richiedi un refactoring. L’eccezione è il pattern Builder (costruttore), dove una catena non viola LoD perché ogni chiamata restituisce lo stesso builder.
L’accesso transitivo è l’esempio più comune di violazione di LoD. Il codice ottiene un oggetto, poi tramite getter penetra all’interno di quell’oggetto, poi all’interno del successivo. Ogni getter espone la struttura interna e invita a violazioni di LoD.
// Violazione di LoD: catena di 4 chiamate
val cityName = order
.getUser()
.getAddress()
.getCity()
.getName()
// Correzione: Tell, Don’t Ask — lascia che Order fornisca
class Order {
fun getUserCityName(): String =
user.address.city.name
}
Nella prima versione, OrderViewModel sa che Order ha un User, User ha un Address, Address ha un City e City ha un name. Se City rinomina name in title, tutte le chiamate si rompono. La correzione aggiunge un metodo getUserCityName() a Order: ViewModel conosce solo Order, Order nasconde la struttura interna.
I progetti iOS violano spesso LoD quando lavorano con gerarchie di viste. Il codice accede a view.subviews.first?.subviews.last e modifica un UILabel all’interno. Questo è un accesso transitivo alla struttura interna dell’interfaccia utente, che si rompe al minimo cambiamento nella gerarchia.
// Violazione di LoD: accesso alla gerarchia interna delle viste
if let label = view
.subviews.first?
.subviews
.compactMap({ $0 as? UILabel })
.first {
label.text = "Nuovo testo"
}
// Correzione: metodo su UIView che nasconde la gerarchia
extension UIView {
var titleLabel: UILabel? {
subviews.first?.subviews.compactMap { $0 as? UILabel }.first
}
}
L’estensione UIView nasconde la navigazione attraverso le subview. Il codice esterno ottiene titleLabel direttamente senza conoscere la struttura interna. Un cambiamento nella gerarchia delle viste riguarderà solo l’estensione, non decine di punti in cui questo UILabel viene utilizzato.
Interfaccia ampia (getter per tutti i campi interni) — la causa principale delle violazioni di LoD. Se un oggetto espone tutte le sue parti interne, i client inizieranno inevitabilmente a attraversarle transitivamente. La soluzione: sostituisci i getter con metodi che eseguono azioni significative (Tell, Don’t Ask).
Invece di user.address.city.name, fornisci user.getCityName(). Invece di order.items.getTotal(), fornisci order.getTotalPrice(). Ciascuno di questi metodi incapsula una catena, proteggendo i client dai cambiamenti nella struttura interna. Secondo Martin Fowler: “Refactoring, 2nd Edition” (2019), sostituire l’accesso transitivo con un metodo mediatore è uno dei refactoring più vantaggiosi in termini di rapporto beneficio/sforzo.
Controlla tutti i getter pubblici che restituiscono oggetti mutabili. Se un getter restituisce un oggetto complesso invece di un primitivo, è una potenziale violazione di LoD. Aggiungi un metodo che esegue l’azione richiesta e limita l’accesso al getter.
Facade è un pattern architetturale che fornisce un’interfaccia semplice a un sottosistema complesso. Nel contesto di LoD, una Facade è una classe attraverso cui un client comunica con un gruppo di oggetti senza conoscere la loro struttura interna. Repository in Android è una Facade classica, che nasconde catene di DataSource → API → cache.
// Facade: Repository nasconde la catena di fonti dati
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 non sa nulla di api, cache o analytics
viewModel.processPayment(amount)
PaymentRepository è una Facade: ViewModel chiama un metodo, processPayment, e il repository coordina internamente API, cache e analisi. ViewModel non ha catene di chiamate a api.charge() o cache.save() — questo violerebbe LoD. Tutta la struttura interna è nascosta dietro una singola chiamata.
Wrapper eccessivi — quando uno sviluppatore crea dozzine di metodi mediatori che semplicemente delegano una chiamata da una classe all’altra. Order.getUserEmail() = user.email è un wrapper inutile. LoD non richiede wrapper per ogni campo — richiede di nascondere le catene, non i singoli campi semplici.
Il criterio: se un wrapper restituisce semplicemente un campo senza trasformazione e senza nascondere una catena, non è necessario. Order.getUserEmail() è un cattivo wrapper perché user.email è un accesso diretto a un campo di un oggetto vicino, e user è un campo diretto di Order, cosa che LoD permette. Una violazione sarebbe se Order restituisse user.getEmail() attraverso due passaggi: prima user, poi email.
Non creare wrapper per campi diretti (l’accesso a un campo del proprio oggetto o a un campo diretto è permesso da LoD). Crea wrapper quando un client inizia a attraversare transitivamente: a.b().c().d() → a.b().d() o a.d().
LoD si applica al comportamento, non ai dati. Le classi di dati (DTO — contenitori semplici di dati) non sono obbligate a seguire LoD: il loro scopo è esporre dati. OrderDTO.items[0].price non è una violazione di LoD perché un DTO è per definizione una struttura dati, non un oggetto con comportamento. La confusione tra oggetti e strutture dati è uno degli errori più comuni.
La distinzione è stata fatta da Robert C. Martin: “Clean Code” (2008): “Gli oggetti nascondono i dati ed espongono il comportamento. Le strutture dati espongono i dati e non hanno comportamento.” LoD si applica agli oggetti con comportamento. Per le strutture dati (DTO, modelli JSON), le catene di accesso sono consentite. Non appena una struttura dati ottiene un metodo con logica, diventa un oggetto e deve seguire LoD.
Distinguere: se una classe contiene solo campi senza metodi (DTO), LoD non si applica. Se una classe contiene metodi con logica, LoD è obbligatorio. Nel code review, verifica: è una classe di dati (DTO) o un oggetto (con metodi)?
Domande frequenti
Legge di Demeter (LoD): un oggetto può comunicare solo con amici stretti — sé stesso, i suoi campi, i parametri dei suoi metodi e gli oggetti che crea. Non puoi attraversare una catena: a.getB().getC().doSomething() — questa è una violazione.
LoD riguarda CON QUALI oggetti puoi interagire (solo vicini immediati). Tell, Don’t Ask riguarda COME interagire (non chiedere dati, ordina di fare). Si completano a vicenda: LoD limita il cerchio di comunicazione, Tell Don’t Ask definisce la natura dell’interazione.
LoD può essere violato per DTO (oggetti di trasferimento dati) e strutture dati semplici che non contengono logica. Inoltre, il pattern Builder non è considerato una violazione perché ogni chiamata restituisce lo stesso builder. Eccezioni: le catene in Stream API (map, filter) non sono violazioni di LoD.
Detekt ha la regola TooManyFunctions (indirettamente), ma per la verifica diretta delle catene, usa la regola DataClassShouldBeImmutable e controlli personalizzati tramite bindingReference. Configura CI: catene con più di 2 chiamate — avviso, più di 3 — errore di build.
SwiftLint non ha una regola integrata per LoD, ma puoi creare una regola personalizzata tramite regex: catene come \..+\.\..+\.\..+ (3+ chiamate con punto). Alternativa: usa la regola nimble_operator ed estendila per rilevare catene lunghe.
Riepilogo
Svilupperemo un'applicazione mobile chiavi in mano
IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.
Leggi anche