Modularità
In generale, per modularità si intende la misura in cui un sistema può essere decomposto e ricombinato. La modularizzazione è applicata al fine di ridurre la complessità di un sistema decomponendolo in varie parti che presentano relazioni di indipendenza e di interdipendenza.
In questa sede parleremo di modularità riferendoci alla decomposizione in classi e non a "moduli" intesi come insiemi di classi.
La dipendenza tra classi diverse è identificata dall'accoppiamento tra di esse, mentre la qualità della decomposizione ha come buon indicatore la coesione della classe stessa.
Accoppiamento
L'accoppiamento misura la dipendenza tra due moduli, e idealmente deve essere minore possibile. Distinguiamo vari livelli di accoppiamento, che elenco dal migliore al peggiore:
- nessun accoppiamento
- per dati: lista di parametri passati costituiti da dati semplici, oppure un modulo usa un dato, semplice, prodotto da un altro modulo
- per struttura: nell'interfaccia di un metodo è presente una struttura dati, o un modulo usa una struttura prodotta da un altro modulo
- per controllo: un modulo passa elementi di controllo (flag, ..) ad un altro modulo per condizionarne l'esecuzione
- esterno: due moduli comunicano in modo strutturato attraverso dati comuni
- comune: due moduli accedono a dati globali senza seguire un particolare protocollo
- per contenuto: un modulo usa e modifica dati "privati" di un altro modulo
Coesione
Esprime il grado di correlazione tra gli elementi dello stesso modulo. Ogni modulo dovrebbe avere una altra coesione, e questo è raggiungibile se il modulo ha un unico compito, e lo esegue bene.
Distinguiamo vari livelli di coesione, che elenco dalla peggiore alla migliore:
- casuale: azioni o istruzioni completamente scorrelate tra loro
- logica: azioni correlate, ossia che trattano lo stesso argomento (I/O, trattamento errori, check..)
- temporale: azioni che devono essere eseguite insieme (inizializzazioni, terminazioni..)
- procedurale: azioni che devono essere eseguiti in un determinato ordine
- comunicazionale: azioni che agiscono sugli stessi dati
- sequenziale: azioni ognuna della quale dipende dal risultato della precedente
- funzionale: modulo che svolge una sola attività e tutti gli elementi del modulo contribuiscono a realizzarla
Avendo bene in mente questi obiettivi, ogni classe dovrebbe contenere un set di dati e le funzioni atte ad agire su quei dati, le classi dovrebbero essere di dimensioni non troppo grandi (sarebbe un segnale che stanno facendo troppe cose), non dovrebbero esserci dipendenze cicliche tra le classi e ogni classe non dovrebbe avere troppe dipendenze da altre classi. Denominati il fan-in ed il fan-out come le dipendenze in ingresso ed in uscita da una classe, questi dovrebbero in generale essere compresi in 5-9.
Il fan-out dovrebbe essere maggiore per i livelli logici più elevati, perché utilizzano servizi, mentre il fan in dovrebbe essere maggiore per quelli più bassi poiché sono quelli che forniscono servizi.
Vediamo i principali problemi che possono presentarsi con la modularizzazione.
Errata decomposizione
Questo accade quando funzioni che logicamente avrebbero dovuto essere nella stessa classe, si trovano sparse in diversi punti, oppure ci sono classi con pochi metodi (perché gestiti da funzioni esterne alla classe), oppure ci sono metodi che agiscono su dati di classi esterne (altra faccia della stessa medaglia).
In questo caso abbiamo un cattivo accoppiamento ed una bassa coesione.
Soluzioni
- se ci sono metodi che agiscono prevalentemente su dati di altre classi, valutare se spostare tali metodi (o parte di essi) nelle classi opportune in modo da ripristinare una buona coesione e correggere l'accoppiamento
- Se ci sono classi "data" senza metodi o con pochi metodi, creare in esse la giusta interfaccia affinché le altre classi "client" possano agire sui dati di queste classi "data" senza accedere direttamente alle strutture
- se un metodo di una classe B che non accede ai dati della classe B è più usato in una classe A che nella classe B stessa, valutare se spostarlo nella classe A, sempre se l'operazione ha senso e migliora la coesione e l'accoppiamento.
- se un campo di B è più usato da una classe A che non nella classe B stessa (situazione direi abbastanza tragica), valutare se spostare tale campo nella classe A, , sempre se l'operazione ha senso e migliora la coesione e l'accoppiamento.
- rendere privati i dati su cui è possibile operare tramite metodi
- spostare campi non strettamente collegati alla funzione (astrazione) implementata dalla classe andrebbero spostati in altre classi
Insufficiente modularità
Questo accade quando una funzione non è stata completamente decomposta, e ci sarebbe bisogno di un'ulteriore decomposizione ai fini di ridurne la complessità e/o la dimensione.
Sintomi possono essere:
- un numero elevato di membri e metodi nell'interfaccia, e questo può essere il sintomo che questa classe "fa troppe cose". Questo caso può nascere anche se più metodi sono stati raggruppati perché agivano sugli stessi dati, ma non sempre è una buona idea, infatti si migliora l'accoppiamento ma può peggiorare la coesione, senza tenere conto del fatto che una classe dovrebbe seguire il principio di "single responsibility"
- un numero molto elevato di metodi nell'implementazione, e questo può essere il sintomo che a sua volta poteva essere decomposta in classi più semplici.
Soluzioni
Se una classe ha troppi metodi o metodi molto complessi, una soluzione è decomporla ulteriormente incapsulando in un'altra classe parte delle sue funzionalità, sempre con in mente l'obiettivo di non comprometterne la coesione.
Se ci sono "isole" di metodi che si riferiscono a insiemi di dati diversi, separare tali isole in classi diverse.
Se ci sono gruppi di (molti) metodi che hanno scopi diversi, valutare se suddividere tali metodi in classi diverse con uno scopo più specifico, eventualmente esponenti un'interfaccia comune.
Se ci sono metodi molto complessi, valutare anche la creazione di classi private "helper" per semplificare la classe principale.
Dipendenze cicliche
Succede quando tra due o più moduli si instaura una dipendenza circolare. Questo è un errore perché rende difficile comprendere la struttura complessiva, e anche il testing. Inoltre da un punto di vista logico risulta difficile capire chi dipende da chi.
Può accadere per vari motivi:
- perché ci sono metodi collocati in classi sbagliate
- ci sono metodi in cui viene passato "this" come parametro e il metodo chiamato agisce sui metodi del chiamante
- ci sono funzioni di callback non necessarie
Soluzioni
In questi casi è possibile, a seconda dei casi:
- introdurre nuove classi o interfacce
- spostare il metodo che crea la dipendenza circolare nella classe che esso richiama
- unire due classi se in effetti implementano la stessa funzione
Moduli "hub"
Succede quando una classe ha un fan-in ed un fan-out elevato.
Abbiamo già visto che per i moduli di livello elevato ci aspettiamo un fan out maggiore mentre per quelli di livello più basso un maggiore fan in.
Un simultaneo fan-in e fan-out elevato è senz'altro un sintomo che la classe sta facendo troppe cose, e, a meno che non si tratti di una classe facente parte di un framework (come un mediator) questo non va bene, poiché diventa impossibile da cambiare senza che cambino i suoi utilizzatori, e viceversa diventa molto passibile di cambiamenti quando cambiano le classi da cui dipende.
Soluzioni
Certamente in questi casi si può cercare di suddividere la classe in classi diverse ed il più possibile indipendenti tra loro.
Se le dipendenze sono dovute a funzioni mal collocate, si può agire spostando i metodi dove sono chiamati, analogamente per l'accesso ai membri di altre classi (orrore)
Se il fan in è elevato, a volte si può ovviare utilizzando il pattern chain of responsibility in modo che ogni funzione deleghi ad una (sola) altra l'elaborazione qualora non la svolga lei stessa.
bibliografia
Refactoring for Software design smells Girish Suryanarayana, Ganesh Samarthyam, Tushar Sharma
Nessun commento:
Posta un commento