Debito tecnico: definizione e approccio pratico
Lo sviluppo di software avviene tradizionalmente in tre fasi: sviluppo, testing, rilascio. A seconda dei casi queste fasi possono poi essere divise ulteriormente: la fase di sviluppo potrebbe prevedere dei rilasci continui in un ambiente alpha che conterrà feature incomplete ma permetterà di avere feedback più rapidi; il testing potrebbe avvenire in diversi ambienti a cascata (es. Il primo è un ambiente isolato con base dati vuota, il secondo contiene una base dati realistica, il terzo è sottoposto a stress testing, etc.).
Nello sviluppo tradizionale di software consideriamo la feature al pari di un prodotto fisico come una sedia o un tavolo: arrivati al rilascio in produzione il prodotto è considerato esente da difetti strutturali, funzionante, e si può passare ai prodotti successivi. In alcuni casi si alloca un periodo di regime controllato dopo il rilascio, durante il quale si ammette che possano verificarsi bug (che verranno quindi risolti). Ma dopo questo periodo, il prodotto è appunto esente da difetti.
Il problema è che una feature software non è una sedia.
Una volta rilasciata potremmo scoprire che gli sviluppatori non avevano compreso appieno le specifiche, con la necessità di riscrivere parti di codice in fretta, senza badare troppo alla leggibilità.
Potrebbe anche produrre esattamente il valore di business richiesto, essere performante e non contenere bug. Eppure, malgrado uno sviluppo eccellente, un cambio di strategia potrebbe richiedere di modificare la feature mesi o anni dopo.
Anche nei casi più fortunati, il solo passare del tempo aumenta di giorno in giorno la possibilità che il codice vada toccato di nuovo, per accomodare un aggiornamento tecnologico ad esempio.
In qualunque modo capiti, d’un tratto abbiamo per le mani codice di tanto tempo fa, scritto da persone che hanno lasciato da tempo il team, scritto con una tecnologia obsoleta, e persino una piccola modifica che normalmente richiederebbe qualche giorno improvvisamente richiede settimane.
Ad esempio ci si ritrova ad aver scritto due anni fa un ecommerce nell’arco di un mese, ma ad aver bisogno oggi di due mesi per poter cambiare una singola feature, e a chiedersi di chi è la colpa, e se è il caso di continuare a mantenere uno strumento tanto costoso.
Stiamo andando per esagerazioni, ma neanche tanto.
Solitamente si tende a considerare il debito tecnico come qualcosa che colpisce solo il “codice vecchio”. Ma non è detto che un applicativo scritto negli anni ‘70 sia più complicato di uno scritto negli anni ‘90.
Il concetto di debito tecnico è una metafora concepita da Ward Cunningham per descrivere quanto un progetto software sia complesso. Secondo Cunningham rilasciare un codice non-buono è come contrarre un debito, che può essere ripagato riscrivendo il codice stesso. Ogni minuto passato sul codice non-buono aumenta l’interesse di questo debito.
Quindi, se lo sviluppo avviene senza seguire una corretta metodologia, l’interesse sul debito può salire fino a paralizzare il progetto.
Certamente c’è differenza tra sviluppatore e sviluppatore, e tra fornitore e fornitore. L’esperienza aiuta a scrivere codice migliore, a comprendere meglio le specifiche di ciò che si sta facendo. Ma non basta: a volte anche il codice migliore è comunque difficile da capire e modificare, e quindi ha un debito tecnologico alto.
Un’altra definizione, la più comunemente accettata a oggi, è quella data da Michael Feathers nel suo Working Effectively with Legacy Code: è codice senza copertura di test. È sicuramente una descrizione migliore che associare il termine solo a codice arcaico.
Il motivo per cui la scrittura di test è una così buona precauzione contro il debito tecnico è che i test forniscono una documentazione sul funzionamento dell’applicativo.
Per cui piuttosto che di codice non testato, si parla di codice:
Scritto in modo non chiaro
Che non contiene spiegazioni sul funzionamento
Che non spiega quali idee e quale processo decisionale hanno portato alla sua scrittura
Il che offre un punto di vista completamente diverso sulla questione. Perché a questo punto il debito tecnico non è più solo un problema tecnico, ma è anche un problema di comunicazione.
Sei interessato conoscere lo stato e i problemi della tua applicazione web?
Come si genera il debito tecnico?
Riprendiamo l’esempio dell’ecommerce citato all’inizio.
Alle prime 100 righe di codice scritte questo debito tecnico si potrebbe ripagare mostrando il codice ad un collega. Questa operazione permetterebbe di condividere la conoscenza, di avere una conferma che la logica sia corretta e che il codice sia leggibile da altri.
Dopo un mese, al rilascio, c’era il codice minimo indispensabile per far funzionare la piattaforma. In questo momento il debito tecnico si potrebbe ripagare illustrando il processo d’acquisto ai colleghi, ricontrollando il codice in modo da assicurarsi che non vi siano punti troppo complessi da leggere e scrivendo dei test automatici che garantiscano il funzionamento del processo d’acquisto. È un debito tecnico più alto, ma sembra ancora accettabile.
Due mesi dopo avviene lo sbarco in un paese asiatico: cambia tutto, da tasse calcolate a dati richiesti. Dato che ogni modifica può rompere il processo d’acquisto, gli sviluppatori iniziano a fare prove sull’ambiente di staging dopo ogni modifica. Il che crea un rallentamento negli sviluppi. Il debito tecnico è ora talmente alto che inizia a causare i primi problemi. Per ripagarlo bisognerebbe cambiare completamente strategia, iniziando a dedicare del tempo per scrivere test e ripulire il codice nei punti più ostici.
Sei mesi dopo. Ad ogni nuovo rilascio appaiono nuovi inquietanti bug, per questo motivo è stato assunto un addetto al controllo qualità che si occupa di visitare l’ambiente di staging prima di ogni rilascio per individuare bug. In questo momento l’interesse generato dal debito tecnico è tale da richiederci una professionalità in più, di cui altrimenti non ci sarebbe bisogno.
Un anno dopo. L’azienda si è ingrandita e bisogna aggiungere il concetto di brand all’ecommerce. Il che richiede di ristrutturare tutto l’applicativo. Il team fa quello che può, ma gli sviluppi procedono a rilento. Possiamo immaginare in questo momento il debito tecnico come un serpente che si morde la coda: il debito tecnico è tale da rallentare il team negli sviluppi, di conseguenza il team non ha tempo per rifattorizzare.
Si arriva di solito ad una situazione di stallo non troppo lontana da quanto descritto. Riscrivere l’applicativo è troppo costoso (e riscrivere non è quasi mai una buona scelta), modificarlo è troppo rischioso.
Volendo sintetizzare, ogni volta che si parla di:
Codice difficile da modificare
Codice difficile da leggere
Lentezza dei tempi di sviluppo
Difficoltà a espandere il team
Difficoltà ad aggiornare l’applicativo
Difficoltà a tenere in piedi l’applicativo
Stiamo parlando di debito tecnico.
Come riduciamo il debito tecnico?
Abbiamo detto che il debito tecnico è un problema di comunicazione, e la cosa non può non farci tornare alla mente la legge di Conway: la base di codice di un’organizzazione riflette le strutture di comunicazione dell’organizzazione stessa.
Prima ancora di risolvere il debito tecnico nel dettaglio, modificando il codice, bisogna quindi risolvere il problema della comunicazione. Una volta stabilite le strutture di comunicazione da usare per documentare il funzionamento del progetto, il progetto decisionale che ha portato a definire le specifiche funzionali, le idee dietro lo sviluppo del codice, allora si può iniziare ad addentrarsi nella modifica del codice.
Quali sono questi strumenti di comunicazione, che determinano con così tanto margine la riduzione del debito tecnico?
I test di behavior: test automatici che verificano il funzionamento di una feature a un livello molto alto (es. Usando il browser in un prodotto web). Oltre al loro valore come test, sono di solito; semplici da capire e rivelatori sulle intenzioni dello sviluppatore
I commenti e la formattazione del codice. Anche una semplice riga vuota tra due blocchi di codice può comunicare molto (ad esempio che i due blocchi si occupano di due fasi distinte del processo);
La nomenclatura di variabili, oggetti, metodi e funzioni. Chiamare una variabile “valore” non dice molto su cosa contiene, un metodo “calc” non sta dicendo molto su cosa stia calcolando;
Gli errori: lanciare errori con una descrizione dettagliata di ciò che succede può salvare ore fra sei mesi o un anno, quando quell’errore sembrerà apparire dal nulla e rompere un processo che non dovrebbe rompersi;
Di nuovo sugli errori: non c’è cosa peggiore che non lanciarli. La situazione peggiore è quando niente si rompe ma non c’è nessun risultato (es. Vedo la pagina “acquisto riuscito” ma non ho comprato nulla). Gli errori servono, non vanno evitati.
Il version control system. Un commit su Git scritto bene può essere lo strumento più prezioso quando bisogna risolvere un bug incomprensibile, o quando si incontra una riga di codice oscura.
La documentazione tipica “di progetto”: procedure di setup, di deploy, di creazione di nuovi ambienti, per l’esecuzione dei task di sistema, e così via;
Chat/gruppi aziendali come slack o forum. Hanno uno storico persistente e possiamo usarle per tenere traccia di discussioni sul processo decisionale;
e ce ne sono tanti altri, alcuni più ovvi e alcuni meno.
Così facendo, si sfrutta il debito tecnico per costruire qualcosa di nuovo, una base di partenza che permetterà la condivisione della conoscenza tra gli sviluppatori (presenti e futuri) e un ponte di comunicazione tra l’owner della piattaforma e gli sviluppatori della stessa.
Con gli strumenti di comunicazione pronti, si procede ad analizzare il progetto come se fosse un ritrovamento archeologico, documentando ogni reperto e cercando di dare una spiegazione ad ogni elemento.
La vera pulizia di codice dovrebbe poi essere fatta per gradi, man mano che si sviluppano nuove funzionalità o che il tempo passa. Riscrivere un prodotto non è quasi mai una buona idea, perché nella riscrittura si perderanno tante specifiche “implicite”, tante feature nascoste che però permettevano il funzionamento che conoscevamo.
Solitamente è più conveniente per tutti se si pulisce ciò che si ha intorno. Avviare un processo di refactoring massiccio pone tutta una serie di problematiche:
Richiede un budget sostanzioso
Richiede che gli sviluppi cessino mentre si effettua la pulizia
Aumenta la possibilità di generare bug perché potremmo pulire feature che non comprendiamo a pieno e che non verranno testate in tutti i loro casi limite
Immaginiamo di avere casa in disordine e che ci siano ospiti in arrivo (comprare una casa nuova e vendere la vecchia non è un’opzione). Se cerchiamo di pulire tutte le camere insieme c’è il rischio che gli ospiti arrivino e la casa sia ancora in disordine, o che dobbiamo accontentarci di una pulizia sommaria. Ha più senso ignorare per ora le stanze che probabilmente non visiteranno come la camera da letto, e concentrarci su soggiorno e sala da pranzo.
Allo stesso modo si può procedere con il refactoring. Si scrivono le parti nuove usando gli strumenti di comunicazione definiti e ogni punto dell’applicativo vecchio che viene toccato deve essere pulito e aggiornato al nuovo standard.
Individua le criticità della tua applicazione e scopri come affrontare un intervento di software remodeling.
L’esperienza ci ha insegnato a procedere con metodo quando veniamo contattati per rifattorizzare, aggiornare o mantenere applicativi esistenti.
Quando veniamo contattati, il primo passo è una Code Inspection, per valutare la situazione. Eseguiamo un’analisi sul codice, facendoci un’idea di quali sono i colli di bottiglia sulle performance, i punti del codice più difficili da leggere o da modificare, le vulnerabilità di sicurezza, quale documentazione manchi. Come risultato consegniamo al cliente una documentazione che descrive cosa abbiamo trovato e quali passi il cliente potrebbe intraprendere per migliorare la situazione.
A questo punto spetta al cliente decidere se far seguire la code inspection con l’avvio di un processo che miri a migliorare la base di codice. Se questo processo parte, si passa alla creazione di un team:
Se esiste già un team tecnico con una certa seniority i nostri sviluppatori di solito si integrano nel team;
Se esiste già un team tecnico composto da sviluppatori giovani cerchiamo di solito di lasciare i nuovi sviluppi al team esistente, magari integrando un mentor. Mentre un secondo team composto da specialisti si occupa della maintenance;
Se non esiste un team tecnico ne forniamo uno noi;
Alla definizione del processo di rilascio:
Nelle situazioni peggiori non è possibile prescindere da un team di operations;
Se puntare a rilasci continui (è ciò che consigliamo quando non vi sono controindicazioni) o schedulati in milestone;
E alla definizione delle cerimonie e degli strumenti di comunicazione: questo passo dipende molto da cliente a cliente. Un esempio potrebbe essere il seguente:
Standup di 15 minuti ogni mattina alle 10:00
Review meeting di 30 minuti ogni mercoledì alle 15:00
Planning meeting di 30 minuti ogni mercoledì alle 15:30
Rilascio in produzione ogni giovedì alle 02:00
Retrospettiva e report ogni primo venerdì del mese alle 17:00
Creiamo le definizioni di Completo. Esempio:
Perché una feature possa essere rilasciata in staging deve:
Contenere test automatici
Ricevere una Code Review da un collega
Essere documentata nel Changelog
Avere un test d’integrazione
Perché una feature vada in produzione deve:
Essere approvata dal Product Owner
Perché un bugfix sia rilasciato deve:
Avere almeno un test che descriva l’assenza del bug
Solo a questo punto si parte con l’operatività. L’obiettivo è di rendere l’applicativo stabile, raggiungendo quel punto di equilibrio tra crescita e stabilità nel quale non siamo rallentati negli sviluppi e riusciamo a mantenere un debito tecnico basso.
Nel tempo ci ritroviamo a ridurre man mano la dimensione del team che allochiamo sul progetto perché:
La mole di test automatici riducono le regressioni ad ogni nuovo sviluppo;
Il codice diventa più leggibile e più facile da gestire;
Gli sviluppatori più giovani imparano come scrivere codice con un debito tecnico ridotto o assente.
Il benessere che si crea a seguito di questo processo permette quindi di effettuare modifiche più facilmente. Allo stesso tempo crea anche una nuova classe di sviluppatori, più virtuosi e più attenti a certi temi. Alla fine questo processo diventa parte della cultura aziendale.
Leggi l'articolo in inglese pubblicato su Mikamayhem, il blog dei Developer di Mikamai.
Contattaci per una analisi della tua applicazione o per un confronto sul nostro metodo di lavoro.
Articolo scritto da Nicola Racco
!function(f,b,e,v,n,t,s) {if(f.fbq)return;n=f.fbq=function(){n.callMethod? n.callMethod.apply(n,arguments):n.queue.push(arguments)}; if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0'; n.queue=[];t=b.createElement(e);t.async=!0; t.src=v;s=b.getElementsByTagName(e)[0]; s.parentNode.insertBefore(t,s)}(window,document,'script', 'https://connect.facebook.net/en_US/fbevents.js'); fbq('init', '475678749439170'); fbq('track', 'PageView');