I grandi sviluppatori sanno quando e come eseguire il refactoring del codice Rails

Pubblicato: 2022-03-11

Refactoring su larga scala: perché mai dovresti fare qualcosa del genere?

Se non è rotto, non aggiustarlo.

È una frase ben nota, ma come sappiamo, la maggior parte del progresso tecnologico umano è stato fatto da persone che hanno deciso di riparare ciò che non è rotto. Soprattutto nell'industria del software si potrebbe sostenere che la maggior parte di ciò che facciamo è riparare ciò che non è rotto.

Correzione delle funzionalità, miglioramento dell'interfaccia utente, miglioramento della velocità e dell'efficienza della memoria, aggiunta di funzionalità: queste sono tutte attività per le quali è facile vedere se vale la pena svolgere, e quindi noi sviluppatori esperti di Rails siamo favorevoli o contrari a dedicarci il nostro tempo. Tuttavia, esiste un'attività, che per la maggior parte rientra in un'area grigia: il refactoring standard e, in particolare, il refactoring del codice su larga scala.

Il termine refactoring su larga scala merita una spiegazione. Esattamente ciò che può essere considerato "su larga scala" varierà da caso a caso poiché il termine è un po' vago, ma ritengo che qualsiasi cosa che influisca in modo significativo su più di poche classi, o più di un solo sottosistema, e la sua interfaccia sia "grande .” D'altra parte, qualsiasi refactoring di Rails che rimane nascosto dietro l'interfaccia di una singola classe sarebbe sicuramente "piccolo". Naturalmente, ci sono molte aree grigie nel mezzo. Infine, fidati del tuo istinto, se hai paura di farlo, probabilmente è "grande".

Il refactoring, per definizione, non produce alcuna funzionalità visibile, niente che puoi mostrare al cliente, nessun risultato finale. Nella migliore delle ipotesi potrebbero produrre piccoli miglioramenti in termini di velocità e utilizzo della memoria, ma questo non è l'obiettivo principale. Si potrebbe dire che l'obiettivo principale è il codice di cui sei soddisfatto. Ma poiché stai riorganizzando il codice in modo tale da avere conseguenze di vasta portata su tutta la base di codice, c'è la possibilità che si scateni l'inferno e ci siano problemi. Questo è ovviamente da dove viene il terrore che abbiamo menzionato. Hai mai presentato qualcuno di nuovo alla tua base di codice e dopo che ti hanno chiesto informazioni su un pezzo di codice organizzato in modo particolare, hai risposto con qualcosa sulla falsariga di:

Sì, questo è un codice legacy che aveva senso all'epoca, ma le specifiche sono cambiate e ora è troppo costoso risolverlo?

Forse gli hai anche dato un'occhiata molto seria e gli hai detto di lasciar perdere e di non toccarlo.

Quando pianifichi come refactoring del codice Rails, potresti aver bisogno di un grafico complesso per iniziare.

La domanda: "Perché dovremmo anche volerlo fare?" è naturale e può essere importante quanto farlo...

La domanda: "Perché dovremmo anche volerlo fare?" è naturale e può essere importante quanto il processo di refactoring perché molto spesso ci sono altre persone che devi convincere per permetterti di dedicare il tuo tempo costoso al refactoring. Consideriamo quindi i casi in cui vorresti farlo e i vantaggi da ottenere:

Miglioramenti delle prestazioni

Sei soddisfatto dell'attuale organizzazione del tuo codice dal punto di manutenibilità, ma sta ancora causando problemi con le prestazioni. È semplicemente troppo difficile ottimizzare il modo in cui è attualmente configurato e le modifiche sarebbero molto fragili.

C'è solo una cosa da fare qui ed è profilarlo ampiamente. Esegui benchmark e fai stime su quanto guadagnerai e poi prova a stimare come si tradurrà in guadagni concreti. A volte potresti anche renderti conto che il refactoring del codice proposto non vale la pena. Altre volte avrai dati rigidi freddi per sostenere il tuo caso.

Miglioramenti architettonici

Forse l'architettura è ok ma un po' datata, o forse è così brutto che ti rabbrividire ogni volta che tocchi quella parte della base di codice. Funziona bene e velocemente, ma è una seccatura aggiungere nuove funzionalità. In quel dolore risiede il valore aziendale del refactoring. Il "dolore" significa anche che il processo di refactoring richiederà più tempo per aggiungere nuove funzionalità, forse molto più tempo.

E c'è un vantaggio da guadagnare. Fai stime del costo/beneficio per alcune funzionalità di esempio con e senza il grande refactoring proposto. Spiega che differenze simili si applicheranno alla maggior parte delle funzionalità imminenti che toccano quella parte del sistema ora e per sempre in futuro mentre il sistema è in fase di sviluppo. Le tue stime potrebbero essere errate poiché spesso lo sono nello sviluppo del software, ma i loro rapporti saranno probabilmente nel campo di battaglia.

Aggiornandolo

A volte il codice è inizialmente ben scritto. Ne sei estremamente felice. È veloce, efficiente in termini di memoria, manutenibile e ben allineato alle specifiche. Inizialmente. Ma poi le specifiche cambiano, gli obiettivi aziendali cambiano o impari qualcosa di nuovo sui tuoi utenti finali che invalida le tue ipotesi iniziali. Il codice funziona ancora bene e ne sei ancora abbastanza felice, ma qualcosa è semplicemente imbarazzante quando lo guardi nel contesto del prodotto finale. Le cose sono posizionate sul sottosistema leggermente sbagliato o le proprietà si trovano nella classe sbagliata, o forse alcuni nomi non hanno più senso. Ora stanno svolgendo un ruolo che in termini commerciali è chiamato in modo completamente diverso. Tuttavia, è ancora molto difficile giustificare qualsiasi tipo di refactoring Rails su larga scala poiché il lavoro coinvolto sarà in scala con qualsiasi altro esempio, ma i vantaggi sono molto meno tangibili. Quando ci pensi, non è nemmeno così difficile mantenerlo. Devi solo ricordare che alcune cose sono in realtà qualcos'altro. Devi solo ricordare che A in realtà significa B e la proprietà Y su A si riferisce effettivamente a C.

E qui sta il vero vantaggio. Nel campo della neuropsicologia ci sono molti esperimenti che suggeriscono che la nostra memoria a breve termine o di lavoro è in grado di contenere solo 7 +/- 2 elementi, uno dei quali è l'esperimento di Sternberg. Quando studiamo un argomento partiamo dagli elementi di base e, inizialmente, quando pensiamo a concetti di livello superiore dobbiamo pensare alle loro definizioni. Ad esempio, considera un semplice termine "password SHA256 salata". Inizialmente dobbiamo mantenere nella nostra memoria di lavoro le definizioni di "salato" e "SHA256" e forse anche una definizione di "funzione hash". Ma una volta che comprendiamo appieno il termine, occupa solo uno slot di memoria perché lo comprendiamo intuitivamente. Questo è uno dei motivi per cui abbiamo bisogno di comprendere appieno i concetti di livello inferiore per poter ragionare su quelli di livello superiore. Lo stesso vale per i termini e le definizioni specifiche del nostro progetto. Ma se dobbiamo ricordare la traduzione nel significato reale ogni volta che discutiamo del nostro codice, quella traduzione sta occupando un altro di quei preziosi slot di memoria di lavoro. Produce carico cognitivo e rende più difficile ragionare attraverso la logica nel nostro codice. A sua volta, se è più difficile ragionare significa che c'è una maggiore possibilità che trascuriamo un punto importante e introduciamo un bug.

E non dimentichiamo gli effetti collaterali più evidenti. C'è una buona possibilità di confusione quando si discutono le modifiche con il nostro cliente o chiunque abbia familiarità con i termini commerciali corretti. Le nuove persone che entrano a far parte del team devono acquisire familiarità sia con la terminologia aziendale che con le sue controparti nel codice.

Penso che queste ragioni siano molto convincenti e giustifichino il costo del refactoring in molti casi. Tuttavia, fai attenzione, potrebbero esserci molti casi limite in cui devi usare il tuo miglior giudizio per determinare quando e come eseguire il refactoring.

In definitiva, il refactoring su larga scala è positivo per le stesse ragioni per cui a molti di noi piace iniziare un nuovo progetto. Guardi quel file sorgente vuoto e un nuovo mondo coraggioso inizia a turbinare nella tua mente. Questa volta lo farai bene, il codice sarà elegante, sarà sia ben strutturato che veloce, robusto e facilmente estensibile e, soprattutto, sarà una gioia lavorare con ogni singolo giorno. Il refactoring, su piccola e larga scala, ti consente di riconquistare quella sensazione, dare nuova vita a una vecchia base di codice e ripagare quel debito tecnico.

Infine, è meglio se il refactoring è guidato da piani per rendere più semplice l'implementazione di una certa nuova funzionalità. In tal caso, il refactoring sarà più mirato e gran parte del tempo dedicato al refactoring verrà recuperato immediatamente grazie a una più rapida implementazione della funzionalità stessa.

Preparazione

Assicurati che la copertura del tuo test sia molto buona in tutte le aree della codebase che probabilmente toccherai. Se vedi alcune parti che non sono ben coperte, dedica prima un po' di tempo ad aumentare la copertura del test. Se non hai affatto dei test, dovresti prima dedicare del tempo a creare quei test. Se non riesci a creare una suite di test adeguata, concentrati sui test di accettazione e scrivi il maggior numero possibile e assicurati di scrivere i test unitari durante il refactoring. Teoricamente è possibile eseguire il refactoring del codice senza una buona copertura del test, ma sarà necessario eseguire molti test manuali e farlo spesso. Ci vorrà molto più tempo e sarà più soggetto a errori. In definitiva, se la copertura del test non è abbastanza buona, il costo dell'esecuzione di un refactoring Rails su larga scala potrebbe essere così alto che dovresti, purtroppo, considerare di non farlo affatto. A mio parere, questo è un vantaggio dei test automatizzati che non viene enfatizzato abbastanza spesso. I test automatizzati ti consentono di eseguire il refactoring spesso e, soprattutto, di essere più audace.

Dopo aver verificato che la copertura del test sia buona, è il momento di iniziare a mappare le modifiche. All'inizio non dovresti eseguire alcuna codifica. Devi mappare approssimativamente tutte le modifiche coinvolte e tracciare tutte le conseguenze attraverso la base di codice, oltre a caricare nella tua mente le conoscenze su tutto ciò. Il tuo obiettivo è capire esattamente perché stai cambiando qualcosa e il ruolo che svolge nella base di codice. Se inciampi cambiando le cose solo perché sembra che debbano essere cambiate o perché qualcosa si è rotto e questo sembra risolverlo, probabilmente finirai in un vicolo morto. Il nuovo codice sembra funzionare, ma in modo errato, e ora non riesci nemmeno a ricordare tutte le modifiche che hai apportato. A questo punto potrebbe essere necessario abbandonare il lavoro svolto sul refactoring del codice su larga scala e sostanzialmente hai perso tempo. Quindi prenditi il ​​tuo tempo ed esplora il codice per comprendere le ramificazioni di ogni modifica che stai per apportare. Alla fine pagherà profumatamente.

Avrai bisogno di un aiuto per il processo di refactoring. Potresti preferire qualcos'altro, ma mi piace un semplice pezzo di carta bianca e una penna. Comincio scrivendo la modifica iniziale che voglio apportare in alto a sinistra del foglio. Poi comincio a cercare tutti i luoghi interessati dalla modifica e li scrivo sotto la modifica iniziale. È importante qui usare il tuo giudizio. Alla fine le note e i diagrammi sulla carta sono lì per te, quindi scegli uno stile che si adatta meglio alla tua memoria. Scrivo frammenti di codice breve con punti elenco sotto di essi e molte frecce che portano ad altre note simili che indicano cose che dipendono da esso direttamente (freccia completa) o indirettamente (frecce tratteggiate). Annoto anche le frecce con segni di stenografia come promemoria per alcune cose specifiche che ho notato nella base di codice. Ricorda, tornerai a quelle note nei prossimi giorni solo mentre esegui le modifiche pianificate in esse ed è perfettamente corretto utilizzare promemoria molto brevi e criptici in modo che utilizzino meno spazio e siano più facili da impaginare sulla carta . Alcune volte stavo pulendo la mia scrivania mesi dopo un refactoring di Rails e ho trovato una di quelle carte. Era completamente senza senso, non avevo assolutamente idea di cosa volesse dire qualcosa su quel foglio, tranne che potrebbe essere stato scritto da qualcuno impazzito. Ma so che quel pezzo di carta era indispensabile mentre stavo lavorando al problema. Inoltre, non pensare di dover scrivere ogni singola modifica. Puoi raggrupparli e tenere traccia dei dettagli in un modo diverso. Ad esempio sul tuo documento principale puoi notare che devi "rinominare tutte le occorrenze di Ab in Cd" e quindi puoi tenere traccia delle specifiche in diversi modi. Puoi scriverli tutti su un foglio separato, puoi pianificare di eseguire una ricerca globale per tutte le occorrenze ancora una volta, oppure puoi semplicemente lasciare tutti i file di origine in cui è necessario apportare la modifica aperti nel tuo editor preferito e prendi nota mentale di ripercorrerli una volta che hai finito di mappare le modifiche.

Quando si delineano le conseguenze del proprio cambiamento iniziale, dato che è su larga scala, molto probabilmente si identificheranno ulteriori cambiamenti che hanno essi stessi ulteriori conseguenze. Ripetere l'analisi anche per loro, annotando tutte le modifiche dipendenti. A seconda della dimensione delle modifiche, puoi scriverle sullo stesso foglio o sceglierne uno nuovo vuoto. Una cosa molto importante da provare a fare durante la mappatura delle modifiche è cercare di identificare i limiti in cui è possibile effettivamente fermare le modifiche ramificate. Si desidera limitare il refactoring al più piccolo insieme sensibile, arrotondato, di modifiche. Se vedi un punto in cui puoi semplicemente fermarti e lasciare il resto così com'è, fallo anche se vedi che dovrebbe essere rifattorizzato, anche se è correlato concettualmente alle altre modifiche. Concludi questo giro di refactoring del codice, testa accuratamente, distribuisci e torna per saperne di più. Dovresti cercare attivamente quei punti per mantenere gestibili le dimensioni delle modifiche. Naturalmente, come sempre, fare una chiamata di giudizio. Abbastanza spesso sono arrivato a un punto in cui potevo interrompere il processo di refactoring aggiungendo alcune classi proxy per eseguire un po' di traduzione dell'interfaccia. Ho anche iniziato a implementarli quando mi sono reso conto che avrebbero lavorato tanto quanto spingere un po' più in là il refactoring fino al punto in cui ci sarebbe stato un punto di "arresto naturale" (cioè quasi nessun codice proxy necessario). Quindi sono tornato indietro, ripristinando le mie ultime modifiche e rifattorizzato. Se tutto suona un po' come tracciare un territorio inesplorato è perché mi sembra che lo sia, tranne per il fatto che le mappe del territorio sono solo bidimensionali.

Esecuzione

Una volta completata la preparazione per il refactoring, è il momento di eseguire il piano. Assicurati che la tua concentrazione sia elevata e assicurati un ambiente privo di distrazioni. A volte arrivo al punto di disattivare completamente la connessione a Internet a questo punto. Il fatto è che, se ti sei preparato bene, tieni una buona serie di appunti sul foglio accanto a te e la tua concentrazione è alta! Spesso puoi muoverti molto velocemente attraverso le modifiche in questo modo. In teoria, la maggior parte del lavoro veniva svolto in anticipo, durante la preparazione.

Una volta che stai effettivamente refactoring del codice, presta attenzione a strani bit di codice che fanno qualcosa di molto specifico e possono sembrare un codice errato. Forse sono codice errato, ma molto spesso stanno effettivamente gestendo uno strano caso d'angolo scoperto durante l'indagine su un bug in produzione. Nel corso del tempo, la maggior parte del codice Rails fa crescere "peli" o "verruche" che gestiscono strani bug di case d'angolo, ad esempio uno strano codice di risposta qui che potrebbe essere necessario per IE6 o una condizione lì che gestisce uno strano bug di temporizzazione. Non sono importanti per il quadro generale, ma sono comunque dettagli significativi. Idealmente, sono esplicitamente coperti da unit test, altrimenti prova a coprirli prima. Una volta mi è stato assegnato il compito di portare un'applicazione di medie dimensioni da Rails 2 a Rails 3. Conoscevo molto bene il codice, ma era un po' disordinato e c'erano molte modifiche da tenere in considerazione, quindi ho optato per la reimplementazione. In realtà, non è stata una vera reimplementazione, poiché non è quasi mai una mossa intelligente, ma ho iniziato con un'app Rails 3 vuota e ho rifattorizzato le sezioni verticali della vecchia app in quella nuova, utilizzando all'incirca il processo descritto. Ogni volta che terminavo una sezione verticale, esaminavo il vecchio codice Rails, esaminando ogni riga e verificando che avesse la sua controparte nel nuovo codice. In sostanza, stavo raccogliendo tutti i "peli" del vecchio codice e replicandoli nella nuova base di codice. Alla fine, la nuova base di codice ha affrontato tutti i casi d'angolo.

Assicurati di eseguire test manuali abbastanza spesso. Ti costringerà a cercare "interruzioni" naturali nel processo di refactoring che ti permetteranno di testare una parte del sistema e ti darà la certezza di non aver rotto nulla che non ti aspettavi di interrompere nel processo .

Avvolgetelo

Una volta terminato il refactoring del codice Rails, assicurati di rivedere tutte le modifiche un'ultima volta. Osserva l'intero differenziale e ripassalo. Molto spesso, noterai cose sottili che ti sono sfuggite all'inizio del refactoring perché non avevi le conoscenze che hai ora. È un bel vantaggio del refactoring su larga scala: ottieni un'immagine mentale più chiara dell'organizzazione del codice, soprattutto se non l'hai originariamente scritta.

Se possibile, chiedi anche a un collega di esaminarlo. Non deve nemmeno essere particolarmente familiare con quella parte esatta della base di codice, ma dovrebbe avere una familiarità generale con il progetto e il suo codice. Avere un nuovo sguardo sui cambiamenti può aiutare molto. Se non riesci assolutamente a convincere un altro sviluppatore a guardarli, dovrai fingere di esserlo tu stesso. Ottieni una buona notte di sonno e rivedilo con una mente fresca.

Se ti manca il controllo qualità, dovrai indossare anche quel cappello. Ancora una volta, fai una pausa e prendi le distanze dal codice, quindi torna per eseguire i test manuali. Hai appena subito l'equivalente di entrare in un armadio elettrico disordinato con un mucchio di strumenti e sistemare tutto, possibilmente tagliando e ricablando roba, quindi è necessario prestare un po' più di attenzione del solito.

Infine, goditi i frutti del tuo lavoro considerando tutte le modifiche pianificate che ora saranno molto più pulite e più facili da implementare.

Quando non lo faresti?

Sebbene ci siano molti vantaggi nell'eseguire regolarmente il refactoring su larga scala per mantenere il codice del progetto aggiornato e di alta qualità, è comunque un'operazione molto costosa. Ci sono anche casi in cui non sarebbe consigliabile:

La copertura del tuo test è scarsa

Come è stato detto: una copertura di test molto scarsa potrebbe essere un grosso problema. Usa il tuo giudizio, ma a breve termine potrebbe essere meglio concentrarsi sull'aumento della copertura mentre si lavora su nuove funzionalità ed eseguire il maggior numero possibile di refactoring su piccola scala localizzato. Questo ti aiuterà molto una volta deciso di fare il grande passo e ordinare parti più grandi della base di codice.

Il refactoring non è guidato da una nuova funzionalità e la base di codice non è cambiata da molto tempo

Ho usato il passato invece di dire "la base di codice non cambierà" apposta. A giudicare dall'esperienza (e per esperienza intendo sbagliare molte volte) non puoi quasi mai fare affidamento sulle tue previsioni su quando una certa parte della base di codice dovrà essere modificata. Quindi, fai la prossima cosa migliore: guarda al passato e supponi che il passato si ripeta. Se qualcosa non è stato modificato per molto tempo, probabilmente non è necessario cambiarlo ora. Aspetta che arrivi quel cambiamento e lavora su qualcos'altro.

Sei a corto di tempo

La manutenzione è la parte più costosa del ciclo di vita del progetto e il refactoring lo rende meno costoso. È assolutamente necessario che qualsiasi azienda utilizzi il refactoring per ridurre il debito tecnico per rendere più economica la manutenzione futura. Altrimenti rischia di entrare in un circolo vizioso in cui diventa sempre più costoso aggiungere nuove funzionalità. Spero che sia evidente il motivo per cui è negativo.

Detto questo, il refactoring su larga scala è molto, molto imprevedibile quando si tratta di quanto tempo ci vorrà e non dovresti farlo a metà. Se per qualsiasi motivo interno o esterno hai poco tempo e non sei sicuro di riuscire a finire entro quel lasso di tempo, potresti dover abbandonare il refactoring. La pressione e lo stress, in particolare il tipo indotto dal tempo, portano a un livello di concentrazione inferiore, che è assolutamente necessario per il refactoring su larga scala. Lavora per ottenere più "buy in" dal tuo team per mettere da parte il tempo e guardare nel tuo calendario per un periodo in cui ne avrai il tempo. Non è necessario che sia un arco di tempo continuo. Ovviamente avrai altri problemi da risolvere, ma quelle pause non dovrebbero durare più di un giorno o due. In tal caso, dovrai ricordare a te stesso il tuo piano perché inizierai a dimenticare ciò che hai appreso sulla base di codice e esattamente dove ti sei fermato.

Conclusione

Spero di averti fornito alcune linee guida utili e di averti convinto dei vantaggi, e oserei dire della necessità, di eseguire il refactoring su larga scala in determinate occasioni. L'argomento è molto vago e, naturalmente, nulla di quanto detto qui è una verità definita e i dettagli variano da progetto a progetto. Ho cercato di dare consigli, che secondo me sono generalmente applicabili, ma come sempre, considera il tuo caso particolare e usa la tua esperienza per adattarti alle sue sfide specifiche. Buona fortuna refactoring!