Refactoring Legacy Code Partea 1 - Maestrul de Aur

Cod vechi. Cod urât. Cod complicat. Codul Spaghete. Nonsens jibber. Cu două cuvinte, Codul vechi. Aceasta este o serie care vă va ajuta să lucrați și să vă ocupați de aceasta.

Într-o lume ideală, ați scrie doar un nou cod. Ai fi scris frumos și perfect. Nu ar trebui să vă revizuiți codul și nu veți mai trebui niciodată să mențineți proiecte de zece ani. Într-o lume ideală ...

Din păcate, trăim într-o realitate care nu este ideală. Trebuie să înțelegem, să modificăm și să îmbunătățim codul vechi - vechi. Trebuie să lucrăm cu codul vechi. Deci ce mai aștepți? Să ne îndreptăm capul în primul tutorial, să obținem codul, să îl înțelegem puțin și să creăm o plasă de siguranță pentru modificările viitoare.

Definiția codului Legacy

Codul moștenitor a fost definit în multe feluri și este imposibil să se găsească o definiție unică, acceptată în mod obișnuit. Câteva exemple de la începutul acestui tutorial sunt doar vârful aisbergului. Deci nu vă voi da o definiție oficială. În schimb, vă voi cita pe cea preferată.

Mie, codul vechi este simplu cod fără teste. ~ Michael Feathers

Ei bine, aceasta este prima definiție formală a expresiei codul vechi, publicat de Michael Feathers în cartea sa "Working Effective with Legacy Code". Desigur, industria a folosit expresia de-a lungul vârstelor, practic pentru orice cod care este greu de schimbat. Cu toate acestea, această definiție are ceva diferit de spus. Aceasta explică foarte clar problema, astfel încât soluția devine evidentă. "Dificil de schimbat" este atât de vag. Ce ar trebui să facem pentru a face schimbarea ușoară? Nu avem idee! "Codul fără teste", pe de altă parte, este foarte concret. Iar răspunsul la întrebarea noastră anterioară este simplu, face testabil codul și îl testați. Deci sa începem.

Obținerea Codului nostru Legacy

Această serie se va baza pe jocul extraordinar de Trivia de J.B. Rainsberger, conceput pentru evenimente Legacy Code Retreat. Este făcut ca un cod de moștenire real și să ofere oportunități pentru o mare varietate de refactorizări, la un nivel decent de dificultate.

Verificați codul sursă

Jocul Trivia este găzduit pe GitHub și este licențiat cu licență GPLv3, astfel încât să puteți juca cu el în mod liber. Vom începe această serie, verificând depozitul oficial. Codul este, de asemenea, atașat la acest tutorial, cu toate modificările pe care le vom face, așa că, dacă te confuzi la un moment dat, poți să te uiți la rezultatul final.

 $ git clone https://github.com/jbrains/trivia.git Clonarea în "trivia" ... remote: Numără obiecte: 429, terminat. telecomanda: comprimarea obiectelor: 100% (262/262), terminat. telecomandă: Total 429 (delta 100), reutilizat 419 (delta 93) Obiecte primite: 100% (429/429), 848,33 KiB | 305,00 KiB / s, terminat. Rezolvarea deltaselor: 100% (100/100), terminat. Verificarea conectivității ... terminată.

Când deschideți trivia director veți vedea codul nostru în mai multe limbi de programare. Vom lucra în PHP, dar sunteți liberi să alegeți preferatul dvs. și să aplicați tehnicile prezentate aici.

Înțelegerea Codului

Prin definiție, codul vechi este greu de înțeles, mai ales dacă nici măcar nu știm ce ar trebui să facă. Deci, primul pas este de a rula codul și de a face un fel de raționament, despre ce este vorba.

Avem două fișiere în directorul nostru.

$ cd php / $ ls -al total 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 pare a fi un bun candidat pentru încercarea noastră de a executa codul.

$ php ./GameRunner.php Chet a fost adăugat Ei sunt numărul jucătorului 1 Pat a fost adăugat Ei sunt numărul jucătorului 2 Sue a fost adăugat Ei sunt numărul de jucător 3 Chet este jucătorul curent Ei au rulat o nouă locație 4 Chet lui este 4 Categoria este Pop Pop Întrebare 0 Răspunsul a fost corrent !!!! Chet are acum 1 monedă de aur. Pat este actualul jucător Ei au rulat o nouă locație 2 Pat este de 2 Categoria este Sports Sports Question 0 Răspunsul a fost corrent !!!! Pat are acum 1 monedă de aur. Sue este actualul jucător Au rulat o locație nouă a lui 1 Sue este 1 Categoria este știință Știință Întrebare 0 Răspunsul a fost corrent !!!! Sue are acum 1 monedă de aur. Chet este jucătorul curent Au rulat un 4 # Unii linii eliminate pentru a păstra ## tutorialul la o dimensiune rezonabilă Răspunsul a fost corrent !!!! Sue are acum 5 monede de aur. Chet este jucătorul curent Ei au rulat un 3 Chet se iese din cutia de penalizare Chet noua locație este 11 Categoria este Rock Rock Întrebarea 5 Răspunsul a fost corect !!!! Chet are acum 5 monede de aur. Pat este actualul jucător Au rulat o locație nouă a lui 1 Pat este de 10 Categoria este Sports Sports Întrebare 1 Răspunsul a fost corrent !!!! Pat are acum 6 monede de aur.

O.K. Părerea noastră a fost corectă. Codul nostru a fugit și a produs unele rezultate. Analizând această ieșire ne vom ajuta să deducem o idee de bază despre ceea ce face codul.

  1. Știm că este un joc Trivia. Am știut-o când am verificat codul sursă.
  2. Exemplul nostru are trei jucători: Chet, Pat și Sue.
  3. Există un fel de rulare a unui zar sau a unui concept similar.
  4. Există o locație curentă pentru un jucător. Probabil pe un fel de consiliu?
  5. Există diferite categorii de întrebări.
  6. Utilizatorii răspund la întrebări.
  7. Răspunsurile corecte le oferă jucătorilor aur.
  8. Răspunsurile greșite trimit jucătorii la caseta de penalizare.
  9. Jucătorii pot ieși din caseta de penalizare, pe baza unor logici care nu sunt destul de clare.
  10. Se pare că utilizatorul obține mai întâi șase monede de aur câștigă.

Acum, aceasta este o mulțime de cunoștințe. S-ar putea să ne dăm seama de cea mai mare parte a comportamentului de bază al aplicației, doar căutând rezultatul. În aplicațiile din viața reală, este posibil ca ieșirea să nu fie text pe ecran, ci poate fi o pagină web, un jurnal de erori, o bază de date, o comunicare în rețea, un fișier de tip dump și așa mai departe. În alte cazuri, modulul pe care trebuie să îl modificați nu poate fi rulat izolat. Dacă da, va trebui să-l rulați prin alte module ale aplicației mai mari. Doar încercați să adăugați minimul, pentru a obține o ieșire rezonabilă din codul vechi.

Scanarea codului

Acum, că avem o idee despre ceea ce rezultă din cod, putem începe să ne uităm la ea. Vom începe cu alergătorul.

Jocul Runner

Îmi place să încep să rulez tot codul prin formatorul IDE-ului meu. Acest lucru îmbunătățește foarte mult lizibilitatea, făcând forma codului familiar cu ceea ce sunt obișnuit. Deci asta:

... va deveni acest lucru:

... care este ceva mai bun. Este posibil să nu fie o diferență enormă cu această cantitate mică de cod, dar va fi pe următorul fișier.

Privind la noi GameRunner.php , putem identifica cu ușurință câteva aspecte cheie pe care le-am observat în producție. Putem vedea liniile care adaugă utilizatorii (9-11), că este apelată o metodă roll () și că este selectat un câștigător. Desigur, acestea sunt departe de secretele interioare ale logicii jocului, dar cel puțin am putea începe prin identificarea unor metode cheie care ne vor ajuta să descoperim restul codului.

Fișierul de joc

Ar trebui să facem aceeași formatare pe Game.php fișierul de asemenea.

Acest fișier este mult mai mare; Aproximativ 200 de linii de cod. Majoritatea metodelor sunt dimensionate corespunzător, dar unele dintre ele sunt destul de mari și după formatare, putem observa că în două locuri, indentația codului depășește patru niveluri. Nivelurile ridicate de indentare înseamnă, de obicei, o mulțime de decizii complexe, deci pentru moment, putem presupune că acele puncte din codul nostru vor fi mai complexe și mai sensibile la schimbare.

Maestrul de Aur

Iar gândul schimbării ne conduce la lipsa de teste. Metodele pe care le-am văzut Game.php sunt destul de complexe. Nu vă faceți griji dacă nu le înțelegeți. În acest moment, ele sunt și un mister pentru mine. Codul vechi este un mister pe care trebuie să-l rezolvăm și să-l înțelegem. Am făcut primul nostru pas să îl înțelegem și acum este timpul celui de-al doilea.

Ce este acest Maestru de Aur??

Când lucrați cu codul vechi, este aproape imposibil să îl înțelegeți și să scrieți cod care va exercita cu siguranță toate căile logice prin cod. Pentru astfel de teste, ar trebui să înțelegem codul, dar nu încă. Așa că trebuie să luăm o altă abordare.

În loc să încercăm să ne dăm seama ce putem testa, putem testa totul, de multe ori, astfel încât să ajungem la o cantitate imensă de producție, despre care putem presupune cu siguranță că a fost produsă prin exercitarea tuturor părților moștenirii noastre cod. Se recomandă rularea codului de cel puțin 10.000 (zece mii) de ori. Vom scrie un test pentru a rula de două ori mai mult și pentru a salva rezultatul.

Scrierea Generatorului de Aur

Putem gândi înainte și începem prin crearea unui generator și a unui test ca fișiere separate pentru teste viitoare, dar este cu adevărat necesar? Nu știm încă asta. Deci, de ce nu începem doar cu un fișier de test de bază care ne va conduce codul o dată și vom construi logica noastră de acolo.

Veți găsi în arhiva codului atașat, în interiorul sursă dar în afara trivia dosarul nostru Test pliant. În acest director, creăm un fișier: GoldenMasterTest.php.

clasa GoldenMasterTest extinde PHPUnit_Framework_TestCase function testGenerateOutput () ob_start (); requ_once __DIR__. '/ ... / trivia/php/GameRunner.php'; $ ieșire = ob_get_contents (); ob_end_clean (); var_dump ($ output); 

Am putea face acest lucru în multe feluri. Am putea, de exemplu, să executăm codul nostru din consola și să redirecționăm ieșirea la un fișier. Cu toate acestea, având-o într-un test care este ușor de rula în IDE noastre este un avantaj nu ar trebui să ignorăm.

Codul este destul de simplu, el tamponează ieșirea și îl pune în $ ieșire variabil. require_once () va rula, de asemenea, tot codul din interiorul fișierului inclus. În vara noastră vom vedea o ieșire deja familiară.

Cu toate acestea, la oa doua rulare, putem observa ceva ciudat:

... ieșirile diferă. Chiar dacă am rulat același cod, rezultatul este diferit. Numerele rulate sunt diferite, pozițiile jucătorilor sunt diferite.

Semănarea Generatorului Randant

face $ aGame-> roll (rand (0, 5) + 1); dacă (rand (0, 9) == 7) $ notAWinner = $ aGame-> greșitAnswer ();  altceva $ notAWinner = $ aGame-> a fost corectdescărit ();  în timp ce ($ notAWinner);

Analizând codul esențial de la alergător, putem vedea că folosește o funcție rand () pentru a genera numere aleatorii. Următoarea noastră oprire este documentația oficială a PHP pentru a cerceta acest lucru rand () funcţie.

Generatorul de numere aleatoare este însămânțat automat.

Documentația ne spune că însămânțarea se întâmplă automat. Acum avem o altă sarcină. Trebuie să găsim o modalitate de a controla semințele. srand () funcția poate ajuta cu asta. Iată definiția sa din documentație.

Seamănă generatorul de numere aleatorii cu semințe sau cu valoare aleatorie dacă nu se dă semințe.

Ne spune că, dacă vom rula acest lucru înainte de orice apel la rand (), ar trebui întotdeauna să ajungem la aceleași rezultate.

funcția testGenerateOutput () ob_start (); srand (1); requ_once __DIR__. '/ ... / trivia/php/GameRunner.php'; $ ieșire = ob_get_contents (); ob_end_clean (); var_dump ($ output); 

Am pus srand (1) înainte de noi require_once (). Acum, ieșirea este întotdeauna aceeași.

Puneți ieșirea într-un fișier

clasa GoldenMasterTest extinde PHPUnit_Framework_TestCase function testGenerateOutput () fișier_put_contents ('/ tmp / gm.txt', $ this-> generateOutput ()); $ fișier_content = file_get_contents ('/ tmp / gm.txt'); $ this-> assertEquals ($ fișier_content, $ this-> generateOutput ());  funcția privată generateOutput () ob_start (); srand (1); requ_once __DIR__. '/ ... / trivia/php/GameRunner.php'; $ ieșire = ob_get_contents (); ob_end_clean (); returnați outputul $; 

Această schimbare pare rezonabilă. Dreapta? Am extras generarea codului într-o metodă, l-am executat de două ori și am așteptat ca ieșirea să fie egală. Cu toate acestea, nu vor fi.

Motivul este acela require_once () nu va solicita de două ori același fișier. Al doilea apel către generateOutput () metoda va produce un șir gol. Deci, ce am putea face? Dacă doar noi require ()? Acest lucru ar trebui să fie executat de fiecare dată.

Asta duce la o altă problemă: "Nu se poate redeclora ecoln ()". Dar de unde vine asta? Este chiar la începutul anului Game.php fişier. Motivul pentru care apare această eroare se datorează faptului că în GameRunner.php noi avem include __DIR__. '/Game.php';, care încearcă să includă fișierul de joc de două ori, de fiecare dată când sunăm generateOutput () metodă.

include_once __DIR__. '/Game.php';

Utilizarea include_once în GameRunner.php ne va rezolva problema. Da, trebuie să modificăm GameRunner.php fără a avea teste pentru el, totuși! Cu toate acestea, putem fi 99% siguri că schimbarea noastră nu va sparge codul în sine. Este o schimbare suficient de mică și simplă pentru a nu ne speria foarte mult. Și, cel mai important, face ca testele să treacă.

Fugiți-o de mai multe ori

Acum, că avem codul, putem rula de mai multe ori, este timpul să generăm o ieșire.

funcția testGenerateOutput () $ this-> generateMany (20, '/tmp/gm.txt'); $ this-> generateMany (20, '/tmp/gm2.txt'); $ fișier_content_gm = file_get_contents ('/ tmp / gm.txt'); $ file_content_gm2 = file_get_contents ('/ tmp / gm2.txt'); $ this-> assertEquals ($ fișier_content_gm, $ file_content_gm2);  funcția privată generateMany ($ times, $ fileName) $ first = true; în timp ce ($ ori) if ($ first) file_put_contents ($ fileName, $ this-> generateOutput ()); $ first = false;  altceva file_put_contents ($ fileName, $ this-> generateOutput (), FILE_APPEND);  ori; 

Am extras o altă metodă aici: generateMany (). Are doi parametri. Unul pentru numărul de ori pe care dorim să-l executăm, celălalt este un fișier de destinație. Acesta va pune ieșirea generată în fișiere. La prima execuție, se eliberează fișierele, iar pentru celelalte iterații se adaugă datele. Puteți examina fișierul pentru a vedea rezultatul generat de 20 de ori.

Dar asteapta! Același jucător câștigă de fiecare dată? Este posibil?

cat /tmp/gm.txt | grep "are 6 monede de aur." Chet are acum 6 monede de aur. Chet are acum 6 monede de aur. Chet are acum 6 monede de aur. Chet are acum 6 monede de aur. Chet are acum 6 monede de aur. Chet are acum 6 monede de aur. Chet are acum 6 monede de aur. Chet are acum 6 monede de aur. Chet are acum 6 monede de aur. Chet are acum 6 monede de aur. Chet are acum 6 monede de aur. Chet are acum 6 monede de aur. Chet are acum 6 monede de aur. Chet are acum 6 monede de aur. Chet are acum 6 monede de aur. Chet are acum 6 monede de aur. Chet are acum 6 monede de aur. Chet are acum 6 monede de aur. Chet are acum 6 monede de aur. Chet are acum 6 monede de aur.

Da! Este posibil! Este mai mult decât posibil. Este un lucru sigur. Avem aceeași sămânță pentru funcția aleatorie. Jucam acelasi joc mereu si repetat.

Fugiți-o diferit de fiecare dată

Trebuie să jucăm diferite jocuri, în caz contrar este aproape sigur că doar o mică parte din codul nostru moștenit este efectiv exercitat de multe ori. Scopul maestrului de aur este să exercite cât mai mult posibil. Trebuie să relansăm generatorul aleatoriu de fiecare dată, dar într-un mod controlat. O opțiune este utilizarea contorului nostru ca valoare a semințelor.

funcția privată generateMany ($ times, $ fileName) $ first = true; în timp ce ($ ori) if ($ first) file_put_contents ($ fileName, $ this-> generateOutput ($ times)); $ first = false;  altceva file_put_contents ($ fileName, $ this-> generateOutput ($ times), FILE_APPEND);  ori;  funcția privată generateOutput ($ seed) ob_start (); srand ($ semințe); solicită __DIR__. '/ ... / trivia/php/GameRunner.php'; $ ieșire = ob_get_contents (); ob_end_clean (); returnați outputul $; 

Acest lucru continuă să treacă testul, deci suntem siguri că generăm aceeași ieșire completă de fiecare dată, în timp ce ieșirea joacă un joc diferit pentru fiecare iterație.

cat /tmp/gm.txt | grep "are 6 monede de aur." Sue are acum 6 monede de aur. Chet are acum 6 monede de aur. Chet are acum 6 monede de aur. Chet are acum 6 monede de aur. Chet are acum 6 monede de aur. Pat are acum 6 monede de aur. Pat are acum 6 monede de aur. Chet are acum 6 monede de aur. Chet are acum 6 monede de aur. Sue are acum 6 monede de aur. Chet are acum 6 monede de aur. Chet are acum 6 monede de aur. Sue are acum 6 monede de aur. Chet are acum 6 monede de aur. Sue are acum 6 monede de aur. Chet are acum 6 monede de aur. Chet are acum 6 monede de aur. Pat are acum 6 monede de aur. Chet are acum 6 monede de aur. Chet are acum 6 monede de aur.

Există diverși câștigători pentru joc în mod aleatoriu. Asta arată bine.

Până la 20.000

Primul lucru pe care îl puteți încerca este să rulați codul nostru pentru iterații de joc de 20.000.

funcția testGenerateOutput () $ times = 20000; $ this-> generateMany ($ ori, '/tmp/gm.txt'); $ this-> generateMany ($ ori, '/tmp/gm2.txt'); $ fișier_content_gm = file_get_contents ('/ tmp / gm.txt'); $ file_content_gm2 = file_get_contents ('/ tmp / gm2.txt'); $ this-> assertEquals ($ fișier_content_gm, $ file_content_gm2); 

Acest lucru va funcționa aproape. Două fișiere de 55MB vor fi generate.

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

Pe de altă parte, testul va eșua cu o eroare de memorie insuficientă. Nu contează cât de mult aveți RAM, acest lucru va eșua. Am 8GB plus un swap de 4GB și nu reușește. Cele două șiruri sunt prea mari pentru a fi comparate în afirmația noastră.

Cu alte cuvinte, generăm fișiere bune, dar PHPUnit nu le poate compara. Avem nevoie de o slujbă.

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

Acesta pare a fi un bun candidat, dar încă nu reușește. Ce păcat. Trebuie să cercetăm mai departe situația.

$ this-> assertTrue ($ fișier_content_gm == $ file_content_gm2);

Cu toate acestea, funcționează.

Se poate compara cele două șiruri și eșua dacă sunt diferite. Cu toate acestea, are un preț mic. Nu va fi capabil să spună exact ce este greșit atunci când șirurile diferă. Pur și simplu va spune "Nu a reușit să afirm că adevărul este fals".. Dar vom aborda acest lucru într-un tutorial viitoare.

Gândurile finale

Am terminat pentru acest tutorial. Am învățat destul de mult pentru prima noastră lecție și suntem într-un bun început pentru activitatea noastră viitoare. Am întâlnit codul, l-am analizat în moduri diferite și am înțeles mai ales logica sa esențială. Apoi am creat un set de teste pentru a ne asigura că este exercitată cât mai mult posibil. Da. Testele sunt foarte lente. Le ia 24 de secunde pe procesorul Core i7 pentru a genera de două ori ieșirea. Din fericire în dezvoltarea noastră viitoare, vom păstra gm.txt fișierul neatins și generează altul o singură dată pe rulare. Dar 12 secunde este încă o cantitate foarte mare de timp pentru o bază de coduri atât de mică.

Până când terminăm această serie, testele noastre ar trebui să ruleze în mai puțin de o secundă și să testeze toate codurile în mod corespunzător. Asadar, fiti atenti la urmatorul tutorial atunci cand vom aborda probleme cum ar fi constante magice, siruri magice si conditii complexe. Vă mulțumim pentru lectură.

Cod