Pertanto, per alcuni è incomprensibile che rifattorizzazione è un'area della programmazione ed è anche una parte molto importante del lavoro del programmatore. Il codice è in continua evoluzione, verrà modificato finché ci sarà la possibilità di aggiungere nuove funzionalità. Tuttavia, potrebbe assumere una forma che non consente più di aggiungere efficacemente nuove funzionalità e sarebbe più semplice riscrivere l'intero programma.
Che cos'è il refactoring?
Di solito, la risposta che si sente è che si tratta di cambiare la struttura del codice applicando una serie di trasformazioni di refactoring senza influenzare il comportamento osservabile del codice. Questo è vero. Recentemente, mi sono imbattuto in una definizione di Martin Fowler nel suo libro "Migliorare la progettazione del codice esistente dove descrive rifattorizzazione come "fare grandi cambiamenti a piccoli passi". Descrive rifattorizzazione come una modifica del codice che non influisce sul suo funzionamento, ma sottolinea che deve essere fatto a piccoli passi.
Il libro sostiene inoltre che rifattorizzazione non influisce sul funzionamento del codice e sottolinea che non ha alcun effetto sul superamento dei test in qualsiasi momento. Descrive passo dopo passo come eseguire in modo sicuro rifattorizzazione. Mi è piaciuto il suo libro perché descrive semplici trucchi che possono essere utilizzati nel lavoro di tutti i giorni.
Perché abbiamo bisogno di refactoring?
Il più delle volte è necessario quando si vuole introdurre una nuova funzionalità e il codice nella sua versione attuale non lo consente o sarebbe più difficile senza modifiche al codice. Inoltre, è utile nei casi in cui l'aggiunta di ulteriori funzionalità non è conveniente in termini di tempo, cioè sarebbe più veloce riscrivere il codice da zero. A volte si dimentica che rifattorizzazione può rendere il codice più pulito e leggibile. Martin scrive nel suo libro come esegue il refactoring quando avverte odori sgradevoli nel codice e, come dice lui stesso, "lascia sempre spazio al meglio". E qui mi ha sorpreso vedendo il refactoring come un elemento del lavoro quotidiano sul codice. Per me il codice è spesso incomprensibile, leggerlo è un po' un'esperienza perché il codice è spesso poco intuitivo.
La caratteristica distintiva di un programma ben progettato è la sua modularitàgrazie alla quale è sufficiente conoscere solo una piccola parte del codice per introdurre la maggior parte delle modifiche. La modularità facilita anche l'inserimento di nuove persone e l'avvio di un lavoro più efficiente. Per ottenere questa modularità, gli elementi del programma correlati devono essere raggruppati insieme e i collegamenti devono essere comprensibili e facili da trovare. Non esiste un'unica regola empirica che indichi come farlo. Man mano che si conosce e si capisce meglio come dovrebbe funzionare il codice, si possono raggruppare gli elementi, ma a volte è necessario anche testare e controllare.
Una delle regole della rifattorizzazione in YAGNIè l'acronimo di "You Aren't Gonna Need It" (non ne avrai bisogno) e deriva da Programmazione eXtreme (XP) utilizzato principalmente in Agile sviluppo software squadre. Per farla breve, YAGNI dice che si dovrebbero fare solo cose aggiornate. Ciò significa che anche se qualcosa potrebbe essere necessario in futuro, non dovrebbe essere fatto adesso. Ma non possiamo nemmeno bloccare ulteriori estensioni e qui la modularità diventa importante.
Quando si parla di rifattorizzazioneÈ necessario menzionare uno degli elementi più essenziali, ovvero i test. In rifattorizzazionedobbiamo sapere che il codice funziona ancora, perché rifattorizzazione non cambia il funzionamento, ma la struttura, quindi tutti i test devono essere superati. È meglio eseguire i test per la parte di codice su cui stiamo lavorando dopo ogni piccola trasformazione. Questo ci dà la conferma che tutto funziona come dovrebbe e accorcia i tempi dell'intera operazione. Questo è ciò di cui parla Martin nel suo libro: eseguire i test il più spesso possibile per non fare un passo indietro e perdere tempo a cercare una trasformazione che ha rotto qualcosa.
Rifattorizzazione del codice senza test è una rottura e c'è una grande possibilità che qualcosa vada storto. Se è possibile, sarebbe meglio aggiungere almeno alcuni test di base che ci rassicurino sul funzionamento del codice.
Le trasformazioni elencate di seguito sono solo esempi, ma sono davvero utili nella programmazione quotidiana:
- Estrazione di funzioni e variabili: se la funzione è troppo lunga, verificare se ci sono funzioni minori che possono essere estratte. Lo stesso vale per le linee lunghe. Queste trasformazioni possono aiutare a trovare duplicazioni nel codice. Grazie a Small Functions, il codice diventa più chiaro e comprensibile,
- Rinominare funzioni e variabili: l'uso di una corretta convenzione di denominazione è essenziale per una buona programmazione. I nomi delle variabili, se ben scelti, possono dire molto sul codice,
- Raggruppamento delle funzioni in una classe: questa modifica è utile quando due classi eseguono operazioni simili, in quanto può ridurre la lunghezza della classe,
- Sovrascrivere la dichiarazione annidata - se la condizione è verificata per un caso speciale, emettere un'istruzione di ritorno quando si verifica la condizione. Questo tipo di test viene spesso chiamato clausola di guardia. La sostituzione di una frase condizionale annidata con una frase di uscita cambia l'enfasi del codice. Il costrutto if-else assegna lo stesso peso a entrambe le varianti. Per chi legge il codice, è un messaggio che indica che ognuna di esse è ugualmente probabile e importante,
- Introdurre un caso speciale: se alcune condizioni vengono utilizzate più volte nel codice, può valere la pena di creare una struttura separata per esse. Di conseguenza, la maggior parte dei controlli dei casi speciali può essere sostituita da semplici chiamate di funzione. Spesso il valore comune che richiede un'elaborazione speciale è null. Per questo motivo, questo schema viene spesso chiamato "oggetto zero". Tuttavia, questo approccio può essere utilizzato in qualsiasi caso speciale,
- Sostituzione del polimorfismo dell'istruzione condizionale.
Esempio
Questo è un articolo su rifattorizzazione ed è necessario un esempio. Qui di seguito voglio mostrare un semplice esempio di rifattorizzazione con l'uso di Sovrascrittura della dichiarazione annidata e Sostituzione del polimorfismo dell'istruzione condizionale. Supponiamo di avere una funzione del programma che restituisce un hash con informazioni su come innaffiare le piante nella vita reale. Tali informazioni probabilmente si troverebbero nel modello, ma per questo esempio le abbiamo nella funzione.
def irrigazione_info(impianto)
risultato = {}
if pianta.is_a? Suculent || plant.is_a? Cactus
result = { water_amount: "Un po'" , come_fare: "Dal fondo", durata_innaffiatura: "2 settimane" }
elsif plant.is_a? Alocasia || plant.is_a? Maranta
result = { water_amount: "Grande quantità", come_fare: "Come preferisci", durata_innaffiatura: "5 giorni" }
elsif plant.is_a? Peperomia
risultato = { quantità_acqua: "Quantità decente",
come_fare: "Dal basso! Non amano l'acqua sulle foglie",
durata_innaffiatura: "1 settimana" }
altrimenti
risultato = { quantità_acqua: "Quantità d'acqua",
come_fare: "Come preferisci",
durata_innaffiatura: "1 settimana"
}
fine
restituire il risultato
fine
L'idea è di cambiare se tornare:
se plant.isa? Suculent || plant.isa? Cactus
result = { wateramount: "Un po'" , howto: "Dal basso",
A
return { water_amount: "Un po'" , how_to: "Dal fondo", durata_innaffiatura: "2 settimane" } if plant.is_a? Suculent || plant.is_a? Cactus
restituire { acquaquantità: "Un po'", comea: "Dal basso", irrigazionedurata: "2 settimane" } if plant.isa? Suculent || plant.is_a? Cactus
E così via, fino ad arrivare a una funzione che assomiglia a questa:
def irrigazione_info(impianto)
return result = { wateramount: "Un po'" , howto: "Dal fondo", wateringduration: "2 settimane" } if plant.isa? Suculent || plant.is_a? Cactus
return result = { wateramount: "Grande quantità", modalità: "Come preferisci", durata dell'irrigazione: "5 giorni" } if plant.isa? Alocasia || plant.is_a? Maranta
return result = { quantità_acqua: "Quantità d'acqua",
howto: "Dal basso! Non amano l'acqua sulle foglie",
durata dell'irrigazione: "1 settimana" } if plant.is_a? Peperomia
return result = { quantità_acqua: "Quantità dicente",
come_fare: "Come preferisci",
durata_innaffiatura: "1 settimana"
}
fine
Alla fine, avevamo già un risultato di ritorno. Una buona abitudine è quella di procedere passo dopo passo e testare ogni modifica. Si potrebbe sostituire questo blocco if con un caso switch e il risultato sarebbe immediatamente migliore, senza dover controllare ogni volta tutti gli if. Sarebbe così:
def irrigazione_info(impianto)
swich plant.class.to_string
caso Suculent, Cactus
{acqua: "Un po'" , howto: "Dal fondo", durata_innaffiatura: "2 settimane" }
caso Alocasia, Maranta
{acqua: "Grande quantità", modalità: "Come preferisci", durata_innaffiatura: "5 giorni" }
caso Peperomia
{acqua_ammontare: "Quantità dicente",
come_fare: "Dal basso! Non amano l'acqua sulle foglie",
durata_innaffiatura: "1 settimana" }
altrimenti
{acqua_ammontare: "Quantità dicente",
come_fare: "Come preferisci",
durata_innaffiatura: "1 settimana" }
fine
fine
E poi si può applicare il metodo Sostituzione del polimorfismo delle istruzioni condizionali. Si tratta di creare una classe con una funzione che restituisca il valore corretto e che li collochi al posto giusto.
classe Suculent
...
def irrigazione_info()
return { wateramount: "Un po'" , howto: "Dal fondo", watering_duration: "2 settimane" }
end
fine
classe Cactus
...
def irrigazione_info()
return { wateramount: "Un po'" , howto: "Dal fondo", watering_duration: "2 settimane" }
end
fine
classe Alocasia
...
def irrigazione_info
return {acqua: "Grande quantità", howto: "Come preferisci", watering_duration: "5 giorni" }
fine
fine
classe Maranta
...
def irrigazione_info
return {acqua: "Grande quantità", howto: "Come preferisci", watering_duration: "5 giorni" }
fine
fine
classe Peperomia
...
def irrigazione_info
return { importo_acqua: "Quantità di acqua",
come_fare: "Dal basso! Non amano l'acqua sulle foglie",
durata_innaffiatura: "1 settimana" }
fine
fine
classe Impianto
...
def irrigazione_info
return { importo_acqua: "Quantità di acqua",
come_fare: "Come preferisci",
durata_innaffiatura: "1 settimana" }
fine
fine
E nella main watering_infofunction, il codice sarà simile a questo:
def irrigazione_info(impianto)
plant.map(&:watering_info)
fine
Naturalmente, questa funzione può essere rimossa e sostituita con il suo contenuto. Con questo esempio, ho voluto presentare la funzione generale modello di rifattorizzazione.
Sintesi
Rifattorizzazione è un argomento importante. Spero che questo articolo sia stato un incentivo a leggere di più. Questi competenze di refactoring vi aiuterà a individuare i bug e a migliorare il vostro laboratorio di codice pulito. Vi consiglio di leggere il libro di Martin (Improving the Design of Existing Code), che è un insieme di regole abbastanza basilari e utili di rifattorizzazione. L'autore mostra le varie trasformazioni passo dopo passo con una spiegazione completa e una motivazione e consigli su come evitare errori in rifattorizzazione. Grazie alla sua versatilità, è un libro delizioso per frontend e sviluppatori backend.
Per saperne di più
GraphQL Ruby. E le prestazioni?