Quando si parla di architettura del software si incontrano molte sigle e molti principi: SOLID, livelli, esagoni, microservizi. Dietro quasi tutti, però, ci sono due idee più antiche e più semplici, formulate già negli anni Settanta nell'ambito della progettazione strutturata: coesione e accoppiamento. Capirle bene permette di valutare qualunque scelta progettuale con una domanda pratica: quanto sarà facile cambiare questo codice domani?
Coesione: ogni modulo ha uno scopo
La coesione misura quanto gli elementi di un modulo (una funzione, una classe, un file, un pacchetto) appartengano davvero insieme. Un modulo ad alta coesione fa una cosa sola e tutte le sue parti contribuiscono a quello scopo.
Un esempio di bassa coesione è il classico file utils.py che contiene di tutto: una funzione per formattare le date, una per inviare email, una per calcolare l'IVA, una per ridimensionare immagini. Non c'è un motivo per cui queste funzioni stiano insieme, se non che nessuno sapeva dove metterle.
Un modulo coeso, invece, si descrive in una frase senza usare «e»: «questo modulo calcola i prezzi», «questa classe rappresenta un ordine e le sue regole».
Perché conta? Perché quando una regola cambia, la modifica resta circoscritta in un posto. Se le regole sui prezzi sono sparse in dieci file, ogni cambiamento richiede di trovarle tutte, e prima o poi qualcuna sfugge.
Accoppiamento: quanto i moduli dipendono l'uno dall'altro
L'accoppiamento misura quanto un modulo dipende dai dettagli interni di un altro. Un accoppiamento forte significa che modificare un modulo costringe a modificarne altri.
Alcuni segnali di accoppiamento eccessivo:
- un modulo accede direttamente ai dati interni di un altro invece di usare le sue funzioni pubbliche;
- una modifica allo schema del database richiede cambiamenti nell'interfaccia utente;
- per testare una funzione bisogna avviare mezzo sistema;
- catene di chiamate come
ordine.cliente.indirizzo.citta.provincia.sigla, che legano il codice alla struttura interna di molti oggetti.
L'obiettivo non è eliminare l'accoppiamento, cosa impossibile: i moduli devono collaborare. L'obiettivo è che dipendano da interfacce stabili e ridotte, non da dettagli che cambiano spesso.
L'equilibrio desiderato
| Bassa coesione | Alta coesione | |
|---|---|---|
| Accoppiamento forte | Il caso peggiore: tutto dipende da tutto | Moduli ordinati ma intrecciati |
| Accoppiamento debole | Moduli indipendenti ma confusi | L'obiettivo: moduli chiari e autonomi |
Le due proprietà sono collegate: quando ogni modulo raccoglie tutto ciò che riguarda il suo scopo, ha meno bisogno di chiedere dettagli agli altri.
Un esempio concreto
Ecco un servizio che invia la conferma di un ordine:
class ServizioOrdini:
def conferma(self, ordine):
ordine.stato = "confermato"
server = smtplib.SMTP("mail.esempio.it", 587)
server.login("utente", "password")
server.sendmail("negozio@esempio.it", ordine.email, "Ordine confermato")
La logica degli ordini è legata a un modo specifico di inviare email, a un server preciso e a credenziali scritte nel codice. Per testare la conferma serve un server di posta; per passare a un servizio di invio diverso bisogna modificare la classe degli ordini.
Una versione con accoppiamento più debole:
class ServizioOrdini:
def __init__(self, notificatore):
self.notificatore = notificatore
def conferma(self, ordine):
ordine.stato = "confermato"
self.notificatore.invia(ordine.email, "Ordine confermato")
Ora ServizioOrdini dipende solo da un oggetto con un metodo invia. Nei test si passa un notificatore finto che registra i messaggi; in produzione uno che usa la posta elettronica; domani, magari, uno che invia SMS. Questa tecnica, passare le dipendenze dall'esterno invece di crearle all'interno, si chiama iniezione delle dipendenze.
Come applicare questi principi
Alcune abitudini pratiche:
- raggruppa per ciò che cambia insieme: se due elementi vengono sempre modificati nella stessa occasione, probabilmente appartengono allo stesso modulo;
- esponi poco: rendi pubblico solo ciò che serve agli altri moduli; in Python, il prefisso
_segnala per convenzione ciò che è interno; - isola i confini: database, servizi esterni, file system e interfaccia utente sono i punti in cui conviene introdurre un'interfaccia, perché sono i più soggetti a cambiare;
- diffida dei moduli "comuni": pacchetti come
commonohelperstendono a diventare depositi a bassa coesione da cui dipende tutto; - ascolta i test: se una classe è difficile da testare, spesso è perché è troppo accoppiata.
Non esagerare
Il rischio opposto esiste: interfacce per ogni cosa, livelli di astrazione che non nascondono nulla, dieci file per un'operazione semplice. Anche questo rende il codice difficile da capire. La regola pratica è introdurre un'astrazione quando c'è una ragione concreta, come un test da scrivere o una seconda implementazione reale, non per una variazione ipotetica che potrebbe non arrivare mai.
In sintesi
- La coesione misura quanto le parti di un modulo appartengono a un unico scopo.
- L'accoppiamento misura quanto un modulo dipende dai dettagli interni degli altri.
- L'obiettivo è alta coesione e accoppiamento debole: moduli chiari che collaborano tramite interfacce stabili.
- L'iniezione delle dipendenze riduce l'accoppiamento e rende il codice più facile da testare.
- Introduci astrazioni per ragioni concrete, non per ipotesi future.