Refactoring Legacy Code Part 3 - Complex Conditionals

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

Mi piace pensare al codice proprio come penso alla prosa. Frasi lunghe e nidificate composte con parole esotiche sono difficili da capire. Di volta in volta ne hai bisogno, ma la maggior parte delle volte puoi semplicemente usare parole semplici e semplici in frasi brevi. Questo è molto vero anche per il codice sorgente. I condizionali complessi sono difficili da capire. I metodi lunghi sono come frasi infinite.

Dalla prosa al codice

Ecco un esempio "prosaico" per tirarti su di morale. Innanzitutto, la frase all-in-one. Il brutto.

Se la temperatura nella stanza del server è inferiore a cinque gradi e l'umidità sale oltre il cinquanta per cento ma rimane sotto gli ottanta e la pressione dell'aria è costante, il tecnico senior John, che ha almeno tre anni di esperienza lavorativa in rete e amministrazione dei server, dovrebbe essere informato, e deve svegliarsi nel mezzo della notte, vestirsi, uscire, prendere la sua auto o chiamare un taxi se non ha una macchina, guidare in ufficio, entrare nell'edificio, avviare l'aria condizionata e attendere che la temperatura aumenti di oltre dieci gradi e che l'umidità scenda al di sotto del venti percento.

Se riesci a capire, comprendere e ricordare quel paragrafo senza rileggerlo, ti do una medaglia (virtuale ovviamente). I paragrafi lunghi e aggrovigliati scritti in una singola frase complicata sono difficili da capire. Sfortunatamente, non conosco abbastanza parole esotiche inglesi per renderlo ancora più difficile da capire.

Semplificazione

Troviamo un modo per semplificarlo un po '. Tutta la sua prima parte, fino a quando "allora" è una condizione. Sì, è complicato ma potremmo riassumerlo in questo modo: Se le condizioni ambientali rappresentano un rischio ... ... allora qualcosa dovrebbe essere fatto. L'espressione complicata dice che dovremmo avvisare qualcuno che soddisfa molte condizioni: quindi notifica il supporto tecnico di livello tre. Infine, viene descritto un intero processo dal risveglio del tecnico finché non viene risolto tutto: e aspettarsi che l'ambiente venga ripristinato entro i normali parametri. Mettiamo tutto insieme.

Se le condizioni ambientali rappresentano un rischio, informare il livello 3 di supporto tecnico e prevedere che l'ambiente venga ripristinato entro i normali parametri.

Ora, questo è solo circa il 20% in lunghezza rispetto al testo originale. Non conosciamo i dettagli e nella maggioranza significativa dei casi, non ci interessa. E questo è molto vero anche per il codice sorgente. Quante volte ti sono piaciuti i dettagli di implementazione di a logInfo ("Qualche messaggio"); metodo? Probabilmente una volta, se e quando l'hai implementato. Quindi registra il messaggio nella categoria "informazioni". O quando un utente acquista uno dei tuoi prodotti, ti interessa come fatturarlo? No. Tutto quello che ti interessa è se il prodotto è stato acquistato, scartalo dall'inventario e fatturalo all'acquirente. Gli esempi potrebbero essere infiniti. Sono fondamentalmente come scriviamo il software corretto.

Condizionatori complessi

In questa sezione cercheremo di applicare la filosofia della prosa al nostro gioco a quiz. Un passo alla volta. A partire da condizionali complessi. Iniziamo con un codice facile. Giusto per scaldarsi.

Linea venti del GameRunner.php il file si legge così:

if (rand ($ minAnswerId, $ maxAnswerId) == $ wrongAnswerId)

Come suonerebbe in prosa? Se un numero casuale tra ID risposta minima e ID risposta massimo equivale all'ID della risposta errata, quindi ...

Questo non è molto complicato, ma possiamo ancora renderlo più semplice. Che dire di questo? Se viene selezionata una risposta errata, quindi ... Meglio, non è vero??

Il metodo di estrazione Refactoring

Abbiamo bisogno di un modo, una procedura, una tecnica per spostare quella dichiarazione condizionale da qualche altra parte. Quella destinazione può essere facilmente un metodo. O nel nostro caso, dal momento che non siamo all'interno di una classe qui, una funzione. Questo spostamento del comportamento in un nuovo metodo o funzione è chiamato il refactoring del metodo "Extract Method". Di seguito sono riportati i passaggi, come definito da Martin Fowler nel suo eccellente libro Refactoring: Improving the Design of Existing Code. Se non hai letto questo libro, dovresti metterlo nella tua lista "Da leggere" ora. È uno dei libri più essenziali per un programmatore moderno.

Per il nostro tutorial, ho preso i passaggi originali e li ho semplificati un po 'per adattarli meglio alle nostre esigenze e al nostro tipo di tutorial.

  1. Crea un nuovo metodo e nominalo dopo quello che fa, non come lo fa.
  2. Copia il codice dal posto estratto, nel metodo. Si prega di notare, questo è copia, non cancellare ancora il codice originale.
  3. Analizza il codice estratto per qualsiasi variabile locale. Devono essere fatti parametri per il metodo.
  4. Verifica se nel metodo estratto sono utilizzate variabili temporanee. Se è così, dichiarali al suo interno e rilascia il parametro extra.
  5. Passa nel metodo di destinazione le variabili come parametri.
  6. Sostituisci il codice estratto con la chiamata al metodo di destinazione.
  7. Esegui i tuoi test.

Ora questo è abbastanza complicato. Tuttavia, il metodo di estrazione è probabilmente il refactoring più frequentemente utilizzato, tranne che per la ridenominazione. Quindi devi capire la sua meccanica.

Fortunatamente per noi, gli IDE moderni come PHPStorm forniscono strumenti di refactoring di grande impatto, come abbiamo visto nel tutorial PHPStorm: Quando l'IDE è davvero importante. Quindi useremo le funzionalità che abbiamo a portata di mano, invece di farlo tutto a mano. Questo è meno soggetto a errori e molto, molto più veloce.

Basta selezionare la parte desiderata del codice e tasto destro del mouse esso.

L'IDE capirà automaticamente che abbiamo bisogno di tre parametri per eseguire il nostro codice e proporrà la seguente soluzione.

// ... // $ minAnswerId = 0; $ maxAnswerId = 9; $ wrongAnswerId = 7; function isCurrentAnswerWrong ($ minAnswerId, $ maxAnswerId, $ wrongAnswerId) return rand ($ minAnswerId, $ maxAnswerId) == $ wrongAnswerId;  do $ dice = rand (0, 5) + 1; $ AGame-> rotolo ($ dadi); if (isCurrentAnswerWrong ($ minAnswerId, $ maxAnswerId, $ wrongAnswerId)) $ notAWinner = $ aGame-> wrongAnswer ();  else $ notAWinner = $ aGame-> wasCorrectlyAnswered ();  while ($ notAWinner);

Mentre questo codice è sintatticamente corretto, romperà i nostri test. Tra tutti i rumori mostrati nei colori rosso, blu e nero, possiamo individuare il motivo per cui:

Errore irreversibile: impossibile redeclare isCurrentAnswerWrong () (precedentemente dichiarato in / home / csaba / Personale / Programmazione / NetTuts / Refactoring Legacy Code - Parte 3: Complex Conditionals e Long Methods / Source /trivia/php/GameRunner.php:16) in / home / csaba / Personale / Programmazione / NetTuts / Refactoring Legacy Code - Parte 3: Conditionals complessi e metodi lunghi /Source/trivia/php/GameRunner.php on line 18

In pratica dice che vogliamo dichiarare la funzione due volte. Ma come può accadere? Lo abbiamo solo una volta nel nostro GameRunner.php!

Dai un'occhiata ai test. C'è un generateOutput () metodo che fa a richiedere() sulla nostra GameRunner.php. Si chiama almeno due volte. Ecco la fonte dell'errore.

Ora abbiamo un dilemma. A causa della semina del generatore casuale, dobbiamo chiamare questo codice con valori controllati.

funzione privata generateOutput ($ seed) ob_start (); srand ($ seme); richiede __DIR__. '/ ... /trivia/php/GameRunner.php'; $ output = ob_get_contents (); ob_end_clean (); restituire $ output; 

Ma non c'è modo di dichiarare una funzione due volte in PHP, quindi abbiamo bisogno di un'altra soluzione. Iniziamo a sentire il peso del nostro maestro d'oro. Eseguendo il tutto 20000 volte, ogni volta che cambiamo un pezzo di codice, potrebbe non essere una soluzione a lungo termine. Oltre al fatto che impiegano anni per funzionare, ci obbliga a cambiare il nostro codice per adattarlo al modo in cui lo testiamo. Questo di solito è un segno di test non validi. Il codice dovrebbe cambiare e continuare a fare il test, ma i cambiamenti dovrebbero avere dei motivi per cambiare, provenienti solo dal codice sorgente.

Ma basta parlare, abbiamo bisogno di una soluzione, anche temporanea per ora. La migrazione ai test unitari inizierà con la nostra prossima lezione.

Un modo per risolvere il nostro problema è prendere tutto il resto del codice GameRunner.php e metterlo in una funzione. Diciamo correre()

include_once __DIR__. '/Game.php'; function isCurrentAnswerWrong ($ minAnswerId, $ maxAnswerId, $ wrongAnswerId) return rand ($ minAnswerId, $ maxAnswerId) == $ wrongAnswerId;  function run () $ notAWinner; $ aGame = new Game (); $ AGame-> aggiungere ( "Chet"); $ AGame-> aggiungere ( "Pat"); $ AGame-> aggiungere ( "Sue"); $ minAnswerId = 0; $ maxAnswerId = 9; $ wrongAnswerId = 7; do $ dice = rand (0, 5) + 1; $ AGame-> rotolo ($ dadi); if (isCurrentAnswerWrong ($ minAnswerId, $ maxAnswerId, $ wrongAnswerId)) $ notAWinner = $ aGame-> wrongAnswer ();  else $ notAWinner = $ aGame-> wasCorrectlyAnswered ();  while ($ notAWinner); 

Questo ci consentirà di testarlo, ma tieni presente che l'esecuzione del codice dalla console non eseguirà il gioco. Abbiamo fatto un leggero cambiamento nel comportamento. Abbiamo acquisito la testabilità al costo di un cambiamento comportamentale, cosa che non volevamo fare in primo luogo. Se si desidera eseguire il codice dalla console, ora sarà necessario un altro file PHP che includa o richieda il programma di esecuzione, quindi chiama esplicitamente il metodo di esecuzione su di esso. Non un grande cambiamento, ma un dovere da ricordare, soprattutto se hai terze parti che usano il tuo codice esistente.

D'altra parte, ora possiamo solo includere il file nel nostro test.

richiede __DIR__. '/ ... /trivia/php/GameRunner.php';

E poi chiama correre() all'interno del metodo generateOutput ().

funzione privata generateOutput ($ seed) ob_start (); srand ($ seme); correre(); $ output = ob_get_contents (); ob_end_clean (); restituire $ output; 

Struttura della directory, file e denominazione

Forse questa è una buona opportunità per pensare alla struttura delle nostre directory e file. Non ci sono condizionali più complessi nel nostro GameRunner.php, ma prima di continuare con il Game.php file, non dobbiamo lasciare un casino dietro di noi. Nostro GameRunner.php non sta eseguendo più nulla, e abbiamo bisogno di hackerare i metodi insieme per renderlo testabile, cosa che ha infranto la nostra interfaccia pubblica. Il motivo è che forse stiamo testando la cosa sbagliata.

Il nostro test chiama correre() nel GameRunner.php file, che include Game.php, gioca il gioco e viene generato un nuovo file master dorato. Cosa succede se introduciamo un altro file? Facciamo il GameRunner.php per eseguire effettivamente il gioco chiamando correre() e nient'altro. Quindi, cosa succede se non c'è logica in là che potrebbe andare storta e non sono necessari test, e quindi spostiamo il nostro codice corrente in un altro file?

Ora questa è una storia completamente diversa. Ora i nostri test stanno accedendo al codice appena sotto il corridore. Fondamentalmente, i nostri test sono solo corridori. E naturalmente nel nostro nuovo GameRunner.php ci sarà solo una chiamata per eseguire il gioco. Questo è un vero corridore, non fa altro che chiamare il correre() metodo. Nessuna logica significa che non sono necessari test.

require_once __DIR__. '/RunnerFunctions.php'; correre();

Ci sono altre domande che potremmo chiederci a questo punto. Abbiamo davvero bisogno di un RunnerFunctions.php? Non potremmo semplicemente prendere le funzioni da lì e spostarle in Game.php? Probabilmente potremmo, ma con le nostre attuali conoscenze, a quale funzione appartiene dove? Non è abbastanza. Troveremo un posto per il nostro metodo in una lezione imminente.

Abbiamo anche provato a dare un nome ai nostri file in base a ciò che sta facendo il codice al loro interno. Uno è solo un mucchio di funzioni per il corridore, funzioni che, a questo punto, consideriamo di stare insieme, al fine di soddisfare le esigenze del corridore. Questo diventerà una classe in un punto nel futuro? Può essere. Forse no. Per ora, è abbastanza buono.

Pulizia di RunnerFunctions

Se diamo un'occhiata al RunnerFunctions.php file, c'è un po 'di confusione che abbiamo introdotto.

Definiamo:

$ minAnswerId = 0; $ maxAnswerId = 9; $ wrongAnswerId = 7;

… dentro il correre() metodo. Hanno una sola ragione per esistere e un posto unico in cui vengono utilizzati. Perché non limitarsi a definirli all'interno di quel metodo e sbarazzarsi del tutto dei parametri?

function isCurrentAnswerWrong () $ minAnswerId = 0; $ maxAnswerId = 9; $ wrongAnswerId = 7; return rand ($ minAnswerId, $ maxAnswerId) == $ wrongAnswerId; 

Ok, i test stanno passando e il codice è molto più bello. Ma non abbastanza buono.

Condizionali negativi

È molto più facile, per la mente umana, comprendere il ragionamento positivo. Quindi, se puoi evitare condizionali negativi, dovresti sempre prendere quella strada. Nel nostro esempio corrente, il metodo verifica una risposta errata. Sarebbe molto più facile capire un metodo che verifica la validità e lo nega, quando necessario.

function isCurrentAnswerCorrect () $ minAnswerId = 0; $ maxAnswerId = 9; $ wrongAnswerId = 7; return rand ($ minAnswerId, $ maxAnswerId)! = $ wrongAnswerId; 

Abbiamo usato il refactoring del metodo Rename. Questo è ancora una volta, piuttosto complicato se usato a mano, ma in qualsiasi IDE è semplice come colpire CTRL + R, o selezionando l'opzione appropriata nel menu. Per far passare i nostri test, dobbiamo anche aggiornare la nostra dichiarazione condizionale con una negazione.

if (! isCurrentAnswerCorrect ()) $ notAWinner = $ aGame-> wrongAnswer ();  else $ notAWinner = $ aGame-> wasCorrectlyAnswered (); 

Questo ci avvicina di un passo alla nostra comprensione del condizionale. utilizzando ! in un Se() affermazione, in realtà aiuta. Si distingue e sottolinea il fatto che qualcosa è effettivamente negato lì. Ma possiamo invertire questo ordine, per evitare completamente la negazione? sì possiamo.

if (isCurrentAnswerCorrect ()) $ notAWinner = $ aGame-> wasCorrectlyAnswered ();  else $ notAWinner = $ aGame-> wrongAnswer (); 

Ora non abbiamo nessuna negazione logica usando !, né negazione lessicale nominando e restituendo le cose sbagliate. Tutti questi passaggi hanno reso il nostro condizionale molto, molto più facile da comprendere.

Condizionali in Game.php

Abbiamo semplificato fino all'estremo, RunnerFunctions.php. Attacchiamo il nostro Game.php file ora. Esistono diversi modi per cercare i condizionali. Se preferisci, puoi semplicemente scansionare il codice semplicemente guardandolo. Questo è più lento, ma ha il valore aggiunto di forzarti a cercare di capirlo in modo sequenziale.

Il secondo modo ovvio per cercare i condizionali è di fare una ricerca per "if" o "if (".) Se hai formattato il tuo codice con le funzionalità integrate del tuo IDE, puoi essere sicuro che tutte le istruzioni condizionali hanno stessa forma specifica, nel mio caso c'è uno spazio tra il "se" e la parentesi.Inoltre, se usi la ricerca integrata, i risultati trovati saranno evidenziati con un colore stridente, nel mio caso giallo.

Ora che abbiamo tutti di illuminare il nostro codice come un albero di Natale, possiamo prenderli uno per uno. Conosciamo il trapano, conosciamo le tecniche che possiamo usare, è il momento di applicarle.

se ($ this-> inPenaltyBox [$ this-> currentPlayer])

Questo sembra abbastanza ragionevole. Potremmo estrarlo in un metodo, ma ci sarebbe un nome per quel metodo per rendere più chiara la condizione?

if ($ roll% 2! = 0) 

Scommetto che il 90% di tutti i programmatori può capire il problema di cui sopra Se dichiarazione. Stiamo cercando di concentrarci su ciò che fa il nostro metodo attuale. E il nostro cervello è collegato al dominio del problema. Non vogliamo "iniziare un altro thread" per calcolare quell'espressione matematica per capire che controlla solo se un numero è dispari. Questa è una di quelle piccole distrazioni che possono rovinare una deduzione logica difficile. Quindi dico estrailo.

se ($ this-> isOdd ($ roll))

Questo è meglio perché riguarda il dominio del problema e non richiede potenza cerebrale aggiuntiva.

if ($ this-> places [$ this-> currentPlayer]> $ lastPositionOnTheBoard)

Questo sembra essere un altro buon candidato. Non è così difficile da capire come espressione matematica, ma, ancora una volta, è un'espressione che ha bisogno di un'elaborazione laterale. Mi chiedo, cosa significa se la posizione attuale del giocatore ha raggiunto la fine del tabellone? Non possiamo esprimere questo stato in un modo più conciso? Probabilmente possiamo.

if ($ this-> playerReachedEndOfBoard ($ lastPositionOnTheBoard))

Questo è meglio. Ma cosa succede realmente dentro il Se? Il giocatore viene riposizionato all'inizio del tabellone. Il giocatore inizia un nuovo "giro" in gara. Cosa succederebbe se in futuro avessimo un motivo diverso per iniziare un nuovo giro? Dovremmo il nostro Se modifica delle istruzioni quando cambiamo la logica sottostante nel metodo privato? Assolutamente no! Quindi, rinominiamo questo metodo in ciò che il Se rappresenta, in ciò che accade, non quello che stiamo cercando.

se ($ this-> playerShouldStartANewLap ($ lastPositionOnTheBoard))

Quando provi a nominare metodi e variabili, pensa sempre a cosa dovrebbe fare il codice e non a quale stato o condizione esso rappresenta. Una volta ottenuto questo, la ridenominazione delle azioni nel codice diminuirà in modo significativo. Tuttavia, anche un programmatore esperto deve rinominare un metodo almeno da tre a cinque volte prima di trovare il nome corretto. Quindi non aver paura di colpire CTRL + R e rinomina frequentemente. Non commettere mai le modifiche al VCS del progetto se non hai analizzato i nomi dei nuovi metodi aggiunti e il tuo codice non viene letto come una prosa ben scritta. La ridenominazione è così a buon mercato ai nostri giorni, che è possibile rinominare le cose solo per provare varie versioni e ripristinare con un singolo colpo di un pulsante.

Il Se l'affermazione alla riga 90 è la stessa della nostra precedente. Possiamo solo riutilizzare il nostro metodo estratto. Voilà, eliminata la duplicazione! E non dimenticare di eseguire i tuoi test ora-e-poi, anche quando ti rifatti utilizzando la magia dell'IDE. Il che ci porta alla nostra prossima osservazione. La magia, a volte, fallisce. Controlla la linea 65.

$ lastPositionOnTheBoard = 11;

Dichiariamo una variabile e la usiamo solo in un singolo posto, come parametro del nostro metodo appena estratto. Questo suggerisce fortemente che la variabile dovrebbe essere all'interno del metodo.

private function playerShouldStartANewLap () $ lastPositionOnTheBoard = 11; restituire $ this-> places [$ this-> currentPlayer]> $ lastPositionOnTheBoard; 

E non dimenticare di chiamare il metodo senza parametri nel tuo Se dichiarazioni.

if ($ this-> playerShouldStartANewLap ())

Il Se dichiarazioni nel askQuestion () il metodo sembra andare bene, così come quelli in currentCategory ().

se ($ this-> inPenaltyBox [$ this-> currentPlayer])

Questo è un po 'più complicato, ma nel dominio e abbastanza espressivo.

se ($ this-> currentPlayer == conta ($ this-> giocatori))

Possiamo lavorare su questo. È ovvio il mezzo di confronto, se il giocatore attuale è fuori limite. Ma come abbiamo imparato in precedenza, vogliamo che l'intenzione non sia dichiarata.

if ($ this-> shouldResetCurrentPlayer ())

È molto meglio, e lo riutilizzeremo alle righe 172, 189 e 203. Duplicazione, intendo triplicazione, intendo quadruplicazione, eliminata!

I test stanno passando e tutto Se le dichiarazioni sono state valutate per complessità.

Pensieri finali

Ci sono diverse lezioni che possono essere apprese dai condizionali di refactoring. Prima di tutto, aiutano a comprendere meglio l'intento del codice. Quindi se si nomina il metodo estratto per rappresentare correttamente l'intento, si eviteranno le future modifiche al nome. Trovare la duplicazione in logica è più difficile che trovare linee duplicate di codice semplice. Potresti aver pensato che dovremmo fare una duplicazione cosciente, ma preferisco occuparmi della duplicazione quando ho dei test unitari su cui posso fidarmi della mia vita. Il Golden Master è buono, ma è al massimo una rete di sicurezza, non un paracadute.

Grazie per la lettura e restate sintonizzati per il nostro prossimo tutorial quando presenteremo i nostri primi test di unità.