Refactoring Code Legacy Partea 9 - Analiza preocupărilor

În acest tutorial, vom continua să ne concentrăm pe logica noastră de afaceri. Vom evalua dacă RunnerFunctions.php aparține unei clase și, dacă da, la ce clasă? Ne vom gândi la preocupări și unde apar metode. În cele din urmă, vom învăța ceva mai mult despre conceptul de batjocură. Deci ce mai aștepți? Citește mai departe.


RunnerFunctions - De la procedură la obiect orientat

Chiar dacă avem cea mai mare parte a codului nostru în formă orientată obiect, bine organizat în clase, unele funcții sunt pur și simplu așezate într-un fișier. Trebuie să luăm câteva pentru a da funcțiile RunnerFunctions.php într-un aspect orientat mai mult asupra obiectelor.

const WRONG_ANSWER_ID = 7; const MIN_ANSWER_ID = 0; const MAX_ANSWER_ID = 9; funcția esteCurrentAnswerCorrect ($ minAnswerId = MIN_ANSWER_ID, $ maxAnswerId = MAX_ANSWER_ID) return rand ($ minAnswerId, $ maxAnswerId)! = WRONG_ANSWER_ID;  funcția execută () $ display = nou CLIDisplay (); $ aGame = Joc nou (afișare $); $ AGame-> adaugă ( "Chet"); $ AGame-> adaugă ( "Pat"); $ AGame-> add ( "Sue"); face $ dice = rand (0, 5) + 1; $ AGame-> rola ($ zaruri);  în timp ce (! didSomebodyWin ($ aGame, isCurrentAnswerCorrect ()));  funcția didSomebodyWin ($ aGame, $ isCurrentAnswerCorrect) if ($ isCurrentAnswerCorrect) retur! $ AGame-> wasCorrectlyAnswered ();  altceva retur! $ AGame-> wrongAnswer (); 

Primul meu instinct este să-i împachetez într-o clasă. Acesta nu este nimic genial, dar este ceva care ne face să începem să schimbăm lucrurile. Să vedem dacă ideea poate funcționa.

const WRONG_ANSWER_ID = 7; const MIN_ANSWER_ID = 0; const MAX_ANSWER_ID = 9; clasa RunnerFunctions funcția isCurrentAnswerCorrect ($ minAnswerId = MIN_ANSWER_ID, $ maxAnswerId = MAX_ANSWER_ID) return rand ($ minAnswerId, $ maxAnswerId)! = WRONG_ANSWER_ID;  funcția execută () // ... // funcția didSomebodyWin ($ aGame, $ isCurrentAnswerCorrect) // ... //

Dacă facem asta, trebuie să modificăm testele noastre și pe cele ale noastre GameRunner.php pentru a folosi noua clasă. Am numit clasa ceva generic pentru moment, redenumirea va fi ușor atunci când este necesar. Nici măcar nu știm dacă această clasă va exista pe cont propriu sau va fi asimilată Joc. Deci, nu vă faceți griji cu privire la numirea încă.

funcția privată generateOutput ($ seed) ob_start (); srand ($ semințe); (noi RunnerFunctions ()) -> run (); $ ieșire = ob_get_contents (); ob_end_clean (); returnați outputul $; 

În a noastră GoldenMasterTest.php fișier, trebuie să modificăm modul în care executăm codul nostru. Funcția este generateOutput () iar a treia linie trebuie modificată pentru a crea un obiect nou și pentru a apela alerga() pe el. Dar acest lucru nu reușește.

Eroare eroare fatală: Apel la funcția nedefinită didSomebodyWin () în ... 

Acum trebuie să ne modificăm noua clasă.

face $ dice = rand (0, 5) + 1; $ AGame-> rola ($ zaruri);  în timp ce (! $ this-> didSomebodyWin ($ aGame, $ this-> isCurrentAnswerCorrect ()));

A trebuit doar să schimbăm starea in timp ce declarație în alerga() metodă. Noul cod solicită didSomebodyWin () și isCurrentAnswerCorrect () din clasa curentă, prin predare $ This-> lor.

Aceasta face permisul de masterat de aur, dar frânează testele alergătorului.

PHP Eroare fatală: Apelați la funcția nedefinită isCurrentAnswerCorrect () în /.../RunnerFunctionsTest.php on line 25

Problema este în assertAnswersAreCorrectFor (), dar ușor de fixat prin crearea unui obiect alergător în primul rând.

funcția privată assertAnswersAreCorrectFor ($ correctAnserIDs) $ runner = new RunnerFunctions (); foreach ($ corectAnserIDs ca $ id) $ this-> assertTrue ($ runner-> isCurrentAnswerCorrect ($ id, $ id)); 

Aceeași problemă trebuie abordată și în alte trei funcții.

funcția testItCanFindWrongAnswer () $ runner = new RunnerFunctions (); $ this-> assertFalse ($ alergător-> isCurrentAnswerCorrect (WRONG_ANSWER_ID, WRONG_ANSWER_ID));  functie testItCanTellIfThereIsNoWinnerWhenAcorrectAnswerIsProvided () $ runner = noi RunnerFunctions (); $ this-> assertTrue ($ runner-> didSomebodyWin ($ această-> aFakeGame (), $ this-> aCorrectAnswer ()));  funcția testItCanTellIfThereIsNoWinnerWhenAWrongAnswerIsProvided () $ runner = noi RunnerFunctions (); $ this-> assertFalse ($ runner-> a făcutSomebodyWin ($ this-> aFakeGame (), $ this-> aWrongAnswer ())); 

În timp ce acest lucru face ca codul să treacă, introduce un pic de dublare a codului. Așa cum suntem acum cu toate testele pe verde, putem extrage crearea runner într-o înființat() metodă.

$ runner privat; funcția setUp () $ this-> runner = nou Runner ();  funcția testItCanFindCorrectAnswer () $ this-> assertAnswersAreCorrectFor ($ this-> getCorrectAnswerIDs ());  funcția testItCanFindWrongAnswer () $ this-> assertFalse ($ this-> runner-> isCurrentAnswerCorrect (WRONG_ANSWER_ID, WRONG_ANSWER_ID));  functie testItCanTellIfThereIsNoWinnerWhenAcorrectAnswerIsProvided () $ this-> assertTrue ($ aceasta-> alergator-> didSomebodyWin ($ this-> aFakeGame (), $ this-> aCorrectAnswer ()));  funcția testItCanTellIfThereIsNoWinnerWhenAWrongAnswerIsProvided () $ this-> assertFalse ($ this-> runner-> didSomebodyWin ($ this-> aFakeGame (), $ this-> aWrongAnswer ()));  funcția privată assertAnswersAreCorrectFor ($ correctAnserIDs) foreach ($ correctAnserIDs ca $ id) $ this-> assertTrue ($ this-> runner-> isCurrentAnswerCorrect ($ id, $ id)); 

Frumos. Toate aceste noi creații și refactorizări m-au făcut să mă gândesc. Am numit variabila noastră alergător. Poate că clasa noastră ar putea fi numită la fel. Să o refacem. Ar trebui sa fie usor.

Dacă nu ați verificat "Căutați apariții de text"în caseta de mai sus, nu uitați să schimbați manualul, deoarece refactoringul va redenumi și fișierul.

Acum avem un fișier numit GameRunner.php, altul numit Runner.php și un al treilea numit Game.php. Nu știu despre tine, dar mi se pare extrem de confuz. Dacă aș vrea să văd aceste trei fișiere pentru prima dată în viața mea, nu aș avea idee ce se întâmplă. Trebuie să scăpăm de cel puțin unul dintre ei.

Motivul pentru care am creat RunnerFunctions.php fișier în stadiile incipiente ale refactorizării noastre a fost să construim o modalitate de a include toate metodele și fișierele de testare. Aveam nevoie de acces la tot, dar nu fugi de tot, decât în ​​mediul pregătit în maestrul nostru de aur. Încă putem face același lucru, doar nu ne execută codul GameRunner.php. Trebuie să actualizăm includerea și să creăm o clasă înăuntru, înainte de a continua.

requ_once __DIR__. '/Display.php'; requ_once __DIR__. '/Runner.php'; (noul Runner ()) -> run ();

Asta o va face. Trebuie să includem Display.php în mod explicit, atunci când alergător încearcă să creeze un nou CLIDisplay, va ști ce să pună în aplicare.


Analizând preocupările

Cred că una dintre cele mai importante caracteristici ale programării orientate obiect este definirea preocupărilor. Intotdeauna imi pun intrebari cum ar fi: "este aceasta clasa sa faca ceea ce spune numele ei?", "Este aceasta metoda de ingrijorare pentru acest obiect?", "Daca obiectul meu ar pasa de acea valoare specifica?"

Surprinzător, aceste tipuri de întrebări au o mare putere de a clarifica atât domeniul de afaceri, cât și arhitectura software. Solicităm și răspunem la aceste tipuri de întrebări într-un grup de la Syneto. De multe ori când un programator are o dilemă, el sau ea se ridică doar în picioare, cere două minute de atenție din partea echipei pentru a găsi opinia noastră cu privire la un subiect. Cei care sunt familiarizați cu arhitectura codului vor răspunde din punct de vedere al software-ului, în timp ce alții, mai familiarizați cu domeniul afacerilor, pot arunca o lumină asupra unor informații esențiale despre aspectele comerciale.

Să încercăm să ne gândim la preocupări în cazul nostru. Putem să ne concentrăm în continuare pe alergător clasă. Este mult mai probabil să eliminați sau să transformați această clasă decât Joc.

În primul rând, un alergător trebuie să aibă grijă de cum isCurrentAnswerCorrect () lucru? Dacă un alergător are cunoștințe despre întrebări și răspunsuri?

Se pare că această metodă ar fi mai bine înăuntru Joc. Cred cu tărie că a Joc despre trivia ar trebui să aibă grijă dacă un răspuns este corect sau nu. Cred cu adevărat a Joc trebuie să fie preocupat de furnizarea rezultatului răspunsului la întrebarea curentă.

E timpul să acționăm. Vom face asta muta metoda refactorizare. După cum am văzut toate astea înainte de tutorialele mele anterioare, vă voi arăta doar rezultatul final.

requ_once __DIR__. '/CLIDisplay.php'; include_once __DIR__. '/Game.php'; clasa Runner function run () // ... // didSomebodyWin ($ aGame, $ isCurrentAnswerCorrect) // ... //

Este esențial să rețineți că nu numai metoda a dispărut, dar și constanța care definește limitele răspunsului.

Dar ce zici didSomebodyWin ()? Dacă un alergător decide când cineva a câștigat? Dacă ne uităm la corpul metodei, vedem o problemă subliniind ca o lanternă în întuneric.

funcția didSomebodyWin ($ aGame, $ isCurrentAnswerCorrect) if ($ isCurrentAnswerCorrect) întoarcere! $ aGame-> a fost corectdescărit ();  altceva return! $ aGame-> wrongAnswer (); 

Oricare ar fi această metodă, o face pe a Joc obiect numai. Verifică răspunsul curent returnat de joc. Apoi se întoarce tot ce se întoarce un obiect de joc wasCorrectlyAnswered () sau răspuns greșit() metode. Această metodă nu face nimic în mod efectiv pe cont propriu. Tot ce îi pasă este Joc. Acesta este un exemplu clasic de miros codat Feature Envy. O clasă face ceva ce ar trebui să facă o altă clasă. E timpul să-l mutați.

clasa RunnerFunctionsTest extinde PHPUnit_Framework_TestCase private $ runner; funcția setUp () $ this-> runner = nou Runner (); 

Ca de obicei, am mutat mai întâi testele. TDD? Oricine?

Acest lucru ne lasă să nu mai avem alte teste, așa că acest fișier poate merge acum. Ștergerea este partea mea preferată de programare.

Iar când executăm testele noastre, avem o eroare frumoasă.

Eroare fatală: Apel la metoda nedefinită Joc :: didSomebodyWin ()

Acum este momentul sa schimbati si codul. Copierea și lipirea metodei în Joc va face magic să treacă toate testele. Atât cei bătrâni, cât și cei care s-au mutat GameTest. Dar, în timp ce acest lucru pune metoda în locul potrivit, are două probleme: alergătorul trebuie, de asemenea, să fie schimbat și trimitem un fals Joc obiect pe care nu mai trebuie să-l facem din moment ce face parte din Joc.

face $ dice = rand (0, 5) + 1; $ AGame-> rola ($ zaruri);  în timp ce (! $ aGame-> didSomebodyWin ($ aGame, $ this-> isCurrentAnswerCorrect ()));

Fixarea alergătorului este foarte ușoară. Tocmai ne schimbăm $ this-> didSomebodyWin (...) în $ aGame-> didSomebodyWin (...). Vom reveni aici și vom schimba din nou, după pasul următor. Refactorizarea testului.

funcția testItCanTellIfThereIsNoWinnerWhenAcorrectAnswerIsProvided () $ aGame = \ Mockery :: mock ('Joc [wasCorrectlyAnswered]'); $ AGame-> shouldReceive ( 'wasCorrectlyAnswered') -> o dată () -> andReturn (false); $ This-> assertTrue ($ aGame-> didSomebodyWin ($ this-> aCorrectAnswer ())); 

Este timpul pentru unii batjocori! În loc să folosim clasa noastră falsă, definită la sfârșitul testelor noastre, vom folosi Mockery. Ne permite să înlocuim cu ușurință o metodă Joc, așteptați ca acesta să fie chemat și să returnați valoarea dorită. Desigur, am reușit acest lucru făcând ca clasa noastră falsă să se extindă Joc și înlocuim noi înșine metoda. Dar de ce face un loc de muncă pentru care există un instrument?

function testItCanTellIfThereIsNoWinnerWhenAWrongAnswerIsProvided () $ aGame = \ Mockery :: fals ('Game [wrongAnswer]'); $ AGame-> shouldReceive ( 'wrongAnswer') -> o dată () -> andReturn (true); $ This-> assertFalse ($ aGame-> didSomebodyWin ($ this-> aWrongAnswer ())); 

După ce a doua metodă este rescrisă, putem să scăpăm de clasa de joc falsă și de toate metodele care l-au inițializat. Problemele rezolvate!

Gândurile finale

Chiar dacă am reușit să ne gândim doar la alergător, am făcut mari progrese astăzi. Am aflat despre responsabilități, am identificat metode și variabile care aparțin unei alte clase. Ne-am gândit la un nivel superior și am evoluat spre o soluție mai bună. În echipa Syneto, există o convingere puternică că există modalități de a scrie bine codul și de a nu efectua o schimbare decât dacă a făcut codul cel puțin puțin mai curat. Aceasta este o tehnică care, în timp, poate duce la o bază de coduri mult mai frumoasă, cu mai puține dependențe, mai multe teste și în cele din urmă mai puține bug-uri.

Multumesc pentru timpul acordat.

Cod