La coesione è una metrica che mostra quanto strettamente correlati sono gli elementi all'interno di un modulo o una classe. Secondo Wikipedia, un'alta coesione è una caratteristica di un modulo ben progettato, dove tutti i metodi e i campi lavorano su un unico compito. La coesione influisce direttamente sulla manutenibilità del codice e si contrappone all'accoppiamento — l'interconnessione tra moduli.
Punti chiave
La coesione è una metrica che valuta quanto logicamente connessi sono i metodi, i campi e le proprietà all'interno di una classe o modulo. Un modulo altamente coeso esegue un compito e contiene solo gli elementi necessari per svolgerlo. Un modulo a bassa coesione cerca di fare più cose contemporaneamente — i suoi metodi sono debolmente correlati nel significato.
Nel contesto della programmazione orientata agli oggetti, la coesione è strettamente legata al Principio di Responsabilità Unica (S). Se una classe ha una responsabilità chiara, la sua coesione è generalmente alta. Se una classe gestisce UI, logica di business e rete contemporaneamente — la coesione è bassa, e tale classe dovrebbe essere suddivisa in più classi separate con responsabilità più ristrette.
Comprendere la coesione aiuta gli sviluppatori a prendere decisioni di refactoring. Quando vedi un metodo in una classe che non utilizza alcun campo della classe, è un segno di bassa coesione. Tale metodo è fuori posto nella classe, oppure la classe è mal progettata. Cercare un'alta coesione è uno sforzo continuo per migliorare l'architettura a ogni livello del codice.
Nell'ingegneria del software, si distinguono sette livelli di coesione, ordinati dal peggiore al migliore. Comprendere questa scala consente di valutare oggettivamente la qualità di un modulo e determinare la direzione per il refactoring. Più alto è il livello, più manutenibile e comprensibile sarà il codice.
Coincidenziale — il peggior livello, dove gli elementi di un modulo sono raggruppati casualmente senza alcuna connessione logica. Esempio: una classe Utilities con metodi per formattare date, inviare email e calcolare sconti. Tale classe non può essere compresa senza leggere tutti i suoi metodi, e modificarne uno potrebbe romperne altri semplicemente perché sono insieme.
Logica — gli elementi eseguono compiti logicamente correlati ma fondamentalmente diversi. Una classe con metodi parseJSON, parseXML e parseCSV è logicamente connessa dal tema dell'“analisi,” ma ogni metodo fa un lavoro radicalmente diverso. Il problema: quando si aggiunge un nuovo formato (YAML), la classe cresce e la sua interfaccia diventa gonfiata.
Temporale — gli elementi sono raggruppati per tempo di esecuzione. Una classe AppInitializer che configura il database, carica la configurazione e inizializza le analisi — tutto ciò accade all'avvio dell'app, ma i compiti stessi non sono correlati. È meglio dividerli in Initializer separati per ogni area di responsabilità.
Procedurale — si verifica quando gli elementi sono uniti da una sequenza di esecuzione. Un modulo “Elaborazione ordini” contiene metodi validateCart, processPayment e sendConfirmation — ogni metodo viene chiamato rigorosamente dopo il precedente. Questo è meglio della coesione coincidenziale o logica, ma non è ancora ideale: ogni passaggio potrebbe essere estratto in un modulo separato.
Comunicazionale — gli elementi lavorano con gli stessi dati. Una classe UserService con metodi getUser, updateUser e deleteUser è unita dall'entità comune User. Questo è significativamente meglio della coesione procedurale: la classe ha un dominio chiaro. La maggior parte delle classi Repository nei progetti mobili hanno coesione comunicazionale.
Funzionale — il livello più alto, dove ogni elemento del modulo partecipa all'esecuzione di un unico compito. Una classe PasswordValidator con un unico metodo validate che verifica lunghezza, presenza di caratteri e complessità della password è un esempio di coesione funzionale. Se tale classe cambia, è solo perché le regole di validazione della password sono cambiate.
Raggiungere la coesione funzionale è l'obiettivo principale del refactoring architetturale. Ogni classe dovrebbe avere esattamente un motivo per cambiare. Nello sviluppo mobile, la coesione funzionale si ottiene estraendo Use Cases separati, View personalizzate, formattatori e validatori. Ciascuna di queste classi è un blocco di costruzione completo con un'area di responsabilità chiara.
Coesione e accoppiamento sono due facce della stessa qualità. Più alta è la coesione all'interno di un modulo, minore tende ad essere l'accoppiamento tra moduli. Un sistema ben progettato cerca simultaneamente un'alta coesione interna e un basso accoppiamento esterno. Questo principio è riconosciuto come fondamentale nell'ingegneria del software dagli anni '70.
La relazione coesione-accoppiamento può essere pensata come un equilibrio. Se uno sviluppatore sacrifica la coesione combinando più compiti in una classe, i moduli vicini ottengono più dipendenze — devono accedere a questa classe sovraccarica per scopi diversi, aumentando l'accoppiamento. Al contrario, suddividere in classi piccole e altamente coese riduce i punti di interazione tra moduli.
In pratica, ciò significa: quando estrai una nuova classe con coesione funzionale, contemporaneamente liberi altri moduli dalla necessità di conoscere i dettagli della sua implementazione. Ad esempio, estrarre EncryptionManager in una classe separata con coesione funzionale fornisce ad altri moduli una semplice interfaccia encrypt/decrypt senza bisogno di comprendere i dettagli dell'algoritmo di crittografia.
// Bassa coesione — la classe fa tutto in una volta
class UserManager {
fun fetchAndSaveUser(id: String) { }
fun parseUserJson(json: String): User { }
fun displayUserName(user: User): String { }
fun validateEmail(email: String): Boolean { }
}
// Alta coesione — ogni classe risolve un compito
class UserRepository {
fun fetchUser(id: String): User { }
}
class UserJsonParser {
fun parse(json: String): User { }
}
class UserNameFormatter {
fun format(user: User): String { }
}
class EmailValidator {
fun isValid(email: String): Boolean { }
}
L'esempio mostra la differenza: UserManager ha coesione logica — tutti i metodi riguardano gli utenti, ma ciascuno fa un lavoro fondamentalmente diverso. Dopo il refactoring, ogni classe ha coesione funzionale e l'accoppiamento si riduce perché altri moduli dipendono solo dalla classe di cui hanno bisogno, non dall'intero UserManager.
LCOM (Mancanza di Coesione dei Metodi) è la metrica più nota per misurare la coesione di una classe. LCOM conta quante coppie di metodi non condividono campi comuni. Un valore di 0 significa coesione ideale (tutti i metodi lavorano con gli stessi campi), mentre un valore alto indica bassa coesione. LCOM4 (una versione migliorata) considera le connessioni transitive attraverso altri metodi.
Nello sviluppo Android, le metriche di coesione possono essere ottenute tramite Detekt con la regola TooManyFunctions. Le classi con decine di metodi che utilizzano diversi gruppi di campi hanno probabilmente bassa coesione. In iOS, SwiftLint ha le regole file_length e function_body_length — indicatori indiretti: file e metodi lunghi spesso segnalano bassa coesione.
Un metodo di valutazione manuale: poni la domanda “Questa classe cambierà per una ragione o per ragioni multiple?” Se puoi nominare più di una ragione indipendente — la classe ha bassa coesione. Un secondo test: “Questa classe può essere divisa in due classi indipendenti?” Se sì — fallo. Controllare regolarmente la coesione durante le revisioni del codice previene le classi God e riduce il debito tecnico.
Il primo passo — applicare il Principio di Responsabilità Unica. Ogni classe dovrebbe avere una responsabilità chiara. Se una classe ha un metodo che non riguarda il suo compito principale, estrailo in una classe separata. La tecnica Extract Class o Extract Delegate negli IDE automatizza questo processo. Dopo l'estrazione, verifica se la classe originale è diventata più focalizzata.
Il secondo passo — utilizzare il pattern Facade per semplificare l'interfaccia. Se una classe fornisce 20 metodi ma i clienti ne usano solo 3–4, la classe potrebbe avere bassa coesione — offre troppe funzionalità diverse. Raggruppa i metodi per argomento, estrai classi separate per ogni gruppo e trasforma la classe originale in una facciata o rimuovila.
Il terzo passo — prestare attenzione ai gruppi di campi. Se una classe ha campi che sono usati solo da un sottoinsieme di metodi — è un indicatore di bassa coesione. Dividi la classe per gruppi di campi. Ad esempio, se una classe contiene i campi userRepository, networkClient e analyticsTracker, ma il primo gruppo di metodi usa solo userRepository mentre il secondo usa networkClient — sono due classi diverse.
Il quarto passo — evitare di creare classi “utility” con metodi static arbitrari. Ogni metodo static in una classe Utils o Helpers è candidato all'estrazione in una classe specializzata. FormatUtils.dateToString è meglio spostarlo in DateFormatter, e ValidationUtils.isValidEmail in EmailValidator. Questo aumenta la coesione di ogni classe e rende il codice auto-documentante.
Domande frequenti
Quasi sempre. La coesione funzionale rende il codice chiaro e prevedibile. Tuttavia, portarla all'estremo può causare una frammentazione eccessiva: creare una classe separata per ogni operazione, rendendo l'architettura eccessivamente complessa. L'equilibrio è poche classi per funzionalità, ciascuna con coesione funzionale.
La coesione è una metrica di consistenza interna all'interno di un singolo modulo o classe. La modularità è un principio architetturale in cui un'applicazione è divisa in moduli fisici. L'alta coesione è un obiettivo sia nella progettazione di singole classi che di interi moduli.
Detekt per Android e Xcode Analyzer per iOS evidenziano classi con un numero sospettosamente elevato di metodi o campi. IntelliJ IDEA e AppCode hanno la visualizzazione delle dipendenze — puoi vedere il grafo delle connessioni e individuare classi con bassa coesione. SonarQube calcola le metriche LCOM automaticamente.
Sì. Un'interfaccia con metodi connect, disconnect e isConnected ha alta coesione — tutti i metodi riguardano la gestione della connessione. Un'interfaccia con connect, parseData e renderUI ha bassa coesione. Il Principio di Segregazione delle Interfacce (SOLID) richiede la creazione di interfacce strettamente focalizzate con alta coesione.
Poniti tre domande: Lo scopo della classe può essere descritto in una frase? Tutti i metodi supportano questo scopo? Ci sono campi nella classe che non vengono usati da alcuni metodi? Se la risposta a qualsiasi domanda è no — la coesione è bassa e la classe dovrebbe essere divisa.
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