Refactoring Legacy Code Del 9 - Analyse Bekymringer

I denne opplæringen vil vi fortsette å fokusere på vår forretningslogikk. Vi vil vurdere om RunnerFunctions.php tilhører en klasse og i så fall hvilken klasse? Vi vil tenke på bekymringer og hvor metodene hører til. Til slutt lærer vi litt mer om begrepet mocking. Så, hva venter du på? Les videre.


RunnerFunctions - Fra prosedyre til objektorientert

Selv om vi har mesteparten av koden vår i objektorientert form, pent organisert i klasser, er enkelte funksjoner bare å sitte i en fil. Vi må ta litt for å gi funksjonene RunnerFunctions.php i et mer objektorientert aspekt.

const WRONG_ANSWER_ID = 7; const MIN_ANSWER_ID = 0; const MAX_ANSWER_ID = 9; funksjonen er CurrentAnswerCorrect ($ minAnswerId = MIN_ANSWER_ID, $ maxAnswerId = MAX_ANSWER_ID) return rand ($ minAnswerId, $ maxAnswerId)! = WRONG_ANSWER_ID;  funksjonsdrift () $ display = nytt CLIDisplay (); $ aGame = nytt spill ($ display); $ AGame-> legge til ( "Chet"); $ AGame-> legge til ( "Pat"); $ AGame-> legge til ( "Sue"); gjør $ terning = rand (0, 5) + 1; $ AGame-> roll ($ terninger);  mens (! gjordeSomebodyWin ($ aGame, isCurrentAnswerCorrect ()));  funksjon didSomebodyWin ($ aGame, $ isCurrentAnswerCorrect) hvis ($ erCurrentAnswerCorrect) return! $ AGame-> wasCorrectlyAnswered ();  ellers return! $ AGame-> wrongAnswer (); 

Mitt første instinkt er å bare pakke dem inn i en klasse. Dette er ingenting geni, men det er noe som gjør at vi begynner å skifte ting. La oss se om ideen faktisk kan fungere.

const WRONG_ANSWER_ID = 7; const MIN_ANSWER_ID = 0; const MAX_ANSWER_ID = 9; klasse RunnerFunctions funksjonen erCurrentAnswerCorrect ($ minAnswerId = MIN_ANSWER_ID, $ maxAnswerId = MAX_ANSWER_ID) return rand ($ minAnswerId, $ maxAnswerId)! = WRONG_ANSWER_ID;  funksjonstrykk () // ... // funksjonen gjordeSomebodyWin ($ aGame, $ isCurrentAnswerCorrect) // ... //

Hvis vi gjør det, må vi endre våre tester og vår GameRunner.php å bruke den nye klassen. Vi kalte klassen noe generisk for øyeblikket, omdøpe det vil være enkelt når det trengs. Vi vet ikke engang om denne klassen vil eksistere alene eller vil bli assimilert i Spill. Så ikke bekymre deg om navngivning ennå.

privat funksjon generereOutput ($ seed) ob_start (); srand ($ frø); (nye RunnerFunctions ()) -> run (); $ output = ob_get_contents (); ob_end_clean (); returnere $ output; 

I vår GoldenMasterTest.php fil, må vi endre måten vi kjører vår kode på. Funksjonen er generateOutput () og den tredje linjen må endres for å opprette et nytt objekt og ringe løpe() på den. Men dette mislykkes.

PHP Fatal feil: Ring til udefinert funksjon didSomebodyWin () i ... 

Vi må nå endre vår nye klasse videre.

gjør $ terning = rand (0, 5) + 1; $ AGame-> roll ($ terninger);  mens (! $ this-> gjordeSomebodyWin ($ aGame, $ this-> isCurrentAnswerCorrect ()));

Vi behøvde bare å endre tilstanden til samtidig som uttalelse i løpe() metode. Den nye koden kaller didSomebodyWin () og isCurrentAnswerCorrect () fra nåværende klasse, ved å forhåndsbehandle $ Dette-> til dem.

Dette gjør det gyldne mesterpasset, men det bremser løpestestene.

PHP Fatal feil: Ring til udefinert funksjon isCurrentAnswerCorrect () i / ... /RunnerFunctionsTest.php på linje 25

Problemet er i assertAnswersAreCorrectFor (), men det er lett å fikse ved å lage et løperobjekt først.

privat funksjon assertAnswersAreCorrectFor ($ correctAnserIDs) $ runner = new RunnerFunctions (); foreach ($ correctAnserIDs som $ id) $ this-> assertTrue ($ runner-> isCurrentAnswerCorrect ($ id, $ id)); 

Det samme problemet må også tas opp i tre andre funksjoner.

funksjon testItCanFindWrongAnswer () $ runner = new RunnerFunctions (); $ this-> assertFalse ($ runner-> isCurrentAnswerCorrect (WRONG_ANSWER_ID, WRONG_ANSWER_ID));  funksjonstestItCanTellIfThereIsNoWinnerWhenACorrectAnswerIsProvided () $ runner = new RunnerFunctions (); $ this-> assertTrue ($ runner-> didSomebodyWin ($ this-> aFakeGame (), $ this-> aCorrectAnswer ()));  funksjon testItCanTellIfThereIsNoWinnerWhenAWrongAnswerIsProvided () $ runner = new RunnerFunctions (); $ this-> assertFalse ($ runner-> didSomebodyWin ($ this-> aFakeGame (), $ this-> aWrongAnswer ())); 

Mens dette gjør koden passerer, introduserer den litt kod duplisering. Som vi nå er med alle tester på grønt, kan vi trekke ut løperopprettelsen til en SETUP () metode.

privat $ løper; funksjon setUp () $ this-> runner = new Runner ();  funksjon testItCanFindCorrectAnswer () $ this-> assertAnswersAreCorrectFor ($ this-> getCorrectAnswerIDs ());  funksjon testItCanFindWrongAnswer () $ this-> assertFalse ($ this-> runner-> isCurrentAnswerCorrect (WRONG_ANSWER_ID, WRONG_ANSWER_ID));  funksjon testItCanTellIfThereIsNoWinnerWhenCorrectAnswerIsProvided () $ this-> assertTrue ($ this-> runner-> didSomebodyWin ($ this-> aFakeGame (), $ this-> aCorrectAnswer ()));  funksjonstestItCanTellIfThereIsNoWinnerWhenAWrongAnswerIsProvided () $ this-> assertFalse ($ this-> runner-> didSomebodyWin ($ this-> aFakeGame (), $ this-> aWrongAnswer ()));  privat funksjon assertAnswersAreCorrectFor ($ correctAnserIDs) foreach ($ correctAnserIDs som $ id) $ this-> assertTrue ($ this-> runner-> isCurrentAnswerCorrect ($ id, $ id)); 

Hyggelig. Alle disse nye kreasjoner og refactorings fikk meg til å tenke. Vi heter vår variabel løper. Kanskje vår klasse kunne bli kalt det samme. La oss reflektere det. Det skal være enkelt.

Hvis du ikke sjekket "Søk etter tekst hendelser"i boksen over, ikke glem å endre din innbefattet manuelt, fordi refactoring vil omdøpe filen også.

Nå har vi en fil som heter GameRunner.php, en annen som heter Runner.php og en tredje som heter Game.php. Jeg vet ikke om deg, men dette virker ekstremt forvirrende for meg. Hvis jeg skulle se disse tre filene for første gang i mitt liv, ville jeg ikke ane hvilken som gjør hva. Vi trenger å kvitte seg med minst en av dem.

Grunnen til at vi opprettet RunnerFunctions.php fil i de tidlige stadiene av vår refactoring, var å bygge opp en måte å inkludere alle metoder og filer for testing. Vi trengte tilgang til alt, men ikke kjøre alt med mindre i et forberedt miljø i vår gyldne mester. Vi kan fortsatt gjøre det samme, bare ikke kjør vår kode fra GameRunner.php. Vi må oppdatere inklusiv og lage en klasse inne, før vi fortsetter.

require_once __DIR__. '/Display.php'; require_once __DIR__. '/Runner.php'; (ny Runner ()) -> run ();

Det vil gjøre det. Vi må inkludere Display.php eksplisitt, så når Løper prøver å lage en ny CLIDisplay, det vil vite hva du skal implementere.


Analysere bekymringer

Jeg tror at en av de viktigste egenskapene ved objektorientert programmering er å definere bekymringer. Jeg stiller meg alltid spørsmål som "gjør denne klassen hva navnet heter?", "Er denne metoden for bekymring for dette objektet?", "Skal objektet bry seg om den spesifikke verdien?"

Overraskende nok har disse typene spørsmål en god makt i å klargjøre både forretningsdomenet og programvarearkitekturen. Vi spør og svarer på disse typer spørsmål i en gruppe på Syneto. Mange ganger når en programmerer har et dilemma, står han eller hun bare opp, spør om to minutter med oppmerksomhet fra teamet for å finne vår mening om et emne. De som er kjent med kodearkitekturen, vil svare fra et programvaresynspunkt, mens andre som er mer kjent med bedriftsdomenet kan kaste lys over noen vesentlige innsikter om kommersielle aspekter.

La oss prøve å tenke på bekymringer i vårt tilfelle. Vi kan fortsette å fokusere på Løper klasse. Det er enormt mer sannsynlig å eliminere eller forvandle denne klassen enn Spill.

Først bør en runner bryr seg om hvordan isCurrentAnswerCorrect () arbeider? Skulle en løper ha kunnskap om spørsmål og svar?

Det virker som om denne metoden ville være bedre i Spill. Jeg tror sterkt at a Spill om trivia bør bryr seg om et svar er riktig eller ikke. Jeg tror virkelig en Spill må være opptatt av å gi resultatet av svaret på det nåværende spørsmålet.

Det er på tide å handle. Vi skal gjøre en flytte metode refactoring. Som vi har sett dette alt før fra mine tidligere opplæringsprogrammer, vil jeg bare vise deg sluttresultatet.

require_once __DIR__. '/CLIDisplay.php'; include_once __DIR__. '/Game.php'; class Runner function run () // ... // funksjonen gjordeSomebodyWin ($ aGame, $ isCurrentAnswerCorrect) // ... //

Det er viktig å merke seg at ikke bare metoden gikk bort, men den konstante definerer svarets grenser også.

Men hva med didSomebodyWin ()? Skal en løper bestemme når noen har vunnet? Hvis vi ser på metodenes kropp, kan vi se et problem som fremhever som en lommelykt i mørket.

funksjon didSomebodyWin ($ aGame, $ isCurrentAnswerCorrect) hvis ($ erCurrentAnswerCorrect) return! $ aGame-> wasCorrectlyAnswered ();  ellers return! $ aGame-> wrongAnswer (); 

Uansett hva denne metoden gjør, gjør det på en Spill bare objekt. Det bekrefter det nåværende svaret som returneres av spillet. Deretter returnerer det hva et spillobjekt returnerer i sin wasCorrectlyAnswered () eller feil svar() metoder. Denne metoden gjør ikke noe i seg selv. Alt det bryr seg om er Spill. Dette er et klassisk eksempel på en kode lukt kalt Feature Envy. En klasse gjør noe som en annen klasse skal gjøre. Tid til å flytte den.

klasse RunnerFunctionsTest utvider PHPUnit_Framework_TestCase private $ runner; funksjon setUp () $ this-> runner = new Runner (); 

Som vanlig flyttet vi testene først. TDD? Hvem som helst?

Dette etterlater oss uten flere tester for å løpe, så denne filen kan gå nå. Slette er min favoritt del av programmeringen.

Og når vi kjører testene våre, får vi en fin feil.

Fatal feil: Ring til udefinert metode Game :: didSomebodyWin ()

Det er nå på tide å endre koden også. Kopier og lim inn metoden i Spill vil gjøre alle testene magisk. Både de gamle og de flyttet til GameTest. Men mens dette setter metoden på rett sted, har det to problemer: løperen må også endres og vi sender inn en falsk Spill objekt som vi ikke trenger å gjøre lenger siden det er en del av Spill.

gjør $ terning = rand (0, 5) + 1; $ AGame-> roll ($ terninger);  mens (! $ aGame-> gjordeSomebodyWin ($ aGame, $ this-> isCurrentAnswerCorrect ()));

Å fikse løperen er veldig enkelt. Vi endrer bare $ this-> didSomebodyWin (...) inn i $ aGame-> didSomebodyWin (...). Vi må komme tilbake hit og endre det igjen, etter vårt neste skritt. Testrefactoring.

funksjon testItCanTellIfThereIsNoWinnerWhenACorrectAnswerIsProvided () $ aGame = \ Mockery :: mock ('Spill [varKorrigertAnswered]'); $ AGame-> shouldReceive ( 'wasCorrectlyAnswered') -> én gang () -> andReturn (false); $ Dette-> assertTrue ($ aGame-> didSomebodyWin ($ dette-> aCorrectAnswer ())); 

Det er på tide for noen mocking! I stedet for å bruke vår falske klasse, definert på slutten av testene, bruker vi Mockery. Det lar oss enkelt overskrive en metode på Spill, Forvent at det skal ringes og returnere verdien vi ønsker. Selvfølgelig kan vi til dette ved å gjøre vår falske klasse utvide Spill og skriv over metoden selv. Men hvorfor gjør en jobb som et verktøy eksisterer for?

funksjon testItCanTellIfThereIsNoWinnerWhenAWrongAnswerIsProvided () $ aGame = \ Mockery :: mock ('Game [wrongAnswer]'); $ AGame-> shouldReceive ( 'wrongAnswer') -> én gang () -> andReturn (true); $ Dette-> assertFalse ($ aGame-> didSomebodyWin ($ dette-> aWrongAnswer ())); 

Etter at vår andre metode er omskrevet, kan vi kvitte seg med den falske spillklassen og alle metoder som initialiserte den. Problemene løst!

Siste tanker

Selv om vi klarte å tenke på bare Løper, Vi har gjort store fremskritt i dag. Vi lærte om ansvar, vi identifiserte metoder og variabler som tilhører en annen klasse. Vi tenkte på et høyere nivå, og vi utviklet seg mot en bedre løsning. I Syneto-teamet er det en sterk tro på at det finnes måter å skrive kode godt på, og aldri forplikte seg, med mindre det gjorde koden minst litt renere. Dette er en teknikk som med tiden kan føre til en mye bedre kodebase, med mindre avhengigheter, flere tester og til slutt mindre bugs.

Takk for tiden din.