Refactoring Code Legacy - Partea 10 Disecarea metodelor lungi cu extracții

În cea de-a șasea parte a seriei noastre, am vorbit despre atacarea metodelor lungi prin folosirea programelor de pereche și a codului de vizualizare de la diferite nivele. Am continuat să mărim și mai departe și am observat atât lucruri mici precum denumirea, precum și forma și indentarea.

Astăzi, vom lua o altă abordare: Vom presupune că suntem singuri, nu avem nici un coleg sau o pereche care să ne ajute. Vom folosi o tehnică numită "Extract until you drop", care rupe codul în bucăți foarte mici. Vom depune toate eforturile pentru a face aceste bucăți la fel de ușor de înțeles încât viitorul nostru sau orice alt programator să le poată înțelege cu ușurință.


Extrageți până la cădere

Am auzit prima dată despre acest concept de la Robert C. Martin. El a prezentat ideea într-unul din videoclipurile sale ca o modalitate simplă de a reface codul greu de înțeles.

Ideea de bază este de a lua mici bucăți de cod și de a le extrage. Nu contează dacă identificați patru linii sau patru caractere care pot fi extrase. Când identificați ceva ce poate fi încapsulat într-un concept mai clar, extrageți. Continuați acest proces atât pe metoda originală, cât și pe piesele recent extrase, până când nu găsiți nici o bucată de cod care poate fi încapsulată ca un concept.

Această tehnică este utilă în special când lucrați singur. Te obligă să te gândești atât la bucăți mici cât și la cele mai mari de cod. Are un alt efect frumos: Te face să te gândești la cod - mult! În afară de metoda de extragere sau refactorizare variabilă menționată mai sus, veți găsi că vă redenumiți variabile, funcții, clase și multe altele.

Să vedem un exemplu pe un cod aleatoriu de pe Internet. Stackoverflow este un loc bun pentru a găsi mici bucăți de cod. Aici este unul care determină dacă un număr este prime:

// Verificați dacă un număr este funcția prime isPrime ($ num, $ pf = null) if (! Is_array ($ pf)) pentru ($ i = 2; $ i

În acest moment, nu am nici o idee despre cum funcționează acest cod. Tocmai am găsit-o pe Internet în timp ce scriu acest articol și o voi descoperi împreună cu tine. Procesul care urmează nu poate fi cel mai curat. În schimb, ea va reflecta raționamentul meu și refactorizarea așa cum se întâmplă, fără planificare în avans.

Refactorizarea primului număr de verificator

Potrivit Wikipedia:

Un număr prime (sau prime) este un număr natural mai mare de 1, care nu are divizori pozitivi alții decât 1 și el însuși. 

După cum puteți vedea, aceasta este o metodă simplă pentru o problemă matematică simplă. Se întoarce Adevărat sau fals, astfel încât ar trebui să fie și ușor de testat.

clasa IsPrimeTest extinde PHPUnit_Framework_TestCase function testItCanRecognizePrimeNumbers () $ this-> assertTrue (isPrime (1));  // Verificați dacă un număr este prima funcție isPrime ($ num, $ pf = null) // ... conținutul metodei așa cum se vede mai sus

Când jucăm doar cu cod exemplu, cel mai simplu mod de a merge este să puneți totul într-un fișier de testare. În acest fel nu trebuie să ne gândim la fișierele pe care să le creăm, la ce fel de directoare aparțin, sau cum să le includem în cealaltă. Acesta este doar un exemplu simplu de folosit pentru a ne familiariza cu tehnica înainte de ao aplica pe una din metodele jocului trivia. Deci, totul merge într-un fișier de testare, puteți numi așa cum doriți. am ales IsPrimeTest.php.

Acest test trece. Următorul meu instinct este să adaug câteva numere prime și să scriu un alt test, fără numere prime.

funcția 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)); 

Asta trece. Dar ce zici de asta??

funcția testItCanRecognizeNonPrimes () $ this-> assertFalse (isPrime (6)); 

Acest lucru nu reușește în mod neașteptat: 6 nu este un număr prime. Mă așteptam ca metoda să se întoarcă fals. Nu știu cum funcționează metoda sau scopul $ pf parametru - Pur și simplu așteptam să se întoarcă fals pe baza numelui și a descrierii. Nu am nici o idee de ce nu funcționează și cum să o rezolv.

Aceasta este o dilemă destul de confuză. Ce ar trebui sa facem? Cel mai bun răspuns este de a scrie teste care trec pentru un volum decent de numere. S-ar putea să trebuiască să încercăm să ghicim, dar cel puțin vom avea o idee despre ceea ce face metoda. Apoi putem începe refactorizarea.

test functionFirst20NaturalNumbers () pentru ($ i = 1; $ i<20;$i++)  echo $i . ' - ' . (isPrime($i) ? 'true' : 'false') . "\n";  

Asta scoate ceva interesant:

1 - adevărat 2 - adevărat 3 - adevărat 4 - adevărat 5 - adevărat 6 - adevărat 7 - adevărat 8 - adevărat 9 - adevărat 10 - fals 11 - adevărat 12 - adevărat 18 - fals 19 - adevărat

Un model începe să apară aici. Toate adevărate până la 9, apoi alternativ până la 19. Dar se repetă acest model? Încercați să o executați pentru 100 de numere și veți vedea imediat că nu este. Este, de fapt, pare a fi de lucru pentru numerele cuprinse între 40 și 99. Acesta misfires o dată între 30-39 prin numirea 35 ca prim. Același lucru este valabil și în domeniul 20-29. 25 este considerat prim.

Acest exercițiu care a început ca un cod simplu pentru a demonstra o tehnică se dovedește a fi mult mai dificil decât se aștepta. M-am hotărât să o țin pentru că reflectă viața reală într-un mod tipic.

De câte ori ați început să lucrați la o sarcină care părea ușor de descoperit că este extrem de dificilă?

Nu vrem să reparăm codul. Oricare ar fi metoda, ar trebui să continue să o facă. Vrem să o refacem pentru ca alții să o înțeleagă mai bine.

Cum nu ne spune numerele prime într-un mod corect, vom folosi aceeași abordare a Maestrului de Aur pe care am învățat-o în Lecția Unu.

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

Rulați o dată pentru a genera Maestrul de Aur. Ar trebui să alerge rapid. Dacă aveți nevoie să o executați din nou, nu uitați să ștergeți fișierul înainte de a efectua testul. Altfel, ieșirea va fi atașată conținutului anterior.

funcția testMatchesGoldenMaster () $ goldenMaster = fișier (__ DIR__. '/IsPrimeGoldenMaster.txt'); pentru ($ i = 1; $ i<10000;$i++)  $actualResult = $i . ' - ' . (isPrime($i) ? 'true' : 'false'). "\n"; $this->assertTrue (in_array ($ actualResult, $ goldenMaster), 'Valoarea' $ actualResult. 'nu este în maestrul de aur.'); 

Acum scrie testul pentru maestrul de aur. Această soluție poate să nu fie cea mai rapidă, dar este ușor de înțeles și ne va spune exact ce număr nu se potrivește dacă spargeți ceva. Dar există o mică dublare în cele două metode de testare pe care le putem extrage într-un privat metodă.

class IsPrimeTest extinde PHPUnit_Framework_TestCase function testGenerateGoldenMaster () $ this-> markTestSkipped (); pentru ($ i = 1; $ i<10000;$i++)  file_put_contents(__DIR__ . '/IsPrimeGoldenMaster.txt', $this->getPrimeResultAsString ($ i), FILE_APPEND);  funcția testMatchesGoldenMaster () $ goldenMaster = fișier (__ DIR__. '/IsPrimeGoldenMaster.txt'); pentru ($ i = 1; $ i<10000;$i++)  $actualResult = $this->getPrimeResultAsString ($ i); $ this-> assertTrue (in_array ($ actualResult, $ goldenMaster), 'Valoarea' $ actualResult. 'nu este în masterul de aur.');  funcția privată getPrimeResultAsString ($ i) retur $ i. "-". (isPrime ($ i)? true: false). "\ N"; 

Acum ne putem muta fără codul nostru de producție. Testul rulează în aproximativ două secunde pe calculatorul meu, deci este ușor de gestionat.

Extragând tot ce putem

Mai întâi putem extrage un isDivisible () în prima parte a codului.

dacă (! is_array ($ pf)) pentru ($ i = 2; $ i

Acest lucru ne va permite să reutilizăm codul în a doua parte astfel:

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

Și de îndată ce am început să lucrăm cu acest cod, am observat că este aliniat neglijent. Bretele sunt uneori la începutul liniei, alteori la sfârșit. 

Uneori, filele sunt folosite pentru indentare, uneori pentru spații. Uneori există spații între operand și operator, uneori nu. Și nu, acesta nu este un cod special creat. Aceasta este viața reală. Cod real, nu un exercițiu artificial.

// Verificați dacă un număr este funcția prime isPrime ($ num, $ pf = null) if (! Is_array ($ pf)) pentru ($ 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;  

Asta arata mai bine. Imediat cele două dacă afirmațiile arată foarte asemănătoare. Dar nu le putem extrage datorită întoarcere declarații. Dacă nu vom reveni, vom rupe logica. 

Dacă metoda extrasă va returna un boolean și o vom compara pentru a decide dacă ar trebui sau nu să revenim isPrime (), care nu ar ajuta deloc. Este posibil să existe o modalitate de a le extrage folosind câteva concepte de programare funcționale în PHP, dar poate mai târziu. Putem face mai întâi ceva mai simplu.

funcția isPrime ($ num, $ pf = null) if (! is_array ($ pf)) întoarcere checkDivisorsBetween (2, intval (sqrt ($ num)), $ num);  altceva $ pfCount = count ($ pf); pentru ($ 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; 

Extragerea pentru buclă ca un întreg este un pic mai ușor, dar când încercăm să reutilizăm metoda noastră extrasă în a doua parte a dacă vedem că nu va funcționa. Există acest misterios $ pf variabilă despre care nu știm aproape nimic. 

Se pare că verifică dacă numărul este divizibil de un set de divizori specifici, în loc să se ia toate numerele până la valoarea cea mai mică determinată de intval (sqrt ($ num)). Poate putem redenumi $ pf în $ divizori.

funcția isPrime ($ num, $ divisors = null) if (! is_array ($ divisors)) întoarcere checkDivisorsBetween (2, intval (sqrt ($ num)), $ num);  altceva return checkDivisorsBetween (0, count ($ divisors), $ num, $ divisors);  funcția checkDivisorsBetween ($ start, $ end, $ num, $ divisors = null) pentru ($ i = $ start; $ i < $end; $i++)  if (isDivisible($num, $divisors ? $divisors[$i] : $i))  return false;   return true; 

Aceasta este o modalitate de ao face. Am adăugat un parametru opțional, opțional, metodei noastre de verificare. Dacă are valoare, îl folosim, altfel vom folosi $ i.

Putem extrage orice altceva? Ce zici de această bucată de cod: intval (sqrt ($ num))?

funcția isPrime ($ num, $ divisors = null) if (! is_array ($ divisors)) return checkDivisorsBetween (2, integerRootOf ($ num), $ num);  altceva return checkDivisorsBetween (0, count ($ divisors), $ num, $ divisors);  funcția integerRootOf ($ num) retur intval (sqrt ($ num)); 

Nu e mai bine? Oarecum. Este mai bine dacă persoana care vine după noi nu știe ce intval () și sqrt () fac, dar nu ajută la înțelegerea mai ușoară a logicii. De ce ne oprim pentru buclă la acel număr specific? Poate că aceasta este întrebarea pe care ar trebui să o răspundă numele funcției noastre.

[PHP] // Verificați dacă un număr este principala funcție isPrime ($ num, $ divisors = null) if (! Is_array ($ divisors)) return checkDivisorsBetween (2, highestPossibleFactor ($ num), $ num);  altceva return checkDivisorsBetween (0, count ($ divisors), $ num, $ divisors);  funcția highestPossibleFactor ($ num) întoarcere intval (sqrt ($ num));  [PHP]

Asta e mai bine, deoarece explică de ce ne oprim acolo. Poate că în viitor vom putea inventa o altă formulă pentru a determina numărul respectiv. Denumirea a introdus și o mică inconsistență. Am numit numerele factorilor, care este un sinonim al divizoarelor. Poate ar trebui să alegem una și să folosim doar asta. Vă voi lăsa să faceți ca refacerea redenumitului să fie un exercițiu.

Întrebarea este, putem extrage mai departe? Ei bine, trebuie să încercăm să scăpăm. Am menționat partea de programare funcțională a PHP câteva paragrafe de mai sus. Există două caracteristici principale de programare funcționale pe care le putem aplica cu ușurință în PHP: funcții de primă clasă și recursivitate. Ori de câte ori văd un dacă declarație cu întoarcere în interiorul a pentru bucla, ca în cazul nostru checkDivisorsBetween () metodă, mă gândesc la aplicarea uneia sau a ambelor tehnici.

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

Dar de ce ar trebui să trecem printr-un astfel de proces de gândire complex? Motivul cel mai enervant este că această metodă are două lucruri distincte: ciclurile și decizia. Vreau doar să ciclu și să las decizia la o altă metodă. O metodă trebuie să facă întotdeauna un singur lucru și să o facă bine.

funcția ($ num, $ divisor) if (este divizibilă ($ num, $ divisor)) return false; ; pentru ($ i = $ start; $ i < $end; $i++)  $numberIsNotPrime($num, $divisors ? $divisors[$i] : $i);  return true; 

Prima noastră încercare a fost să extragem condiția și declarația de returnare într-o variabilă. Acest lucru este local, pentru moment. Dar codul nu funcționează. De fapt, pentru buclă complică lucrurile destul de puțin. Am sentimentul că o mică recursivitate vă va ajuta.

Funcția checkRecursiveDivisibility ($ curent, $ sfârșit, $ num, $ divisor) if ($ curent == $ end) return true; 

Când ne gândim la recursivitate trebuie să începem întotdeauna cu cazurile excepționale. Prima noastră excepție este când ajungem la sfârșitul recursului nostru.

Funcția checkRecursiveDivisibility ($ curent, $ sfârșit, $ num, $ divisor) if ($ curent == $ end) return true;  dacă (esteDivizibil ($ num, $ divisor)) return false; 

Cel de-al doilea caz excepțional care va rupe recursiunea este atunci când numărul este divizibil. Nu vrem să continuăm. Și asta e despre toate cazurile excepționale.

ini_set ('xdebug.max_nesting_level', 10000); funcția checkDivisorsBetween ($ start, $ end, $ num, $ divisors = null) retur checkRecursiveDivisibility ($ start, $ end, $ num, $ divisors);  funcția checkRecursiveDivisibility ($ curent, $ end, $ num, $ divisors) dacă ($ curent == $ end) return true;  dacă (esteDivizibil ($ num, $ divisors? $ divisors [$ current]: $ current)) return false;  checkRecursiveDivisibility ($ curent ++, $ end, $ num, $ divisors); 

Aceasta este o altă încercare de a folosi recursul pentru problema noastră, dar, din păcate, repetarea de 10.000 de ori în PHP duce la un accident de PHP sau PHPUnit pe sistemul meu. Deci, pare a fi un alt punct mort. Dar dacă ar fi funcționat, ar fi fost o înlocuire plăcută a logicii originale.


Provocare

Când am scris Maestrul de Aur, am uitat intenționat ceva. Să spunem doar că testele nu acoperă codul așa cum ar trebui. Puteți observa problema? Dacă da, cum v-ați apropia?


Gândurile finale

"Extras până când picătură" este o modalitate bună de a diseca metode lungi. Ea te forțează să te gândești la mici bucăți de cod și să dai piesele un scop prin extragerea lor în metode. Mi se pare uimitor cum această procedură simplă, împreună cu redenumirea frecventă, mă poate ajuta să descopăr că unele coduri fac lucruri pe care nu le-am crezut niciodată.

În următorul nostru și ultimul tutorial despre refactorizare, vom aplica această tehnică jocului trivia. Sper că ți-a plăcut acest tutorial care sa dovedit a fi puțin diferit. În loc să vorbim despre exemple de manuale, am luat un adevărat cod și trebuia să ne luptăm cu problemele reale cu care ne confruntăm în fiecare zi.

Cod