Ti ricordi cosa dicevano i tuoi nonni quand’eri bambino? “Peppe, non devi assolutamente parlare con gli sconosciuti!”.

Se da ragazzo non capivo quell’affermazione adesso, che ho un po’ di barba, riesco a concepirne il motivo. Il dialogare con qualche sconosciuto m’avrebbe potuto portare a commettere qualche errore.

Può sembrare una banalità, ma tale affermazione è stata ripresa per filo e per segno dalla maggior parte dei linguaggi di programmazione orientati agli oggetti.

La legge di Demetra o Principio della Conoscenza Minima stabilisce che un oggetto dovrebbe conoscere solamente oggetti strettamente correlati e che dovrebbe interagire solo con quelli che conosce direttamente.

Cosa significa?

Ipotizziamo d’avere 3 oggetti: A, B e C.

Secondo la Law of Demeter, un oggetto A può richiedere un servizio dell’oggetto B (cioè può richiamare un suo metodo) ma, l’oggetto A, non può utilizzare B per raggiungere un servizio dell’oggetto C.

Il perché è abbastanza semplice ed è riconducibile a quello che accade quando si gioca al “telefono senza fili“. Il passaggio da A a C, attraverso un oggetto B, implica la piena conoscenza della struttura di C da parte dell’oggetto A.

Spiegato in maniera più semplice, nel caso C dovesse cambiare la sua struttura interna (nuovi metodi e proprietà), sia A che B dovrebbero modificare l’interazione con tale oggetto.

Come si risolve?

Applicando la legge di Demetra, se A volesse accedere ad un servizio di C attraverso B, l’oggetto B dovrà fornire un servizio (cioè un metodo) che richiami il metodo di C. In questo modo A accederà indirettamente a C attraverso la sola conoscenza di B.

class A {
    var b: B
    func doSomethingWithB() {
        b.doSomething()
    }

    func doSomethingWithC() {
        b.doSomethingWithC()
    }
}

class B {
    var c: C
    func doSomething() {}
    
    func doSomethingWithC() {
        c.doSomething()
    }
}

class C {
    func doSomething() {}
}


var a = A()
a.doSomethingWithB() // CORRETTA APPLICAZIONE DELLA LEGGE DI DEMETRA
a.b.doSomething() // VIOLAZIONE DELLA LEGGE

Proviamo a formalizzare un po’ meglio il tutto e vediamo insieme come applicare la legge di Demetra al linguaggio Swift!

Sei pronto?
Allora cominciamo!

Le regole

La legge di Demetra afferma che ogni metodo M, di un oggetto O, possa invocare solo i metodi dei seguenti tipi di oggetti:

  1. I propri (cioè di self)
  2. Dei suoi parametri
  3. Di ogni oggetto che crea
  4. Dei suoi componenti diretti (le sue proprietà)

Viceversa, un oggetto dovrebbe evitare di invocare metodi di un oggetto ritornato da un altro metodo.

Dato che la maggior parte dei linguaggi di programmazione, tra cui anche il linguaggio Swift, utilizzano la dot notation per accedere ai metodi di un oggetto, queste 5 regole, a loro volta, potrebbero essere ricondotte alla semplice affermazione: “usa un solo punto“.

Esempio

Un’applicazione pratica della legge di Demetra potrebbe essere quella dell’impostazione del DataSource e Delegate della UITableView.

Ipotizziamo d’avere una lista di elementi da visualizzare. Generalmente, quello che si fa, è creare la struttura logica che rappresenti quei dati (quindi una classe o struct):

struct Item {
    var property: String
}

class ItemsContainer {
    var array: [Item] = []
}

Nel nostro caso abbiamo due tipi di modelli. Il primo, Item, che ci rappresenta il singolo dato ed il secondo, ItemsContainer, che gestirà i vari Item e le operazioni su di essi.

Una volta fatto questo, il secondo passaggio è quello della definizione del DataSource e Delegate della tabella.

Partiamo da un errore comune, ammetto che per scopi didattici lo faccio anche io, che è quello di definire il ViewController come sottoscrittore dei protocolli UITableViewDataSource e UITableViewDelegate. Questo genera già un primo problema in quanto, per un altro principio che si chiama di Singola Responsabilità (ne parlerò più avanti), appesantisce il ViewController con codice che non dovrebbe gestire per sua natura.

In generale, dovresti creare un oggetto TableViewController che implementi questi protocolli:

class TableViewController: NSObject, UITableViewDataSource {
    var container: ItemsContainer
    
    init(container: ItemsContainer) {
        self.container = container
    }
    
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return self.container.array.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        
        let dataToDisplay = self.container.array[indexPath.row].property
        cell.textLabel?.text = dataToDisplay
        return cell
    }
}

Nel codice ci sono delle chiarissime violazioni della legge di Demetra.

Le hai notate?

Sia nel metodo numberOfRowsInSection che nel metodo cellForRowAt stai accedendo all’oggetto array che è un estraneo per l’oggetto UITableViewController. Quindi stai dando troppa conoscenza al TableViewController, il quale, come unico vicino ed amico ha l’oggetto container.

Per la legge di Demetra, nel caso volessi accedere ai dati dell’array, cioè dell’oggetto C partendo dall’oggetto A, dovrebbe essere B a fornirti queste informazioni. Ovvero ItemsContainer dovrebbe fornire sia il numero di elementi presenti che l’Item dato un indice:

struct Item {
    var property: String
}

class ItemsContainer {
    var array: [Item] = []
    
    var numberOfItems: Int {
        return array.count
    }
    
    func item(at index: Int) -> Item {
        return self.array[index]
    }
}


class TableViewController: NSObject, UITableViewDataSource {
    var container: ItemsContainer
    
    init(container: ItemsContainer) {
        self.container = container
    }
    
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return self.container.numberOfItems
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        
        let dataToDisplay = self.container.item(at: indexPath.row)
        cell.textLabel?.text = dataToDisplay.property

        return cell
    }
}

La Law of Demeter indubbiamente permette di separare meglio le logiche di un’applicazione e di avere, come conseguenza, un codice notevolmente più flessibile.

Per esempio, senza la legge e se avessi voluto cambiare il modello dei dati dell’ItemsContainer, da un semplice Array a Dictionary, avrei dovuto modificare anche la classe TableViewController. Con la legge di Demetra, invece, dovrò solamente riadattare la proprietà numberOfItems ed il metodo item(at index).

Perché?

La legge di Demetra ha una storia molto antica. É stata formulata 30 anni fa ed ovviamente, nel corso degli anni, è stata riadattata e riconsiderata secondo le esigenze moderne (le regole sono sempre le stesse ma le applicazioni cambiano).

A sostegno della LoD sono stati compiuti diversi studi. Per esempio, uno studio condotto ad Ottobre del 1996, intitolato “A Validation of Object-Oriented Design Metrics as Quality Indicators“, evinse due caratteristiche:

  1. Con il probabile aumento delle invocazioni di metodi, da parte del metodo chiamato dalla classe di partenza, si riduce la presenza di bug nel codice. Ovvero la suddivisione crea un codice più snello e di facile comprensione.
  2. Dall’altro lato, con l’aumento della presenza di metodi all’interno di una classe, c’è il rischio che aumenti la presenza di bug. Ovvero aumentano le dimensioni delle classi per via dei metodi wrapper e aumenta la complessità di lettura e analisi (vedi le God Class).

Ad ogni modo, oggi la legge di Demetra è una condizione necessaria per poter sviluppare applicazioni complesse. La suddivisione dei compiti è una prerogativa in tutte quelle applicazioni che fanno uso dell’architettura multilayered dove si da per scontato che un livello comunichi solo con i livelli vicini.

Le leggi si rispettano

Ma fai attenzione a non confonderti perché non è un Pattern, ma una legge.

Un pattern è una soluzione standardizzata per la risoluzione di un determinato set di problemi, la regola è qualcosa che lo prescinde e che sta al di sopra.

Ma vuoi dirmi che dovrei utilizzarla soltanto per questo?

Non considerare queste nozioni come cose astratte o accademiche.

Non pensare che il codice che stai scrivendo, perché riguarda una piccola app o un progetto personale, non debba seguire delle regole, dei pattern o delle metodologie di programmazione definite.

É vero che hai il potere di scrivere il codice come vuoi e comunque vederlo funzionare ma, come dice un detto: non perché puoi fare una cosa vuol dire che devi farla.

legge di Demetra

Devi cominciare ad approcciarti ad ogni applicazione o problema come se tu fossi un medico.

Il medico prima di entrare in sala operatoria, prima di togliere un semplice punto o prima di toccarti segue delle procedure standard. Ovvero si lava e disinfetta le mani ecc. Non può sottrarsi a questo compito perché sa che la dimenticanza o il non seguire una pattern o una legge potrebbe significare la morte del paziente o una complicanza in corso d’opera.

Tu devi cominciare a pensare da professionista.

Quindi, scegli, vuoi essere un medico o un semplice macellaio? (inteso come dispregiato, non come categoria)

Alla prossima e buona programmazione!

Considerazioni

Se vuoi approfondire:

  1. Qui trovi lo studio condotto dalla Northeastern University che ha introdotto la Legge di Demetra.
  2. Qui, Yegor Bugayenko, descrive meglio il concetto dell”usa un solo punto” in ottica moderna. Ti consiglio questa lettura solo se già hai avuto modo di sperimentare la LoD altrimenti potresti essere indotto a pensare che quello che ho scritto non serva a niente.
  3. Qui invece trovi il tutorial da cui ho preso l’esempio del codice.

Changelog

  1. 8/04/2017 – Prima versione del tutorial.