Refactoring: come migliorare un codice sorgente
Durante lo sviluppo di un’applicazione nei codici sorgente si accumulano punti strutturati in modo non pulito, che mettono a rischio la compatibilità e la possibilità di applicare un programma. Due sono le possibili soluzioni: un codice sorgente completamente nuovo o una ristrutturazione a piccoli step. Molti programmatori e aziende si orientano sempre di più verso il code refactoring per ottimizzare a lungo termine un software funzionante o per renderlo più chiaro e leggibile per altri programmatori.
Nel caso del refactoring ci si pone la seguente domanda: qual è il problema del codice che può essere risolto e cosa si può fare per risolverlo? Nel frattempo il refactoring è diventato una delle basi dello studio della programmazione e sta acquisendo sempre più importanza. Quali metodi vengono utilizzati e quali sono i vantaggi e gli svantaggi?
Cos’è il refactoring?
La programmazione di un software è un processo lungo e complicato al quale a volte lavorano più sviluppatori e il testo sorgente viene spesso rielaborato, adattato o ampliato. La pressione in termini di tempo e pratiche obsolete provocano però l’accumularsi di punti poco lineari nel codice sorgente, i cosiddetti code smell. I punti deboli, accresciuti nel tempo, mettono a rischio l’applicabilità e la compatibilità di un programma. Per evitare l’erosione continua e il peggioramento di un software è necessario il refactoring.
Il refactoring può essere paragonato alla revisione di un libro, che non comporta la nascita di un libro totalmente nuovo, bensì di un testo più facile da comprendere. Così come diversi sono gli approcci durante la revisione tramite troncature, riformulazioni, tagli e modifiche, anche nel code refactoring troviamo metodi come l’incapsulazione, la riformattazione e l’estrazione per ottimizzare un codice senza cambiarne il fulcro.
Questo processo chiaramente è meno costoso rispetto alla realizzazione di una struttura del codice completamente nuova. Il refactoring ha un ruolo importante soprattutto nello sviluppo di software di tipo interattivo e incrementale e anche nello sviluppo software agile, perché i programmatori modificano costantemente un software tramite questo modello ciclico. Il refactoring quindi è una fase di lavoro costante.
Quando un codice sorgente viene eroso: spaghetti code
Per prima cosa è necessario capire come invecchia un codice e come può mutare in un famigerato “spaghetti code”. A causa del poco tempo a disposizione, della mancanza di esperienza o di istruzioni poco chiare e di comandi inutilmente complicati, la programmazione di un codice porta a perdite di funzionalità. Un codice viene eroso in maniera proporzionale alla velocità e alla complessità del suo campo di applicazione.
Si chiama spaghetti code un codice sorgente confuso e illeggibile, il cui linguaggio è difficilmente comprensibile per i programmatori. Semplici esempi di codice confuso sono quei comandi di salto superflui (GOTO) che istruiscono un programma a saltare da una parte all’altra del codice sorgente, oppure for/while loop o istruzioni if non necessari.
Sono soprattutto i progetti ai quali lavorano più sviluppatori software ad avere dei testi sorgente poco leggibili. Se il codice sorgente passa per più mani e se già l’originale presenta dei punti deboli, è difficile evitare un aumento di grovigli ricorrendo a “soluzioni di emergenza” e a una costosa revisione del codice. Nel suo scenario peggiore uno spaghetti code può mettere a repentaglio l’intero sviluppo di un software; in tal caso nemmeno il code refactoring può eliminare il problema.
Non così gravi sono poi i code smell e code rot. Con il passare del tempo, un codice, a causa di elementi non puliti, può risultare poco lineare. I punti poco chiari peggiorano ulteriormente in seguito all’intervento di altri programmatori o alle estensioni. Se non si opera un refactoring alle prime avvisaglie di un code smell, il codice sorgente perde a vista d’occhio d’integrità e tramite il code rot (dall’inglese rot “marcio”) perde la sua funzione.
Qual è lo scopo del refactoring?
Il refactoring mira a un codice pulito e semplice, in una parola migliore. Se il codice è efficiente, i nuovi elementi del codice possono essere integrati in maniera migliore senza che si creino nuovi errori. I programmatori che possono leggere un codice senza sforzo, si orientano più rapidamente e possono eliminare o evitare in maniera più semplice i bug. Un altro compito del refactoring è una miglior analisi degli errori e manutenibilità del software. I programmatori che verificano un codice implicano quindi un lavoro minore.
Quali sono le fonti di errore che vengono eliminate con il refactoring?
Le tecniche che vengono impiegate nel refactoring sono tanto varie quanto gli errori che sono in grado di eliminare. Il code refactoring si definisce in base ai suoi errori ed indica gli step necessari per eliminare un problema o trovare rapidamente una soluzione. Le fonti di errori che possono essere eliminate tramite il refactoring sono tra le altre:
- metodi confusi o troppo lunghi: le catene o i blocchi di comandi sono così lunghi che terze persone non riescono a comprendere la logica del software.
- Duplicazioni del codice (ridondanze): un codice confuso presenta spesso delle ridondanze che, nel caso di manutenzioni, devono essere modificate in ciascun punto separatamente e quindi implicano perdite di tempo e costi notevoli.
- Elenchi di parametri eccessivamente lunghi: gli oggetti non vengono attribuiti direttamente a un metodo, bensì i loro attributi vengono trasmessi a un elenco di parametri.
- Classi con troppe funzioni: classi con troppe funzioni definite come metodi, denominate anche come God object, che rendono quasi impossibile un adeguamento del software.
- Classi con troppo poche funzioni: classi con così poche funzioni definite come metodi da essere inutili.
- Codici troppo generali con casi speciali: funzioni con eccezioni troppo specifiche, che sopraggiungono molto raramente o non si verificano affatto e che quindi complicano l’inserimento di estensioni necessarie.
- Middle man: una classe separata fa da intermediario tra metodi e classi diverse, invece che portare a richiami di metodi direttamente in una classe.
Come si procede nel refactoring?
Il refactoring dovrebbe sempre essere effettuato prima della modifica di una funzione di un programma. Il modo migliore per eseguirlo è procedere a piccolissimi step, testando le modifiche del codice tramite processi di sviluppo software come Test Driven Development (TDD) e Continuous Integration (CI). Per farla breve i TDD e i CI costituiscono i continui test di nuovi, piccoli segmenti di codice, che vengono creati e integrati dai programmatori e le cui funzionalità vengono testate tramite procedure spesso automatizzate.
Vale la seguente regola: modificare il programma internamente solo a piccoli step, senza influire sulla funzione esterna. Dopo ogni modifica andrebbe effettuato, per quanto possibile, un test di funzionamento automatizzato.
Quali sono le tecniche a disposizione?
Ci sono numerose concrete tecniche di refactoring. Una panoramica esaustiva si trova nell’opera più completa sul refactoring, ossia quella di Martin Fowler e Kent Beck: “Refactoring: Improving the Design of Existing Code”. Di seguito una breve sintesi:
Approccio rosso-verde
L’approccio rosso-verde è un metodo test driven (a sviluppo guidato) dello sviluppo software agile. Viene utilizzato quando si vuole integrare una nuova funzione in un codice già esistente. Il rosso rappresenta la prima serie di test prima dell’implementazione di una nuova funzione in un codice. Il verde invece sta per il segmento di codice più semplice possibile, necessario perché la funzione superi il test. Ne segue un’estensione con test costanti per eliminare codici che presentano errori e per aumentarne la funzionalità. L’approccio rosso-verde è uno dei fondamenti per il refactoring continuo nello sviluppo continuo del software.
Branching-by-Abstraction
Questo sistema di refactoring descrive un cambiamento graduale di un sistema e il passaggio delle posizioni di codice implementate precedentemente nei nuovi segmenti integrati. Il Branching-by-Abstraction viene solitamente impiegato quando si effettuano grossi cambiamenti che riguardano la gerarchia delle classi, l’ereditarietà e l'estrazione. Implementando un’astrazione, che resta collegata ad una vecchia implementazione, è possibile collegare altri metodi e classi con l’astrazione stessa e la funzionalità del vecchio segmento di codice può essere sostituita tramite l’astrazione.
Ciò spesso è realizzato con metodi pull-up o push-down. Questi collegano una nuova e migliore funzione con l’astrazione e trasmettono a quest’ultima i collegamenti. Così, una classe inferiore viene spostata verso l’alto (pull-up) o parti di una classe superiore passano ad una classe inferiore (push-down).
Le vecchie funzioni possono poi essere cancellate senza mettere a repentaglio l’intera funzionalità. Tramite queste modifiche capillari il sistema funziona senza che ci siano cambiamenti, mentre sostituite i codici unclean con quelli clean segmento per segmento.
Metodi di compilazione
Il refactoring dovrebbe rendere i metodi di un codice il più leggibili possibile. Già nella fase di lettura, nel migliore dei casi, la logica intrinseca di un metodo appare chiara anche ai programmatori esterni. Per una creazione di metodi efficiente ci sono diverse tecniche nel refactoring. Lo scopo di ogni modifica è quello di standardizzare i metodi, eliminare le duplicazioni e dividere i metodi troppo lunghi in segmenti separati che siano più accessibili a modifiche successive.
Tra queste tecniche vi sono per esempio:
- estrazione dei metodi
- posizionamento metodi inline
- eliminazione di variabili temporanee
- sostituzione di variabili temporanee per mezzo di metodi di richiesta
- introduzione di variabili descrittive
- separazione di variabili temporanee
- eliminazione di assegnazioni alle variabili dei parametri
- sostituzione di un metodo con un oggetto-metodo
- sostituzione di un algoritmo
Spostare le proprietà tra le classi
Per migliorare un codice, a volte è necessario spostare attributi o metodi tra le classi. A tal fine sono disponibili le seguenti tecniche:
- spostamento metodo
- spostamento attributo
- estrazione classe
- posizionamento classe inline
- nascondere delegato
- eliminazione classe al centro
- introduzione metodo esterno
- introduzione estensione locale
Organizzazione dati
Questo metodo ha come scopo quello di suddividere i dati in classi e mantenere queste ultime il più possibile chiare e comprensibili. I collegamenti non necessari tra classi, che possono danneggiare la funzionalità dei software alla più piccola modifica, vanno eliminati e suddivisi in classi coerenti.
Esempi di queste tecniche sono:
- incapsulamento dei propri accessi agli attributi
- sostituzione dei propri attributi con riferimenti ad oggetti
- sostituzione di valore con riferimento
- sostituzione di riferimento con valore
- accoppiamento dei dati osservabili
- incapsulamento di attributi
- sostituzione di record di dati con classi di dati
Semplificazione di espressioni condizionali
Sarebbe bene semplificare il più possibile le espressioni condizionali durante il refactoring. A tal fine sono disponibili le seguenti tecniche:
- scomposizione delle condizioni
- ricongiungimento di espressioni condizionali
- ricongiungimento di istruzioni ripetute in espressioni condizionali
- eliminazione di interruttori di controllo
- sostituzione di espressioni condizionali con guardie
- sostituzione della differenziazione con la polimorfia
- introduzione di oggetti nulli
Semplificazione delle chiamate dei metodi
Le chiamate dei metodi possono essere effettuate tra le altre cose attraverso i seguenti metodi in maniera più rapida e semplice:
- ridenominazione metodi
- inserimento parametri
- eliminazione parametri
- sostituzione parametri tramite metodi espliciti
- sostituzione di codici errore con eccezioni
Esempio di refactoring: ridenominazione metodi
Nel seguente esempio si nota che nel codice originale la denominazione del metodo non rende la sua funzionalità chiara e facile da capire. Il metodo dovrebbe trasmettere il codice di avviamento postale dell’indirizzo di un ufficio, ma non mostra questo compito direttamente nel codice. Per formulare in maniera più chiara il codice, si può ricorrere al code refactoring per rinominare il metodo.
Prima:
String getPostalCode() {
return (theOfficePostalCode+“/“+theOfficeNumber);
}
System.out.print(getPostalCode());
Dopo:
String getOfficePostalCode() {
return (theOfficePostalCode+“/“+theOfficeNumber);
}
System.out.print(getOfficePostalCode());
Refactoring: quali sono i vantaggi e gli svantaggi?
Vantaggi | Svantaggi |
---|---|
Una migliore comprensibilità facilita la manutenibilità e la possibilità di ampliare un software. | Un refactoring non preciso potrebbe implementare nuovi bug ed errori nel codice. |
La ristrutturazione del codice sorgente è possibile senza modificarne la funzionalità. | Non esiste una definizione chiara di “clean code”. |
Una miglior leggibilità aumenta la comprensibilità del codice per altri programmatori. | Un codice migliore spesso non è riconoscibile per il cliente perché la funzionalità resta la stessa, quindi i vantaggi non sono evidenti. |
L’eliminazione delle ridondanze e delle duplicazioni aumenta l’efficienza del codice. | In caso di team numerosi che lavorano al refactoring, lo sforzo di coordinamento potrebbe essere inaspettatamente elevato. |
I metodi coerenti impediscono che le modifiche apportate a livello locale possano influire su altre parti del codice. | |
Un codice pulito con metodi e classi più brevi e coerenti è caratterizzato da una maggior testabilità. |
In linea di massima, nel refactoring bisogna inserire nuove funzioni solamente quando il codice sorgente esistente resta inalterato. Vanno apportate modifiche al codice sorgente, ovvero si esegue un refactoring solo se non si inseriscono nuove funzioni.