Quando fare Refactoring: la mia esperienza.
Enri
Piu’ volte su questo blog ho parlato di quanto sia importante la qualità per abbattere i costi totali.
Con questo post voglio riflettere piu’ nel dettaglio per comprendere quando una soluzione comoda e magari poco elegante possa avere senso. Inoltre quando paga rifattorizzare una soluzione dal forte grado di accoppiamento tra le componenti?
Di seguito un piccolo sunto del paragrafo “When to refactor” di “Refactoring, improving the design of existing code“:
- Regola dei tre strikes: “la prima volta implemento la piu’ semplice, la seconda volta, consapevole della duplicazione, duplico, la terza volta che faccio qualcosa di simile, rifattorizzo”
- Quando si fixa un bug: codice piu’ chiaro e/o si migliora la testabilità
- Quando una nuova funzionalità deve innestarsi in codice poco chiaro: migliorare la comprensibilità per diminuire il rischio di interventi non corretti
- Quando rende piu’ veloce implementare una nuova funzionalità: migliorare la malleabilità
Alla base del refactoring vi è l’idea di minimizzare il costo di cambiamenti futuri non con una previsione degli stessi, ma creando un design che per la sua semplicità sia in grado di essere modificato e reso pronto a qualsiasi cambiamento con poco sforzo. L’effort di tali modifiche (aka mosse di Refactoring) rimane relativamente basso se e solo se il design è mantenuto ragionevolmente semplice. La pratica del TDD a questo fine è un arma molto potente.
Sappiamo tuttavia che anche con un processo scope-oriented e con un team Agile, molto spesso ci si trova ad avere applicazioni o zone dell’applicazione con del debito tecnico. In questi casi cercare di scontare quel debito di design seguendo unicamente le linee guida sopra elencate per la mia esperienza non è efficace. Spesso in presenza di un forte debito tecnico ereditato dal passato, i refactoring sarebbero molto, troppo estesi. Dire semplicemente facciamoli a piccoli passi non mi aiuta molto: servono delle euristiche che tengano conto del contesto e dei particolari vincoli di risorse.
Alcuni dei dubbi che sorgono spesso quando devo scrivere una funzionalità e mi imbatto di fronte a codice poco chiaro, troppo complesso o pieno di smell, sono:
- Ha senso per questa funzionalità, fare refactoring?
- Fino a che punto ha senso addebitare a questa storia il costo aggiuntivo (pagare il debito) derivante dal refactoring?
- In altri termini: ammesso che in questa particolare situazione sia piu’ veloce ignorare il disordine non rifattorizzando, ha senso mettere ordine nel design, o per questa funzionalità in realtà sarebbe piu’ efficace non farlo?
Sono domande che spesso mi hanno portato a frustrazione per mancanza di linee guida che realmente mi dessero un aiuto concreto per cercare una risposta. Sto parlando delle situazioni in cui fare refactoring aggiunge un costo alla storia rispetto all’aggiungere la funzionalità senza farlo. In caso contrario è chiaro che la scelta non si pone neanche.
Dal momento che ragionamenti puramente tecnici non mi hanno dato una mano per prendere queste decisioni, ho provato a guardare piu’ in alto, a cercare delle linee guida illuminato dagli obiettivi di business della particolare storia sulla quale si pongono quelle scelte. Ancora una volta mi sono sentito piu’ libero come programmatore ponendomi domande a livello di business.
Ad oggi i ragionamenti che faccio per farmi guidare sono guidati da un’analisi costi/benefici:
- Su questa storia, con un valore X e stimata di un costo Y, è ancora ragionevole il rapporto costi/benefici aumentandone il costo? Esempio: una storia mi dà Valore 10, e costa 5 senza refactoring. Con il refactoring aggiuntivo, il costo salirebbe a 7. Ha ancora senso per il business sviluppare quella storia? Se no, è possibile fare solo una parte del refactoring?
- Analisi costi/benefici a “medio/lungo termine“: riguardo a questo vi sono due aspetti:
- la mantenibilità (=complessità, rischio di errori, comprensibilità) del SW che implementa la storia, alla luce del refactoring/non refactoring, quale sarà?
- quali probabilità vi sono di avere estensioni future in qualche modo legate alla storia? Confrontare costi dell’investimento aggiuntivo rispetto all’ipotetico valore guadagnato in futuro.
Il secondo punto implica tra le altre cose, una consapevolezza del design che non è scontata. Implica da un punto di vista tecnico essere in grado di comprendere quali sono i punti di estendibilità lasciati dal codice, quali gli assi di cambiamento ai quali le varie classi rispondono, sapere identificare gli smell ed i debiti del codice, quali parti del codice andrebbero cambiate per supportare una nuova funzionalità.
Senza questi skills da parte dei programmatori e senza un management fondamentalmente orientato alla qualità diventa molto rischioso prendere queste decisioni. Vedere la qualità come l’unica variabile del processo su cui sia possible tagliare è sempre dietro l’angolo. Inutile ripetere che questo approccio non paga.
E’ importante saper valutare quali sono i requisiti e le regole di business a piu’ alta volatilità, isolarli dal resto del sistema ed investirvi le giuste risorse. Una buona strategia puo’ essere di guardare nella storia passata, evincendo la frequenza di cambiamento, la volatilità del sistema sotto osservazione. Ad esempio per una zona del sistema per cui sappiamo che il cliente non chiede modifiche da svariati mesi, avrà senso investire un costo aggiuntivo di refactoring minore rispetto ad una zona con alta probabilità di cambiamento, o su un progetto nuovo. Tutto questo puo’ essere fatto solo a stretto contatto con il product manager e con il business, analizzando Vision del prodotto e obiettivi di business.
Anche nel caso frequente in cui non si riesca ad arrivare a dei numeri, il solo fatto di fare ragionamenti di questo tipo permette di prendere una decisione in modo piu’ consapevole. Ad ogni modo nel dubbio, preferire la scelta che si muove verso la soluzione pulita: gli interessi da pagare per un Debito Tecnico possono diventare molto alti.
Concludo con una piccola esperienza personale. Un programma che importa dei nuovi clienti nel nostro sistema presentava dei costi fissi incidentali dovuti all’intervento umano a causa di errori di vario tipo nei dati da importare. Le scadenze imposte dal Boss per il particolare import dei clienti e l’impatto di un ritardo, non permettevano una modifica “pulita” al fine di gestire tali errori nei dati in modo automatico. Si è tuttavia deciso pragmaticamente di effettuare un piccolo passo verso l’aumento del Ritorno, sacrificando la mantenibilità (abbiamo duplicato) del codice, a favore del ritorno derivante dall’alta frequenza con la quale quell’importer veniva eseguito. Abbiamo paragonato il costo dovuto alla necessità dell’intervento umano, valutato sulla frequenza dell’esecuzione dello stesso, rispetto al costo della modifica per eliminarlo e al costo della mantenibilità, arrivando ad un numero che rendeva piu’ evidente la scelta.
Ragionare in questi termini aiuta molto i programmatori, fornire questi dati al Business in modo da metterlo in grado di prendere una scelta informata rende tutto il processo molto piu’ “smooth”.
Il valore guadagnato con la modifica all’Importer puo’ essere letto inoltre come un Credito spendibile anche solo in parte per ripulire l’Importer alla sviluppo della prossima funzionalità che lo interessa.
I vantaggi derivanti dall’aver abbattuto dei costi fissi per un’attività ad alta frequenza in questo caso superavano i costi aggiuntivi che potenzialmente la mantenibilità della modifica stessa comporterà. In questo caso quindi un’azione di Refactoring aveva poco senso, perchè avrebbe reso insostenibile la modifica all’importer.
Alcuni link sull’importante concetto di Debito tecnico:
Posted in Quality, Test Driven Development (TDD), Metrics |


August 7th, 2007 at 9:46 am
Riferendomi all’ultimo link che hai dato, ripropongo per l’ennesima volta la mia letterina ai programmatori. Per decenza ho tolto le parolacce, che su questo blog serio non stanno bene
Caro programmatore, sia tu un junior alle prime armi o un senior di provata esperienza (?!?!?), è così difficile?
E’ così difficile scrivere classi con meno di 50 metodi?
E’ così difficile scrivere metodi con meno di 50 righe?
E’ così difficile usare nomi di variabili comprensibili?
E’ così difficile scrivere codice che sia almeno un po’ object oriented?
E’ così difficile imparare un po’ i principi di design?
E’ così difficile imparare un po’ i patterns?
E’ così difficile non reinventare tutto da zero ed usare google?
Se hai risposto sì a tutte le domande…. cambia lavoro! Perché il tuo codice finirà nelle mani di qualcuno, e se quel qualcuno sono io spera di essere lontano… molto lontano.
August 10th, 2007 at 9:53 am
Bel post, come sempre. Aggiungerei però un fattore al conto economico della scelta se rifattorizzare o meno, ed è anche un po’ il motivo per cui “farlo all’ultimo momento utile” è un principio ottimo che non si applica così bene col refactoring : rifattorizzare del codice appena scritto costa tipicamente molto meno che rifattorizzare del codice scritto settimane prima. Non parlo del fatto che su quel codice si sia costruito dell’altro, a quel punto avremmo già rifattorizzato per permettere l’estensione delle funzionalità senza subire gli interessi compositi, ma del semplice fatto che il codice non rifattorizzato richiede sempre uno sforzo di comprensione prima di poterlo toccare, sforzo che è nullo nel caso si rifattorizzi a caldo. Forse dovremmo suddividere tra rifattorizzazione per la comprensione e rifattorizzazione per il design? La seconda la devi pagare se devi creare nuove funzionalità senza accumulare interessi, la prima accumula interessi a prescindere dalle nuove funzionalità, solo col tempo.
August 29th, 2007 at 10:42 am
Frank, bella la lettera ! Penso che la appendero’ da qualche parte in ufficio …
Carlo, non so se esiste una differenza tra rifattorizzare per la comprensione e per il desing. Se si rifattorizza bene, non si ha forse un vantaggio sia nel design che nella comprensione del codice ? In caso contrario, forse non conveniva rifattorizzare ?
Enri - bel post !
August 29th, 2007 at 9:55 pm
Bel post, complimenti!
E’ molto interessante la questione se fare o non fare il refactoring. Tuttavia mi pare che non sia stata messa in luce una problematica che, a mio avviso, capita abbastanza spesso e aumenta notevolmente il costo a seguito di un refactoring.
Sul progetto a cui sto partecipando accade spesso che più persone tocchino quasi contemporaneamente gli stessi file. Ciò accade perchè ci sono tante attività in corso, e i singoli svilppatori non hanno la possibilità di accordarsi per rendere omogenei gli interventi sul codice.
Questo implica che ci si trovi spesso di fronte alla necessità di fare un’operazione di merge. Credo sia evidente che un’operazione di merge possa già di per sè essere fonte di vari problemi ed inefficienze nello sviluppo (la persona A tocca il file in vari punti, B ne tocca altri, e non sempre questi punti risultano disgiunti e “autonomi” quando si vanno a combinare assieme durante il merge).
Proviamo ora ad immaginare cosa capita se uno dei due sviluppatori, oltre a fare l’intervento che gli è stato commissionato, decide anche di fare un refactoring dell’intero file. Qualsiasi tool di compare vede i due file quasi completamente diversi, per cui (supponendo che il refactoring l’abbia fatto A), per individuare dove e come inserire le modifiche di B nel codice di A post-refactoring è tutt’altro che banale. Può implicare la necessità di ristudiarsi tutto il codice per capire la logica e il modo per integrare le modifiche di B. Eventualmente eseguendo un secondo refactoring alla fine. Con il rischio aggiuntivo di impattare a sua volta su un altro merge…
Non vorrei apparire con questo troppo critico nei confronti del refactoring, ma vorrei evidenziare il costo che questo può avere in certe situazioni (anche a voi capita di dover fare spesso il merge dei vostri sorgenti?), anche in termini di “rischio”. Il rischio è, in questo caso, la possibilità di inrodurre bug o pericolose regressioni nel comportamente dell’applicazione, di cui non è sempre facile accorgersi in tempo in quanto non si possono fare sempre tutti i test ogni volta che si rilascia una nuova release. Infatti se la release comprende due funzionalità nuove, tipicamente si testano quelle mentre tutte le altre vengono (erroneamente, ma di fatto è così) date per scontate. Invece un refactoring seguito da uno o più merge complessi potrebbe mettere tutto in discussione…
August 30th, 2007 at 10:04 am
Davide,
Nel caso specifico penso che ci si potrebbe organizzare limitando il refactoring a sessioni specifiche, magari sincronizzate al momento in cui il merge di diverse modifiche e’ gia’ stato effettuato.
Tieni comunque presente che una frequenza molto alta di merge potrebbe indicare la presenza di classi, metodi, o moduli molto grandi e poco specializzati. In questo caso capita piu’ spesso che diversi programmatori lavorino sullo stesso codice. Un refactoring continuo dovrebbe limitare, a lungo termine, la necessita’ dei merge (anche se ovviamente e’ impossibile escluderli del tutto).
Per quanto riguarda il fattore rischio mi sento di poter affermare che ogni refactoring rappresenta un rischio (merge o non merge) e che questo rischio dovrebbe essere ridotto il piu’ possibile tramite una buona copertura di test automatici.