Qualche giorno fa l’amico JustB ha sollevato un interessante quesito su Meemi:
“Avete consigli su come approcciare lo studio di un’applicazione legacy su cui dovrete lavorare ed effettuare modifiche, considerando che si tratta di un’infinita schiera di file salvati in n+1 cartelle, che si includono a vicenda, senza uno straccio di documentazione né commenti esplicativi?”
Domanda molto interessante, a cui vorrei rispondere ora, seppur brevemente…
L’approccio al problema
A chi non è mai capitata una cosa del genere, ossia avere a che fare con codice di terzi, magari legacy? Ovviamente la risposta è banale – tutti! – e, salvo rari casi, il compito non è mai una passeggiata.
Il problema non è solo metterci le mani, quanto capirlo e comprenderlo per poi decidere il da farsi.
Questo tipo di strategia però non sempre è possibile ed è abbastanza normale dover ottenere in fretta dei risultati – quindi modificare direttamente quel codice! – senza avere il tempo di effettuare un’ispezione accurata. Un po’ come buttarsi a scrivere un nuovo programma senza neppure avere un’idea di fondo di quello che dovrà fare e soprattutto come dovrà farlo.
Esiste poi una sorta di atteggiamento spocchioso e terribilmente comune nei programmatori che si approcciano al lavoro di terzi ed è quello di partire prevenuti nei confronti degli altri, salvo poi peggiorare facilmente nel giudizio man mano che va avanti nella lettura: al minimo errore individuato – reale o presunto che sia -, al minimo commento incompleto o inesatto, alla minima imprecisione nell’indentazione, ecco che iniziano i mugugni del programmatore di turno verso “chi ha fatto quello scempio“.
Le motivazioni di questo tipo di atteggiamento sono le più disparate: dal “io queste cose le so fare meglio” fino al diventare una buona scusa per giustificare ulteriori ritardi (“è talmente ingarbugliato che in pratica lo sto riscrivendo!”).
| Per esperienza, direi di concedere al codice altrui ed al suo autore il beneficio del dubbio, almeno finchè non si è conclusa un’attenta e relativamente lunga fase di ispezione.
Di rimando, mai fidarsi ciecamente delle impressioni di un programmatore alle prese con codice non suo, almeno finchè non si è sporcato le mani davvero, in profondità. Anche perchè, finchè un dev non arriva ad un certo livello di intimità con del codice, di norma non può sapere quanto ne dovrà cambiare: in altre parole, a seconda del singolo progetto e a fronte anche di migliaia e migliaia di potenziali righe di codice da visionare, quelle effettivamente da modificare potrebbero essere molto poche. Un conto sono valutazioni oneste, frutto di indagini serie e prolungate nel codice altrui, un conto sono supposizioni ed illazioni “spannometriche”, emesse in fretta e furia e, tavolta, anche con una certa malizia di fondo. |
Di certo leggere poche righe di codice e, senza nemmeno averle capite, decidere di riprogettare il codice da zero è una scelta radicale e spesso controproducente: posso capire quando il codice altrui fa veramente pietà e non c’è modo per manutenerlo, ma il capriccio di volerlo rifare a tutti i costi no. Non c’è sindrome “Not Invented Here” (NIH) o volontà di dimostrarsi più bravi che tenga.
Il problema: approcciare l’ignoto
Il problema più grosso con il codice è l’ignoto. L’ignoto è codice altrui, che vedete per la prima volta, e perfino codice che voi stessi avete scritto anni prima e che non avete più toccato da eoni.
È un ammasso minaccioso di righe che dovete comprendere prima ancora di poterle modificare. Naturale quindi che, impaurito, qualcuno si ribelli a questa situazione e, piuttosto di affrontarla, opti per ripartire da zero. Inoltre, come direbbe Sposlky:
It’s harder to read code than to write it.
Da questo punto di vista i programmatori sono l’esempio per antonomasia di persone che non sono mai contente fino in fondo e che bisogna in qualche modo controllare: è normale che un dev arrivi ad un certo punto del suo lavoro e pensi/provi insistentemente a ripartire da capo – un errore generalmente gravissimo! -, figuriamoci se il codice iniziale l’ha scritto qualcun altro.
Prima regola per affrontare l’ignoto: non andare in panico, non tirare i remi in barca prima di averlo affrontato e possibilmente compreso, anche solo parzialmente. Sono le fasi iniziali quelle critiche: l’autocontrollo è essenziale. Non appena si comincia a capire qualcosa, la paura passa di colpo, automagicamente.
Un approccio personale
Dopo aver sconfitto il demone della riscrittura from scratch, è fondamentale circoscrivere ed analizzare l’ignoto. Ognuno di noi sviluppa col tempo le sue personali strategie per raggiungere i suoi scopi.
Il mio è relativamente semplice ed è sintetizzabile in analizza, circoscrivi e migliora, ripeti.
1. Analizza
Analizzare il codice significa volerlo capire con un certo livello di dettaglio. Se le dimensioni del codice e dell’eventuale documentazione a corredo sono contenute, apro il codice in un IDE e provo a capire da solo.
Molto più spesso uso invece Doxygen, che considero alla stregua di un coltellino svizzero. Da Wikipedia:
“Doxygen è una applicazione per la generazione automatica della documentazione a partire dal codice sorgente di un generico software. È un progetto open source rilasciato sotto licenza GPL, scritto per la maggior parte da Dimitri van Heesch a partire dal 1997.
Doxygen è un sistema multipiattaforma (Windows, Mac OS, Linux, ecc.) ed opera con i linguaggi C++, C, Java, Objective C, Python, IDL (versioni CORBA e Microsoft), Fortran, PHP, C#, e D. Nell’ambito del C++, è compatibile con le estensioni Qt.
È il sistema di documentazione di gran lunga più utilizzato nei grandi progetti open source in C++. Due esempi per tutti, sono l’adozione di doxygen da parte di ACE e KDE. In Java invece, la posizione leader viene meno, in virtù della presenza del concorrente Javadoc.
Il sistema estrae la documentazione dai commenti inseriti nel codice sorgente e dalla dichiarazione delle strutture dati.”
Non solo è utile per creare documentazione partendo dal codice, documentato tramite opportuni tag, ma è estremamente efficace nell’estrarre anche altro.
Ad esempio, recentemente ho deciso di dare un’occhiata al codice di Doom 3 di ID Software, rilasciato finalmente sotto licenza GPL e pubblicato sul sito GitHub in questo repository pubblico.
Non è certamente un progetto-giocattolo ed infatti richiede una buona competenza tecnica per maneggiarlo, nonostante sia generalmente ben scritto (in realtà avrei alcune riserve da programmatore a programmatore – vedi sezione precedente -, ma non è la sede giusta per farlo).
Capirci qualcosa non è quindi stato molto facile, ma Doxygen si è nuovamente rivelato utilissimo per esplorare il codice cercando di capire come è stato strutturato – non sempre l’organizzazione dei file è intuitiva nè i programmatori-autori lasciano spesso file di spiegazione esaustivi sull’argomento -.
Premesso che non volevo studiare il codice a fondo, ma solamente dargli un’occhiata rapida, ma con Doxygen questa “passeggiata” mi è diventata decisamente più godibile.
Usare Doxygen poi è veramente facile: basta installarlo, lanciare l’interfaccia grafica (GUI), configurare pochi parametri e fa tutto lui. Se poi avete installato anche la suite Graphviz, potrete attivare l’opzione per ottenere i diagrammi , anche simil-UML, con dot.
Un breve riassunto, step-by-step:
Dopo un’oretta buona di processamento – il codice è relativamente complesso e denso di informazioni estraibili – ho quindi potuto leggere i risultati direttamente in un browser. Trovo molto comodo che sia possibile includere nella documentazione generata da Doxygen anche i sorgenti stessi. In un colpo si riesce a:
- osservare il progetto nella sua interezza, a dispetto del suo essere eventualmente sparpagliato in mille file in mille directory diverse;
- esplorarlo in vari modi: per classe, namespace, e così via,
- leggerlo in un modo esteticamente piacevole: Doxygen uniforma tutto, alla faccia dei file/sorgenti scritti in modo diverso;
- analizzarlo in modo efficiente: viva gli hyperlink per passare da un file ad un altro, da una funzione all’altra senza perdere tempo e con la possibilità di usare il tast “Indietro” del browser!.
Inoltre Doxygen stesso è efficiente nel senso che se modificate un file, di norma non rigenererà da zero la documentazione, ma solo la porzione di essa che è toccata dai cambiamenti nel codice.
2. Circoscrivi e migliora
Ogni progetto di una certa taglia è il frutto del lavoro di uno o più individui nel corso del tempo. Il risultato è che raramente il suo codice appare ed è omogeneo.
Realisticamente, soprattutto per programmi con una certa anzianità, ci si trova con una pletora di file, ognuno col suo codice scritto in modo “particolare“: non di rado capita di trovare differenze abissali fra un file ed un altro nello stesso progetto.
| In C e C++ si parla di compilazione per unità singole per indicare il modo con cui avviene normalmente la compilazione, ossia file per file. In sostanza, al netto del significato reale, ogni file sorgente fa storia a sè durante quella fase.
Avviene la stessa cosa anche in fase di scrittura: file diversi fanno storia a sè. Uno stesso programmatore nel tempo può cambiare in modo considerevole il suo stile di scrittura, al punto da non riuscire a identificare come suo del codice che ha sviluppato molto tempo prima. |
Dopo aver capito cosa fa grossomodo un progetto si passa all’analisi per “sottosistemi”, se è possibile individuarne dei limiti: di solito i file sorgenti nei progetti di una certa dimensione sono raggruppati in qualche modo, per agevolare lo sviluppo e la manutenzione di parti logicamente legate.
Ad esempio tutto quello che riguarda l’accesso ad un filesystem potrebbe essere stato posto in una directory fs oppure tutti i file potrebbero risiedere nell’unica directory con gli altri sorgenti ma essere rapidamente individuabili da un prefisso fs nel nome. Nel caso invece tutto fosse alla rinfusa, nuovamente, Doxygen torna utile per capire le interrelazioni fra file e funzioni.
A quel punto, in perfetto stile divide et impera, si può approcciare l’ignoto dedicandosi ad un “sottosistema” per volta, cercando di comprenderne la struttura ed il ruolo per poi modificarlo se è il caso.
Durante questa fase, se è il caso, di solito provo anche a ridurre il numero di file, per renderlo maneggiabile: detesto maneggiare una miriade di piccolissimi file ognuno con una classe di pochissime righe (una certa filosofia nel mondo Java sembra favorire questo approccio). Per cui si procede con una riorganizzazione per passi dei file e poi al refactoring del codice vero e proprio.
L’idea è capire cosa fa un pezzettino del programma ignoto e, compreso, ridurne le dimensioni per rendere le successive modifiche più efficienti. Accorpare e semplificare sono le due parole magiche:
“La perfezione è raggiunta non quando non c’è più niente da aggiungere, ma quando non c’è più niente da togliere. (Antoine de Saint-Exupéry)
Durante questa fase, procedendo per passi piccoli ma decisi, vale la pena uniformare il codice, ossia applicare e riapplicare un manuale di stile e delle coding convention, precisi e predeterminati.
A questo punto il trucco si ripete, ossia si riparte dall’analisi e si continua finchè l’ignoto diventa qualcosa di compatto, manutenibile ed assolutamente non spaventoso.
Che ne pensate?






Come al solito chiaro e utile!
Mi sento ovviamente chiamato in causa quando parli di criticare il lavoro altrui, ma i commenti che si fanno sono più “da stadio” (“Uààà com’è scarso, ha sbagliato il rigore, io l’avrei segnato”) che un attacco verso chi ha scritto quel codice, dato che come dici anche tu, non sappiamo quali condizioni contingenti hanno spinto i nostri predecessori a scrivere proprio quel codice.
Detto questo, volevo aggiungere una piccola considerazione, sul refactoring. Mi ha molto aiutato, una volta circoscritto un sottosistema e i suoi metodi, scrivere dei test proprio per analizzare il codice invece di limitarmi ad effettuare una lettura passiva del codice. (con qualunque suite della famiglia xUnit disponibile per quasi ogni linguaggio esistente)
In questo modo si crea una specie di documentazione e di esempio sull’utilizzo dei metodi (“ma come si usa questa funzione?”..se non ricordo male ne parlavi in un tuo precedente post) a uso e consumo proprio e di chi verrà dopo di noi.
Insomma…è un lavoro sporco, ma bisogna farlo
Jp, stranamente concordo con te
rilancio solo di due considerazioni:
- a doxygen (che adoro, fra parentesi) preferisco un analisi manuale del codice (quindi con diagrammi uml generati a manina) solo perché IMHO riesci a capire meglio le dipendenze. Infatti a parità di codice e quindi di UML generato in modo automatico, una dipendenza può essere nata per motivi diversi: o per semplificare il codice da scrivere o per implementare un determinato design. Questo implica che potrebbe essere utile avere (in realtà) rappresentazioni diverse per i due casi.
- concordo con JustB che l’introduzione dei test è fondamentale. Soprattutto prima di ogni refactoring serio; questo permette di utilizzarli come regression-test per essere sicuri di non introdurre nuovi e potenzialmente dannosi bug.
Naturalmente IMHO il refactoring andrebbe fatto per piccoli componenti e ogni piccolo componente andrebbe considerato come una black-box (quando possibile): dovrebbe essere sostituito a quello iniziale senza cambiare il funzionamento del tutto
@justb: i test, inclusi quelli di tipo Unit, sono un altro buon modo per circoscrivere il perimetro dell ignoto, analizzando gli output partendo dagli input noti. In pratica tramite una serie di prove si può intuire il comportamento delle singole parti.
Decisamente un (altro) buon modo di procedere, direi un asso in più da giocare senza problemi.
@Eros: come ho detto, ne faccio un discorso di dimensioni nel senso che “scomodo” Doxygen quando mi rendo conto di avere a che fare con troppo codice e “non so da dove partire”. Sui test, ho detto poco sopra: concordo in pieno! ^^
Ciao & grazie di essere passati! ^^
Ciao JP, ottimo post come al solito
Mi unisco ai commenti sugli Unit Test e aggiungo (visto che ormai si tratta di una pratica che comincia ad essere in uso da qualche anno) che anche il codice legacy ha sempre più possibilità di avere, da qualche parte, una suite di test.
E siccome il codice dei test risulta spesso utilissimo per comprendere il funzionamento di un progetto, suggerisco come prima cosa di … andare subito a cercare eventuali test, e di eseguirli ed analizzarli quanto prima !!
@Stefano: il bello degli unit test è che si ragiona per unità, cioè parti. Molto comodo analizzarli in prospettiva di reverse engineering: capiti quelli, si capisce la struttura e la suddivisione in componenti. E come funzionano.
Ciao & grazie di essere passato! ^^