Modularizzazione di un sistema: la storia è maestra di vita
Recentemente Marco Testa si è imbattuto in un articolo di David L Parnas, “On the Criteria To Be Used In Decomposing Systems into Modules” (trovate il link nel paragrafo Riferimenti). L’articolo tratta della modularizzazione come meccanismo per migliorare la flessibilità e la comprensibilità di un sistema riducendo nel contempo i tempi di sviluppo.
Ecco dunque che cosa ha imparato Marco leggendo un articolo pubblicato quasi cinquant’anni fa.
Dagli anni ’70 ad oggi, ciò che è stato è ancora attuale.
Sull’utilità di suddividere un sistema in parti più piccole, il classico divide et impera, si sono spesi fiumi di inchiostro. Su quali criteri effettivamente utilizzare per rendere davvero efficace una suddivisione, si trova ben poco materiale.
Mi sono quindi stupito di trovare espressa in forma semplice, chiara e ben argomentata quali criteri usare per una modularizzazione efficace e soprattutto quali criteri invece non usare, perché poco efficaci.
Ciò che più mi ha stupito però è la data nella quale questo articolo è stato scritto: il 1971.
Giusto per contestualizzare, nel 1971 non era stato ancora inventato il linguaggio C, che sarà sviluppato da Dennis Ritchie l’anno seguente. E nel 1972 nascerà anche la prima versione ufficiale di Smalltalk. Bjarne Stroustrup inizierà a lavorare al C++ nel 1979 e James Gosling inizierà a lavorare a Java nel 1991, quasi venti anni dopo la stesura di questo articolo. Il termine microservice verrà coniato nel 2011, ben quaranta anni dopo ed oggi, Aprile 2020, sono quasi passati cinquant’anni da allora.
Ma cosa ci potrà essere di interessante in un polveroso articolo di quasi mezzo secolo fa in un ambito quale quello informatico dove le innovazioni si sviluppano nell’arco temporale di mesi?
Beh, basta vedere ad esempio il dibattito attuale sui criteri per spezzare un monolite in microservizi e sugli errori che si possono commettere. I criteri per decomporre un sistema in moduli sono un tema di discussione quanto mai attuale.
Ma veniamo all’articolo di Parnas.
I vantaggi della modularizzazione
Mi ha stupito fin dall’introduzione la lucida esposizione dei vantaggi della modularizzazione:
- a livello manageriale, potendo sviluppare in parallelo i diversi moduli;
- flessibilità, potendo sostituire un modulo senza modificare gli altri;
- comprensibilità, potendo studiare il sistema un singolo modulo alla volta.
Ho sempre pensato che la comprensibilità e per contro la fatica cognitiva necessaria per affrontare un codice incomprensibile sono le caratteristiche principali che dovrebbero guidare le scelte di design. Devo ammettere che vedere così chiaramente espressa la comprensibilità come uno dei vantaggi della modularizzazione in un articolo così datato mi ha molto colpito.
Sempre nell’introduzione si definisce un modulo come un’entità con un’assegnata responsabilità, anziché come un sottoprogramma. Anche qui stupisce la lucidità con cui si indica la responsabilità come la caratteristica principale che identifica un modulo, precorrendo il responsibility-driven design della programmazione ad oggetti.
Ma veniamo ai criteri da usare per dividere un sistema in moduli.
Un caso d’uso e criteri per la modularizzazione di un sistema
Per mostrare i punti di forza e di debolezza di diversi criteri di modularizzazione Parnas usa come esempio l’implementazione di un Indice KWIC (KeyWord In Context).
Non è molto importante il singolo esempio e a mio parere non è nemmeno l’esempio migliore. Evitando di entrare in dettagli, si tratta di una indicizzazione nella quale per ciascuna riga di un testo si trovano le permutazioni circolari delle parole. Una permutazione circolare si ottiene prendendo la prima parola della riga e spostandola in fondo, ripetendo il procedimento fino a tornare alla disposizione iniziale.
Vengono quindi mostrate due diverse modularizzazioni.
Primo criterio di modularizzazione
Nel primo criterio si individuano i seguenti moduli:
- Input module: legge le righe di testo e le tiene in memoria.
- Circular Shift module: prepara un indice di permutazioni circolari.
- Alphabetizing module: prende il risultato precedente e lo ordina alfabeticamente.
- Output module: prende l’indice ordinato alfabeticamente e lo stampa.
- Master Control module: chiama i diversi moduli nel giusto ordine.
Questi moduli sono piccoli ed hanno un’interfaccia ben delineata. Questa suddivisione è probabilmente quella più immediata e che molti programmatori userebbero.
Secondo criterio di modularizzazione
Nel secondo criterio vengono invece trovati i seguenti moduli:
- Line Storage module: fornisce una serie di funzioni per gestire le stringhe.
- Input module: legge le righe di testo e le aggiunge a Line Storage.
- Circular Shifter module: ha una funzione che costruisce l’indice delle permutazioni circolari restituito come un Linee Storage.
- Alphabetizer module: una funzione che ordina alfabeticamente.
- Output module: stampa.
- Master Control module: chiama i diversi moduli nel giusto ordine.
Le due modularizzazioni sono molto simili: entrambe hanno dei moduli relativamente piccoli, con un’interfaccia definita.
Vediamo però come si comportano rispetto ad un possibile cambiamento.
Introduciamo una modifica: come si comportano i due sistemi modularizzati?
Supponiamo di voler cambiare il modo con cui le righe di testo vengono mantenute in memoria (‘core’ nel testo). Nell’articolo non viene esplicitato come queste vengano salvate, ma possiamo pensare ad un array o ad una lista di stringhe contenenti le righe e potremmo volerlo cambiare in una lista di liste di stringhe di parole.
Nel primo caso dovremo cambiare ogni modulo perché l’insieme di righe viene passato di modulo in modulo . La suddivisione è stata fatta sulla base delle diverse fasi di elaborazione delle linee di testo, in altre parole il criterio di suddivisione è basato sulla divisione nei vari step di un flowchart.
Nel secondo caso invece il particolare modo con cui sono salvate le righe dell’indice è nascosto nel modulo Line Storage (oggi probabilmente lo chiameremmo un oggetto). La suddivisione si basa sul criterio di nascondere quanto più possibile le scelte implementative che possono cambiare nel tempo e diciamo che si basa appunto sull’information hiding.
Information hiding
Si badi bene, l’information hiding della programmazione ad oggetti ha origine da questo articolo, ma viene spesso declinato come una proprietà che devono avere gli oggetti, cioè gli oggetti devono rivelare di sé il meno possibile. Raramente viene utilizzato come criterio per definire gli oggetti stessi, vale a dire come criterio per individuare gli oggetti e le loro responsabilità.
Il suggerimento quindi è quello di non suddividere in moduli che rappresentano le singole fasi di un flowchart di un processo, ma invece di suddividere in moduli in modo tale che nascondano gli uni agli altri le scelte implementative. Il fatto che le interfacce dei moduli debbano rivelare il meno possibile dei dettagli implementativi e del loro funzionamento interno è una conseguenza del fatto che un modulo nasce per nascondere una scelta di design.
Come abbiamo visto questo criterio rende il sistema più resiliente ai cambiamenti, ma oltre a questo ha anche altri vantaggi non trascurabili.
Vantaggi della modularizzazione con information hiding
In primo luogo il design ottenuto è molto più comprensibile perché i dettagli implementativi sono confinati all’interno del singolo modulo mentre le relazioni e le interfacce tra i moduli sono più semplici e generiche.
Proprio questa semplicità e chiarezza nella relazione tra i moduli permette anche di suddividere il carico di lavoro tra soggetti diversi che non avranno bisogno di lunghe sessioni di coordinamento per definire le interfacce di comunicazione tra i moduli e potranno quindi svilupparli in relativa autonomia.
Infine le dipendenze tra i moduli sono ridotte al minimo, facilitandone il riuso. Nel nostro esempio il modulo Line Storage non dipende da nessun altro modulo e può quindi essere riutilizzato in qualsiasi altro contesto dove c’è la necessità di gestire liste di stringhe.
Conclusioni
In conclusione si dimostra ancora una volta che, più che gli artifici tecnologici, sono le buone idee, tradotte in criteri e pratiche che migliorano la qualità del codice e che le buone idee sopravvivono alle mode e mantengono la loro forza negli anni, formando e indirizzando il nostro agire.