Refactoring Legacy Code Part 1 - The Golden Master

Vecchio codice Codice brutto. Codice complicato spaghetti code. Assurdità senza senso. In due parole, Codice legacy. Questa è una serie che ti aiuterà a lavorare e ad affrontarla.

In un mondo ideale, si scriverebbe solo un nuovo codice. Lo scriverei bello e perfetto. Non avresti mai bisogno di rivisitare il tuo codice e non dovrai mai mantenere i progetti di dieci anni. In un mondo ideale ...

Sfortunatamente, viviamo in una realtà che non è l'ideale. Dobbiamo capire, modificare e migliorare il codice vecchio di secoli. Dobbiamo lavorare con il codice legacy. Allora, cosa stai aspettando? Prendiamo le nostre teste in questo primo tutorial, prendiamo il codice, lo capiamo un po 'e creiamo una rete di sicurezza per le nostre future modifiche.

Definizione del codice legacy

Il codice legacy è stato definito in così tanti modi che è impossibile trovare una singola definizione comunemente accettata. I pochi esempi all'inizio di questo tutorial sono solo la punta dell'iceberg. Quindi non ti darò alcuna definizione ufficiale. Invece, ti citerò il mio preferito.

Per me, codice legacy è semplicemente codice senza test. ~ Michael Feathers

Bene, questa è la prima definizione formale dell'espressione codice legacy, pubblicato da Michael Feathers nel suo libro Working Effectively with Legacy Code. Naturalmente, l'industria ha usato l'espressione per anni, fondamentalmente per qualsiasi codice che è difficile da modificare. Tuttavia questa definizione ha qualcosa di diverso da dire. Spiega il problema molto chiaramente, in modo che la soluzione diventi ovvia. "Difficile cambiare" è così vago. Cosa dovremmo fare per rendere più semplice il cambiamento? Non ne abbiamo idea! "Codice senza prove" d'altra parte è molto concreto. E la risposta alla nostra domanda precedente è semplice, rende il codice testabile e testalo. Quindi iniziamo.

Ottenere il nostro codice legacy

Questa serie si baserà sull'eccezionale Trivia Game di J.B. Rainsberger progettato per gli eventi Legacy Code Retreat. È fatto per essere come un vero codice legacy e per offrire anche opportunità per una vasta gamma di refactoring, con un discreto livello di difficoltà.

Controlla il codice sorgente

The Trivia Game è ospitato su GitHub ed è concesso in licenza GPLv3, quindi puoi giocarci liberamente. Inizieremo questa serie controllando il repository ufficiale. Il codice è anche allegato a questo tutorial con tutte le modifiche che apporteremo, quindi se ti confondi ad un certo punto, puoi dare una sbirciatina al risultato finale.

 $ git clone https://github.com/jbrains/trivia.git Clonazione in 'trivia' ... remote: conteggio oggetti: 429, fatto. remote: comprimere gli oggetti: 100% (262/262), fatto. remote: totale 429 (delta 100), riutilizzato 419 (delta 93) Oggetti di ricezione: 100% (429/429), 848,33 KiB | 305,00 KiB / s, fatto. Delta di risoluzione: 100% (100/100), fatto. Controllo della connettività ... fatto.

Quando apri il banalità directory vedrete il nostro codice in diversi linguaggi di programmazione. Lavoreremo in PHP, ma sei libero di scegliere il tuo preferito e applicare le tecniche qui presentate.

Capire il codice

Per definizione, il codice legacy è difficile da capire, specialmente se non sappiamo nemmeno cosa dovrebbe fare. Quindi il primo passo è eseguire il codice e fare una sorta di ragionamento, di cosa si tratta.

Abbiamo due file nella nostra directory.

$ cd php / $ ls -al totale 20 drwxr-xr-x 2 csaba csaba 4096 Mar 10 21:05. drwxr-xr-x 26 csaba csaba 4096 mar 10 21: 05 ... -rw-r - r-- 1 csaba csaba 5568 mar 10 21:05 Game.php -rw-r - r-- 1 csaba csaba 410 mar 10 21:05 GameRunner.php

GameRunner.php sembra essere un buon candidato per il nostro tentativo di eseguire il codice.

$ php ./GameRunner.php Chet è stato aggiunto Sono il giocatore numero 1 Pat è stato aggiunto Sono il giocatore numero 2 È stata aggiunta la Sue Sono il giocatore numero 3 Chet è il giocatore attuale Hanno fatto un giro 4 La nuova posizione di Chet è 4 La categoria è Pop Domanda pop 0 La risposta era corretta !!!! Chet ora ha 1 monete d'oro. Pat è il giocatore attuale Hanno rotolato una nuova posizione di 2 Pat è 2 La categoria è Sport Sport Domanda 0 La risposta era corretta !!!! Pat ora ha 1 monete d'oro. Sue è il giocatore attuale Hanno fatto rotolare 1 La nuova posizione di 1 Sue è 1 La categoria è Science Science Question 0 La risposta è stata corretta !!!! Sue ora ha 1 monete d'oro. Chet è il giocatore attuale Hanno tirato un 4 ## Alcune linee rimosse per mantenere ## il tutorial ad una dimensione ragionevole La risposta era corretta !!!! Sue ora ha 5 monete d'oro. Chet è il giocatore attuale Hanno tirato un 3 Chet sta uscendo dalla casella di rigore La nuova posizione di Chet è 11 La categoria è Rock Rock Domanda 5 La risposta era corretta !!!! Chet ora ha 5 monete d'oro. Pat è il giocatore attuale Hanno tirato un 1 La nuova posizione di Pat è 10 La categoria è Sport Sport Domanda 1 La risposta era corretta !!!! Pat ora ha 6 monete d'oro.

OK. La nostra ipotesi era corretta. Il nostro codice funzionava e produceva un po 'di output. L'analisi di questo output ci aiuterà a dedurre alcune idee di base su cosa fa il codice.

  1. Sappiamo che è un gioco a quiz. Lo sapevamo quando abbiamo controllato il codice sorgente.
  2. Il nostro esempio ha tre giocatori: Chet, Pat e Sue.
  3. C'è una sorta di lancio di un dado o di un concetto simile.
  4. C'è una posizione corrente per un giocatore. Forse su una specie di tavola?
  5. Esistono varie categorie da cui vengono poste le domande.
  6. Gli utenti rispondono alle domande.
  7. Le risposte corrette danno oro ai giocatori.
  8. Le risposte sbagliate inviano i giocatori nella cassetta delle penalità.
  9. I giocatori possono uscire da una casella di rigore, in base a una logica non abbastanza chiara.
  10. Sembra che l'utente che per primo raggiunge sei monete d'oro vince.

Questa è una grande conoscenza. Potremmo capire la maggior parte del comportamento di base dell'applicazione semplicemente guardando l'output. Nelle applicazioni reali, l'output potrebbe non essere testo sullo schermo, ma può essere una pagina Web, un registro errori, un database, una comunicazione di rete, un file di dettagli e così via. In altri casi, il modulo che è necessario modificare non può essere eseguito isolato. Se è così, dovrai eseguirlo attraverso altri moduli dell'applicazione più grande. Prova ad aggiungere il minimo, per ottenere un risultato ragionevole dal tuo codice legacy.

Scansione del codice

Ora che abbiamo un'idea su cosa emette il codice, possiamo iniziare a guardarlo. Inizieremo con il corridore.

The Game Runner

Mi piace iniziare con l'esecuzione di tutto il codice attraverso il formattatore del mio IDE. Ciò migliora notevolmente la leggibilità rendendo la forma del codice familiare con quello a cui sono abituato. Così questo:

... diventerà questo:

... che è un po 'meglio. Potrebbe non essere un'enorme differenza con questa piccola quantità di codice, ma sarà sul nostro prossimo file.

Guardando il nostro GameRunner.php file, possiamo facilmente identificare alcuni aspetti chiave che abbiamo osservato nell'output. Possiamo vedere le linee che aggiungono gli utenti (9-11), che viene chiamato un metodo roll () e viene selezionato un vincitore. Naturalmente, questi sono lontani dai segreti interiori della logica del gioco, ma almeno potremmo iniziare identificando i metodi chiave che ci aiuteranno a scoprire il resto del codice.

Il file di gioco

Dovremmo fare la stessa formattazione sul Game.php file anche.

Questo file è molto più grande; Circa 200 righe di codice. La maggior parte dei metodi ha dimensioni appropriate, ma alcuni sono piuttosto grandi e dopo la formattazione, possiamo vedere che in due punti l'indentazione del codice supera i quattro livelli. Alti livelli di rientro di solito significano molte decisioni complesse, quindi per ora, possiamo assumere che quei punti nel nostro codice saranno più complessi e più sensibili al cambiamento.

Il maestro d'oro

E il pensiero del cambiamento ci porta alla mancanza di test. I metodi che abbiamo visto Game.php sono abbastanza complessi Non preoccuparti se non li capisci. A questo punto, sono anche un mistero per me. Il codice legacy è un mistero che dobbiamo risolvere e capire. Abbiamo fatto il nostro primo passo per capirlo ed è giunto il momento per il nostro secondo.

Allora, qual è questo maestro d'oro?

Quando si lavora con codice legacy, è quasi impossibile capirlo e scrivere codice che sicuramente eserciterà tutti i percorsi logici attraverso il codice. Per quel tipo di test, avremmo bisogno di capire il codice, ma non lo siamo ancora. Quindi dobbiamo prendere un altro approccio.

Invece di cercare di capire cosa testare, possiamo testare tutto, un sacco di volte, in modo da ottenere una quantità enorme di output, di cui possiamo quasi certamente presumere che sia stato prodotto esercitando tutte le parti della nostra eredità codice. Si consiglia di eseguire il codice almeno 10.000 (diecimila) volte. Scriveremo un test per eseguirlo il doppio e salvare l'output.

Scrivere il Golden Master Generator

Possiamo pensare al futuro e iniziare creando un generatore e un test come file separati per i test futuri, ma è davvero necessario? Non lo sappiamo ancora per certo. Quindi, perché non iniziare con un file di test di base che eseguirà il nostro codice una volta e costruirà la nostra logica da lì.

Troverete nell'archivio dei codici allegato, all'interno di fonte cartella ma al di fuori del banalità cartella nostra Test cartella. In questa cartella, creiamo un file: GoldenMasterTest.php.

class GoldenMasterTest estende PHPUnit_Framework_TestCase function testGenerateOutput () ob_start (); require_once __DIR__. '/ ... /trivia/php/GameRunner.php'; $ output = ob_get_contents (); ob_end_clean (); var_dump ($ output); 

Potremmo farlo in molti modi. Potremmo, ad esempio, eseguire il nostro codice dalla console e reindirizzare il suo output su un file. Tuttavia, averlo in un test che viene eseguito facilmente all'interno del nostro IDE è un vantaggio che non dovremmo ignorare.

Il codice è abbastanza semplice, bufferizza l'output e lo inserisce nel file $ uscita variabile. Il require_once () eseguirà anche tutto il codice all'interno del file incluso. Nel nostro var dump vedremo un output già familiare.

Tuttavia, in una seconda sessione, possiamo osservare qualcosa di strano:

... le uscite differiscono. Anche se abbiamo eseguito lo stesso codice, l'output è diverso. I numeri tirati sono diversi, le posizioni dei giocatori sono diverse.

Semina il generatore casuale

fai $ aGame-> roll (rand (0, 5) + 1); if (rand (0, 9) == 7) $ notAWinner = $ aGame-> wrongAnswer ();  else $ notAWinner = $ aGame-> wasCorrectlyAnswered ();  while ($ notAWinner);

Analizzando il codice essenziale del corridore, possiamo vedere che utilizza una funzione rand () generare numeri casuali. La nostra prossima tappa è la documentazione ufficiale di PHP per la ricerca di questo rand () funzione.

Il generatore di numeri casuali viene seminato automaticamente.

La documentazione ci dice che la semina avviene automaticamente. Ora abbiamo un altro compito. Dobbiamo trovare un modo per controllare il seme. Il srand () la funzione può aiutare con quello. Ecco la sua definizione dalla documentazione.

Semina il generatore di numeri casuali con seme o con un valore casuale se non viene dato seme.

Ci dice che se lo facciamo prima di ogni chiamata a rand (), dovremmo sempre finire con gli stessi risultati.

function testGenerateOutput () ob_start (); srand (1); require_once __DIR__. '/ ... /trivia/php/GameRunner.php'; $ output = ob_get_contents (); ob_end_clean (); var_dump ($ output); 

Abbiamo messo srand (1) prima di noi require_once (). Ora l'output è sempre lo stesso.

Metti l'output in un file

class GoldenMasterTest estende PHPUnit_Framework_TestCase function testGenerateOutput () file_put_contents ('/ tmp / gm.txt', $ this-> generateOutput ()); $ file_content = file_get_contents ('/ tmp / gm.txt'); $ this-> assertEquals ($ file_content, $ this-> generateOutput ());  funzione privata generateOutput () ob_start (); srand (1); require_once __DIR__. '/ ... /trivia/php/GameRunner.php'; $ output = ob_get_contents (); ob_end_clean (); restituire $ output; 

Questo cambiamento sembra ragionevole. Destra? Abbiamo estratto la generazione del codice in un metodo, l'abbiamo eseguito due volte e ci aspettavamo che l'output fosse uguale. Comunque non lo saranno.

La ragione è questa require_once () non richiederà lo stesso file due volte. La seconda chiamata al generateOutput () il metodo produrrà una stringa vuota. Quindi, cosa potremmo fare? Cosa succede se semplicemente richiedere()? Questo dovrebbe essere eseguito ogni volta.

Bene, questo porta ad un altro problema: "Impossibile redeclare echoln ()". Ma da dove viene? È proprio all'inizio del Game.php file. Il motivo per cui si sta verificando questo errore è perché in GameRunner.php noi abbiamo include __DIR__. '/Game.php';, che cerca di includere il file di gioco due volte, ogni volta che chiamiamo il generateOutput () metodo.

include_once __DIR__. '/Game.php';

utilizzando include_once nel GameRunner.php risolverà il nostro problema. Sì, dovevamo modificare GameRunner.php senza avere prove per questo, ancora! Tuttavia, possiamo essere sicuri al 99% che il nostro cambiamento non infrangerà il codice stesso. È un cambiamento abbastanza piccolo e semplice da non spaventarci molto. E, soprattutto, fa passare i test.

Eseguire più volte

Ora che abbiamo il codice che possiamo eseguire più volte, è tempo di generare un po 'di output.

function testGenerateOutput () $ this-> generateMany (20, '/tmp/gm.txt'); $ this-> generateMany (20, '/tmp/gm2.txt'); $ file_content_gm = file_get_contents ('/ tmp / gm.txt'); $ file_content_gm2 = file_get_contents ('/ tmp / gm2.txt'); $ this-> assertEquals ($ file_content_gm, $ file_content_gm2);  funzione privata generateMany ($ times, $ fileName) $ first = true; while ($ times) if ($ first) file_put_contents ($ fileName, $ this-> generateOutput ()); $ prima = falso;  else file_put_contents ($ fileName, $ this-> generateOutput (), FILE_APPEND);  $ times--; 

Abbiamo estratto un altro metodo qui: generateMany (). Ha due parametri. Uno per il numero di volte che vogliamo eseguire il nostro generatore, l'altro è un file di destinazione. Metterà l'output generato nei file. Alla prima esecuzione svuota i file, e per il resto delle iterazioni, aggiunge i dati. Puoi guardare nel file per vedere l'output generato 20 volte.

Ma aspetta! Lo stesso giocatore vince ogni volta? È possibile?

cat /tmp/gm.txt | grep "ha 6 monete d'oro." Chet ora ha 6 monete d'oro. Chet ora ha 6 monete d'oro. Chet ora ha 6 monete d'oro. Chet ora ha 6 monete d'oro. Chet ora ha 6 monete d'oro. Chet ora ha 6 monete d'oro. Chet ora ha 6 monete d'oro. Chet ora ha 6 monete d'oro. Chet ora ha 6 monete d'oro. Chet ora ha 6 monete d'oro. Chet ora ha 6 monete d'oro. Chet ora ha 6 monete d'oro. Chet ora ha 6 monete d'oro. Chet ora ha 6 monete d'oro. Chet ora ha 6 monete d'oro. Chet ora ha 6 monete d'oro. Chet ora ha 6 monete d'oro. Chet ora ha 6 monete d'oro. Chet ora ha 6 monete d'oro. Chet ora ha 6 monete d'oro.

Sì! È possibile! È più che possibile È una cosa sicura Abbiamo lo stesso seme per la nostra funzione casuale. Giochiamo sempre allo stesso gioco.

Eseguirlo diversamente ogni volta

Dobbiamo giocare a giochi diversi, altrimenti è quasi certo che solo una piccola parte del nostro codice legacy viene effettivamente esercitata più e più volte. Lo scopo del maestro d'oro è di esercitare il più possibile. Abbiamo bisogno di ri-seminare il generatore casuale ogni volta, ma in modo controllato. Un'opzione è usare il nostro contatore come valore di seme.

funzione privata generateMany ($ times, $ fileName) $ first = true; while ($ times) if ($ first) file_put_contents ($ fileName, $ this-> generateOutput ($ times)); $ prima = falso;  else file_put_contents ($ fileName, $ this-> generateOutput ($ times), FILE_APPEND);  $ times--;  funzione privata generateOutput ($ seed) ob_start (); srand ($ seme); richiede __DIR__. '/ ... /trivia/php/GameRunner.php'; $ output = ob_get_contents (); ob_end_clean (); restituire $ output; 

Questo continua a far passare il test, quindi siamo sicuri di generare lo stesso output completo ogni volta, mentre l'output gioca un gioco diverso per ogni iterazione.

cat /tmp/gm.txt | grep "ha 6 monete d'oro." Sue ora ha 6 monete d'oro. Chet ora ha 6 monete d'oro. Chet ora ha 6 monete d'oro. Chet ora ha 6 monete d'oro. Chet ora ha 6 monete d'oro. Pat ora ha 6 monete d'oro. Pat ora ha 6 monete d'oro. Chet ora ha 6 monete d'oro. Chet ora ha 6 monete d'oro. Sue ora ha 6 monete d'oro. Chet ora ha 6 monete d'oro. Chet ora ha 6 monete d'oro. Sue ora ha 6 monete d'oro. Chet ora ha 6 monete d'oro. Sue ora ha 6 monete d'oro. Chet ora ha 6 monete d'oro. Chet ora ha 6 monete d'oro. Pat ora ha 6 monete d'oro. Chet ora ha 6 monete d'oro. Chet ora ha 6 monete d'oro.

Ci sono vari vincitori per il gioco in modo casuale. Questo sembra buono.

Arrivare a 20.000

La prima cosa che puoi provare è eseguire il nostro codice per 20.000 iterazioni di gioco.

function testGenerateOutput () $ times = 20000; $ this-> generateMany ($ times, '/tmp/gm.txt'); $ this-> generateMany ($ times, '/tmp/gm2.txt'); $ file_content_gm = file_get_contents ('/ tmp / gm.txt'); $ file_content_gm2 = file_get_contents ('/ tmp / gm2.txt'); $ this-> assertEquals ($ file_content_gm, $ file_content_gm2); 

Questo funzionerà quasi. Verranno generati due file da 55 MB.

ls -alh / tmp / gm * -rw-r - r-- 1 csaba csaba 55M 14 Mar 14 20:38 /tmp/gm2.txt -rw-r - r-- 1 csaba csaba 55M 14 Mar 20:38 /tmp/gm.txt

D'altra parte, il test fallirà con un errore di memoria insufficiente. Non importa quanta RAM hai, questo fallirà. Ho 8 GB più uno scambio da 4 GB e fallisce. Le due stringhe sono semplicemente troppo grandi per essere confrontate nella nostra affermazione.

In altre parole, generiamo buoni file, ma PHPUnit non può confrontarli. Abbiamo bisogno di un work-around.

$ this-> assertFileEquals ('/ tmp / gm.txt', '/tmp/gm2.txt');

Sembra essere un buon candidato, ma fallisce ancora. Che peccato. Abbiamo bisogno di ricercare ulteriormente la situazione.

$ this-> assertTrue ($ file_content_gm == $ file_content_gm2);

Questo tuttavia sta funzionando.

Può confrontare le due stringhe e fallire se sono diverse. Ha tuttavia un piccolo prezzo. Non sarà in grado di dire esattamente cosa c'è di sbagliato quando le stringhe differiscono. Semplicemente dirà "Fallire affermando che il falso è vero.". Ma ci occuperemo di questo in un prossimo tutorial.

Pensieri finali

Abbiamo finito per questo tutorial. Abbiamo imparato molto per la nostra prima lezione e siamo di buon inizio per il nostro lavoro futuro. Abbiamo incontrato il codice, l'abbiamo analizzato in modi diversi e abbiamo capito la sua logica essenziale. Quindi abbiamo creato una serie di test per assicurarci che venga esercitata il più possibile. Sì. I test sono molto lenti. Ci vogliono 24 secondi sulla mia CPU Core i7 per generare l'output due volte. Fortunatamente nel nostro sviluppo futuro, manterremo il gm.txt file intoccato e generane un altro una sola volta per corsa. Ma 12 secondi sono ancora una quantità enorme di tempo per una base di codice così piccola.

Quando finiremo questa serie, i nostri test dovrebbero essere eseguiti in meno di un secondo e testare correttamente tutto il codice. Quindi, restate sintonizzati per il nostro prossimo tutorial quando affronteremo problemi come costanti magiche, stringhe magiche e condizionali complessi. Grazie per aver letto.