Refactoring Legacy Code - Part 10 Dissecting Long Methods with Extractions

Nella sesta parte della nostra serie abbiamo parlato di attaccare metodi lunghi sfruttando la programmazione di coppie e visualizzando il codice da diversi livelli. Abbiamo ingrandito e rimpicciolito continuamente e osservato sia le piccole cose come la denominazione, sia la forma e la rientranza.

Oggi adotteremo un altro approccio: assumeremo che siamo soli, nessun collega o coppia che ci aiuti. Useremo una tecnica chiamata "Estrai fino allo sfinimento" che rompe il codice in pezzi molto piccoli. Faremo tutto il possibile per rendere questi pezzi il più facili da capire possibile in modo che il futuro noi o qualsiasi altro programmatore saranno in grado di capirli facilmente.


Estratto fino allo sfinimento

Ho sentito parlare per la prima volta di questo concetto di Robert C. Martin. Ha presentato l'idea in uno dei suoi video come un modo semplice per refactoring codice che è difficile da capire.

L'idea di base è di prendere piccoli pezzi di codice comprensibili e di estrarli. Non importa se si identificano quattro righe o quattro caratteri che possono essere estratti. Quando si identifica qualcosa che può essere incapsulato in un concetto più chiaro, si estrae. Continui questo processo sia sul metodo originale che sui pezzi appena estratti finché non trovi alcun pezzo di codice che possa essere incapsulato come un concetto.

Questa tecnica è particolarmente utile quando lavori da solo. Ti costringe a pensare a pezzi di codice sia piccoli che più grandi. Ha un altro buon effetto: ti fa pensare al codice: molto! Oltre al metodo di estrazione o al refactoring delle variabili sopra menzionato, ti troverai a rinominare variabili, funzioni, classi e altro.

Vediamo un esempio su un codice casuale da Internet. Stackoverflow è un buon posto per trovare piccoli pezzi di codice. Ecco uno che determina se un numero è primo:

// Controlla se un numero è una funzione primaria isPrime ($ num, $ pf = null) if (! Is_array ($ pf)) for ($ i = 2; $ i

A questo punto, non ho idea di come funzioni questo codice. L'ho appena trovato su Internet mentre scrivo questo articolo e lo scoprirò insieme a te. Il processo che segue potrebbe non essere il più pulito. Piuttosto, rifletterà il mio ragionamento e il mio refactoring come accade, senza una pianificazione anticipata.

Refactoring del Prime Number Checker

Secondo Wikipedia:

Un numero primo (o un numero primo) è un numero naturale maggiore di 1 che non ha divisori positivi diversi da 1 e se stesso. 

Come puoi vedere questo è un metodo semplice per un semplice problema matematico. Ritorna vero o falso, quindi dovrebbe anche essere facile da testare.

class IsPrimeTest estende PHPUnit_Framework_TestCase function testItCanRecognizePrimeNumbers () $ this-> assertTrue (isPrime (1));  // Controlla se un numero è una funzione primaria isPrime ($ num, $ pf = null) // ... il contenuto del metodo come visto sopra

Quando stiamo solo giocando con il codice di esempio, il modo più semplice per andare è mettere tutto in un file di test. In questo modo non dobbiamo pensare a quali file creare, in quali directory appartengono o come includerli nell'altro. Questo è solo un semplice esempio da utilizzare per familiarizzare con la tecnica prima di applicarla su uno dei metodi di gioco a quiz. Quindi, tutto va in un file di prova, puoi nominare come desideri. ho scelto IsPrimeTest.php.

Questo test passa. Il mio prossimo istinto è quello di aggiungere un numero maggiore di numeri primi e di scrivere un altro test con numeri non primi.

function testItCanRecognizePrimeNumbers () $ this-> assertTrue (isPrime (1)); $ This-> assertTrue (isPrime (2)); $ This-> assertTrue (isPrime (3)); $ This-> assertTrue (isPrime (5)); $ This-> assertTrue (isPrime (7)); $ This-> assertTrue (isPrime (11)); 

Questo passa. Ma che dire di questo?

function testItCanRecognizeNonPrimes () $ this-> assertFalse (isPrime (6)); 

Ciò non riesce in modo imprevisto: 6 non è un numero primo. Mi aspettavo che il metodo tornasse falso. Non so come funzioni il metodo o lo scopo del $ pf parametro - Mi stavo semplicemente aspettando che tornasse falso in base al nome e alla descrizione. Non ho idea del perché non funzioni né di come risolverlo.

Questo è un dilemma abbastanza confuso. Cosa dovremmo fare? La migliore risposta è scrivere test che passino per un volume decente di numeri. Potremmo aver bisogno di provare e indovinare, ma almeno avremo qualche idea su cosa faccia il metodo. Quindi possiamo iniziare a rifarlo.

function testFirst20NaturalNumbers () for ($ i = 1; $ i<20;$i++)  echo $i . ' - ' . (isPrime($i) ? 'true' : 'false') . "\n";  

Ciò produce qualcosa di interessante:

1 - vero 2 - vero 3 - vero 4 - vero 5 - vero 6 - vero 7 - vero 8 - vero 9 - vero 10 - falso 11 - vero 12 - falso 13 - vero 14 - falso 15 - vero 16 - falso 17 - true 18 - false 19 - true

Un modello inizia ad emergere qui. Tutto vero fino a 9, quindi alternato fino a 19. Ma questo schema si ripete? Prova ad eseguirlo per 100 numeri e vedrai immediatamente che non lo è. In realtà sembra funzionare per numeri tra 40 e 99. Non varia una volta tra 30-39 nominando 35 come primo. Lo stesso vale per la gamma 20-29. 25 è considerato primo.

Questo esercizio iniziato come un semplice codice per dimostrare una tecnica si dimostra molto più difficile del previsto. Ho deciso di tenerlo perché riflette la vita reale in un modo tipico.

Quante volte hai iniziato a lavorare su un compito che sembrava facile solo per scoprire che è estremamente difficile?

Non vogliamo correggere il codice. Qualunque sia il metodo, dovrebbe continuare a farlo. Vogliamo refactoring per far capire agli altri meglio.

Poiché non indica i numeri primi in modo corretto, useremo lo stesso approccio del Golden Master che abbiamo imparato nella Lezione Uno.

function testGenerateGoldenMaster () for ($ i = 1; $ i<10000;$i++)  file_put_contents(__DIR__ . '/IsPrimeGoldenMaster.txt', $i . ' - ' . (isPrime($i) ? 'true' : 'false') . "\n", FILE_APPEND);  

Esegui questa volta per generare il Golden Master. Dovrebbe correre veloce. Se è necessario rieseguire, non dimenticare di eliminare il file prima di eseguire il test. In caso contrario, l'output verrà allegato al contenuto precedente.

function testMatchesGoldenMaster () $ goldenMaster = file (__ DIR__. '/IsPrimeGoldenMaster.txt'); per ($ i = 1; $ i<10000;$i++)  $actualResult = $i . ' - ' . (isPrime($i) ? 'true' : 'false'). "\n"; $this->assertTrue (in_array ($ actualResult, $ goldenMaster), 'Il valore'. $ actualResult. 'non è nel golden master.'); 

Ora scrivi il test per il maestro d'oro. Questa soluzione potrebbe non essere la più veloce, ma è facile da capire e ci dirà esattamente quale numero non corrisponde se si rompe qualcosa. Ma c'è una piccola duplicazione nei due metodi di test che potremmo estrarre in a privato metodo.

class IsPrimeTest estende PHPUnit_Framework_TestCase function testGenerateGoldenMaster () $ this-> markTestSkipped (); per ($ i = 1; $ i<10000;$i++)  file_put_contents(__DIR__ . '/IsPrimeGoldenMaster.txt', $this->getPrimeResultAsString ($ i), FILE_APPEND);  function testMatchesGoldenMaster () $ goldenMaster = file (__ DIR__. '/IsPrimeGoldenMaster.txt'); per ($ i = 1; $ i<10000;$i++)  $actualResult = $this->getPrimeResultAsString ($ i); $ this-> assertTrue (in_array ($ actualResult, $ goldenMaster), 'Il valore'. $ actualResult. 'non è nel golden master.');  funzione privata getPrimeResultAsString ($ i) return $ i. '-'. (isPrime ($ i)? 'true': 'false'). "\ N"; 

Ora possiamo passare al nostro codice di produzione. Il test viene eseguito in circa due secondi sul mio computer, quindi è gestibile.

Estrarre tutto ciò che possiamo

Per prima cosa possiamo estrarre un Divisibile () metodo nella prima parte del codice.

if (! is_array ($ pf)) for ($ i = 2; $ i

Questo ci consentirà di riutilizzare il codice nella seconda parte in questo modo:

 else $ pfCount = count ($ pf); for ($ i = 0; $ i<$pfCount;$i++)  if(isDivisible($num, $pf[$i]))  return false;   return true; 

E non appena abbiamo iniziato a lavorare con questo codice, abbiamo osservato che è allineato incautamente. Le parentesi graffe sono a volte all'inizio della linea, altre volte alla fine. 

A volte, le schede vengono utilizzate per il rientro, a volte spazi. A volte ci sono spazi tra operandi e operatori, a volte no. E no, questo non è un codice appositamente creato. Questa è la vita reale. Codice reale, non un esercizio artificiale.

// Controlla se un numero è una funzione primaria isPrime ($ num, $ pf = null) if (! Is_array ($ pf)) for ($ i = 2; $ i < intval(sqrt($num)); $i++)  if (isDivisible($num, $i))  return false;   return true;  else  $pfCount = count($pf); for ($i = 0; $i < $pfCount; $i++)  if (isDivisible($num, $pf[$i]))  return false;   return true;  

Sembra migliore Immediatamente i due Se le dichiarazioni sembrano molto simili. Ma non possiamo estrarli a causa del ritorno dichiarazioni. Se non torniamo, infrangeremo la logica. 

Se il metodo estratto restituisce un valore booleano e lo confrontiamo per decidere se dovremmo o non tornare da isPrime (), quello non aiuterebbe affatto. Potrebbe esserci un modo per estrarlo usando alcuni concetti di programmazione funzionale in PHP, ma forse dopo. Possiamo fare qualcosa di più semplice prima.

function isPrime ($ num, $ pf = null) if (! is_array ($ pf)) return checkDivisorsBetween (2, intval (sqrt ($ num)), $ num);  else $ pfCount = count ($ pf); per ($ i = 0; $ i < $pfCount; $i++)  if (isDivisible($num, $pf[$i]))  return false;   return true;   function checkDivisorsBetween($start, $end, $num)  for ($i = $start; $i < $end; $i++)  if (isDivisible($num, $i))  return false;   return true; 

Estrazione del per il ciclo nel suo complesso è un po 'più semplice, ma quando proviamo a riutilizzare il nostro metodo estratto nella seconda parte del Se possiamo vedere che non funzionerà. C'è questo misterioso $ pf variabile su cui non sappiamo quasi nulla. 

Sembra che controlli se il numero è divisibile da un insieme di divisori specifici invece di prendere tutti i numeri fino all'altro valore magico determinato da intval (sqrt ($ num)). Forse potremmo rinominare $ pf in $ divisori.

function isPrime ($ num, $ divisors = null) if (! is_array ($ divisors)) return checkDivisorsBetween (2, intval (sqrt ($ num)), $ num);  else return check Divisors Between (0, count ($ divisors), $ num, $ divisors);  function checkDivisorsBetween ($ start, $ end, $ num, $ divisors = null) for ($ i = $ start; $ i < $end; $i++)  if (isDivisible($num, $divisors ? $divisors[$i] : $i))  return false;   return true; 

Questo è un modo per farlo. Abbiamo aggiunto un quarto parametro opzionale al nostro metodo di controllo. Se ha un valore, lo usiamo, altrimenti usiamo $ i.

Possiamo estrarre qualcos'altro? Che dire di questo pezzo di codice: intval (sqrt ($ num))?

function isPrime ($ num, $ divisors = null) if (! is_array ($ divisors)) return checkDivisorsBetween (2, integerRootOf ($ num), $ num);  else return check Divisors Between (0, count ($ divisors), $ num, $ divisors);  function integerRootOf ($ num) return intval (sqrt ($ num)); 

Non è meglio? Un po '. È meglio se la persona che viene dopo di noi non sa cosa intval () e sqrt () stanno facendo, ma non aiuta a rendere la logica più facile da capire. Perché finiamo il nostro per loop a quel numero specifico? Forse questa è la domanda alla quale dovrebbe rispondere il nostro nome di funzione.

[PHP] // Controlla se un numero è funzione primaria isPrime ($ num, $ divisors = null) if (! Is_array ($ divisori)) return checkDivisorsBetween (2, highestPossibleFactor ($ num), $ num);  else return check Divisors Between (0, count ($ divisors), $ num, $ divisors);  function highestPossibleFactor ($ num) return intval (sqrt ($ num));  [PHP]

È meglio perché spiega perché ci fermiamo qui. Forse in futuro possiamo inventare una formula diversa per determinare quel numero. La denominazione ha anche introdotto un po 'di incoerenza. Abbiamo chiamato i fattori numerici, che è sinonimo di divisori. Forse dovremmo sceglierne uno e usarlo solo Ti lascerò fare il refactoring del renaming come esercizio.

La domanda è, possiamo estrarre ulteriormente? Bene, dobbiamo provarci finché non cadiamo. Ho citato il lato della programmazione funzionale di PHP alcuni paragrafi sopra. Ci sono due principali caratteristiche di programmazione funzionale che possiamo facilmente applicare in PHP: funzioni di prima classe e ricorsione. Ogni volta che vedo un Se dichiarazione con a ritorno dentro a per loop, come nel nostro checkDivisorsBetween () metodo, penso di applicare una o entrambe le tecniche.

function checkDivisorsBetween ($ start, $ end, $ num, $ divisors = null) for ($ i = $ start; $ i < $end; $i++)  if (isDivisible($num, $divisors ? $divisors[$i] : $i))  return false;   return true; 

Ma perché dovremmo affrontare un processo di pensiero così complesso? La ragione più fastidiosa è che questo metodo fa due cose distinte: cicli e decide. Voglio solo andare in bicicletta e lasciare la decisione a un altro metodo. Un metodo dovrebbe sempre fare una cosa sola e farlo bene.

function checkDivisorsBetween ($ start, $ end, $ num, $ divisors = null) $ numberIsNotPrime = function ($ num, $ divisor) if (isDivisible ($ num, $ divisor)) return false; ; per ($ i = $ inizio; $ i < $end; $i++)  $numberIsNotPrime($num, $divisors ? $divisors[$i] : $i);  return true; 

Il nostro primo tentativo è stato quello di estrarre la condizione e l'istruzione return in una variabile. Questo è locale, per il momento. Ma il codice non funziona. In realtà il per il ciclo complica un po 'le cose. Ho la sensazione che un po 'di ricorsione aiuterà.

function checkRecursiveDivisibility ($ current, $ end, $ num, $ divisor) if ($ current == $ end) return true; 

Quando pensiamo alla ricorsività, dobbiamo sempre iniziare con i casi eccezionali. La nostra prima eccezione è quando raggiungiamo la fine della nostra ricorsione.

function checkRecursiveDivisibility ($ current, $ end, $ num, $ divisor) if ($ current == $ end) return true;  if (isDivisible ($ num, $ divisor)) return false; 

Il nostro secondo caso eccezionale che interromperà la ricorsione è quando il numero è divisibile. Non vogliamo continuare. E questo riguarda tutti i casi eccezionali.

ini_set ('xdebug.max_nesting_level', 10000); function checkDivisorsBetween ($ start, $ end, $ num, $ divisors = null) return checkRecursiveDivisibility ($ start, $ end, $ num, $ divisors);  function checkRecursiveDivisibility ($ current, $ end, $ num, $ divisors) if ($ current == $ end) return true;  if (isDivisible ($ num, $ divisors? $ divisors [$ current]: $ current)) return false;  checkRecursiveDivisibility ($ current ++, $ end, $ num, $ divisors); 

Questo è un altro tentativo di usare la ricorsione per il nostro problema, ma sfortunatamente, ricorrenti 10.000 volte in PHP portano ad un crash di PHP o PHPUnit sul mio sistema. Quindi questo sembra essere un altro vicolo cieco. Ma se avrebbe funzionato, sarebbe stata una buona sostituzione della logica originale.


Sfida

Quando ho scritto il Golden Master, ho intenzionalmente trascurato qualcosa. Diciamo solo che i test non coprono tutto il codice che dovrebbero. Riesci a individuare il problema? Se sì, come ti avvicineresti?


Pensieri finali

"Estrarre fino allo sfinimento" è un buon modo per analizzare i metodi lunghi. Ti costringe a pensare a piccoli pezzi di codice ea dare ai pezzi uno scopo estraendoli in metodi. Trovo incredibile come questa semplice procedura, insieme alla frequente rinomina, possa aiutarmi a scoprire che un codice fa cose che non avrei mai pensato possibili.

Nel nostro prossimo e ultimo tutorial sul refactoring, applicheremo questa tecnica al gioco a quiz. Spero ti sia piaciuto questo tutorial che si è rivelato un po 'diverso. Invece di parlare su esempi di libri di testo, abbiamo preso del codice reale e abbiamo dovuto combattere con i problemi reali che affrontiamo ogni giorno.