Refactoring Legacy Code Del 9 - Analysera Bekymmer

I denna handledning fortsätter vi att fokusera på vår affärslogik. Vi kommer att utvärdera om RunnerFunctions.php tillhör en klass och i så fall till vilken klass? Vi kommer att tänka på bekymmer och där metoder hör hemma. Slutligen lär vi oss lite mer om begreppet mocking. Så vad väntar du på? Läs vidare.


RunnerFunctions - Från processorienterad till objektorienterad

Även om vi har det mesta av vår kod i objektorienterad form, snyggt organiserad i klasser, sitter vissa funktioner helt enkelt helt enkelt i en fil. Vi behöver ta några för att ge funktionerna RunnerFunctions.php i en mer objektorienterad aspekt.

const WRONG_ANSWER_ID = 7; const MIN_ANSWER_ID = 0; const MAX_ANSWER_ID = 9; funktionen ärCurrentAnswerCorrect ($ minAnswerId = MIN_ANSWER_ID, $ maxAnswerId = MAX_ANSWER_ID) return rand ($ minAnswerId, $ maxAnswerId)! = WRONG_ANSWER_ID;  funktionskörning () $ display = nytt CLIDplay (); $ aGame = nytt spel ($ display); $ AGame-> lägga ( "Chet"); $ AGame-> lägga ( "Pat"); $ AGame-> lägga ( "Sue"); gör $ tärning = rand (0, 5) + 1; $ AGame-> rulle ($ tärningar);  medan (! gjordeSomebodyWin ($ aGame, isCurrentAnswerCorrect ()));  fungerar didSomebodyWin ($ aGame, $ isCurrentAnswerCorrect) if ($ isCurrentAnswerCorrect) return! $ AGame-> wasCorrectlyAnswered ();  annars returnera! $ AGame-> wrongAnswer (); 

Min första instinkt är att bara sätta dem i en klass. Det här är inget geni, men det är något som gör att vi börjar ändra saker. Låt oss se om idén faktiskt kan fungera.

const WRONG_ANSWER_ID = 7; const MIN_ANSWER_ID = 0; const MAX_ANSWER_ID = 9; klass RunnerFunctions funktionen ärCurrentAnswerCorrect ($ minAnswerId = MIN_ANSWER_ID, $ maxAnswerId = MAX_ANSWER_ID) return rand ($ minAnswerId, $ maxAnswerId)! = WRONG_ANSWER_ID;  funktionskörning () // ... // funktionen gjordeSomebodyWin ($ aGame, $ isCurrentAnswerCorrect) // ... //

Om vi ​​gör det måste vi ändra våra test och vårt GameRunner.php att använda den nya klassen. Vi kallade klassen någonting generisk för tillfället, omdirigering blir lätt när det behövs. Vi vet inte ens om denna klass kommer att existera ensam eller kommer att jämställas i Spel. Så oroa dig inte om namngivning än.

privat funktion genereraOutput ($ seed) ob_start (); srand ($ utsäde); (nya RunnerFunctions ()) -> run (); $ output = ob_get_contents (); ob_end_clean (); returnera $ output; 

I vår GoldenMasterTest.php fil måste vi ändra hur vi kör vår kod. Funktionen är generateOutput () och dess tredje rad måste ändras för att skapa ett nytt objekt och samtal springa() på det. Men det misslyckas.

PHP Dödligt fel: Ring till odefinierad funktion didSomebodyWin () i ... 

Vi behöver nu ändra vår nya klass ytterligare.

gör $ tärning = rand (0, 5) + 1; $ AGame-> rulle ($ tärningar);  medan (! $ this-> didSomebodyWin ($ aGame, $ this-> isCurrentAnswerCorrect ()));

Vi behövde bara ändra tillståndet hos medan uttalande i springa() metod. Den nya koden ringer didSomebodyWin () och isCurrentAnswerCorrect () från den nuvarande klassen, genom att lägga ut $ This-> till dem.

Detta gör det gyllene mästerspåret, men det bromsar löpare testen.

PHP Dödligt fel: Ring till odefinierad funktion isCurrentAnswerCorrect () in / ... /RunnerFunctionsTest.php på rad 25

Problemet ligger i assertAnswersAreCorrectFor (), men lätt fixerbar genom att skapa ett löpareobjekt först.

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

Samma sak måste behandlas även i tre andra funktioner.

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

Medan detta gör att koden går över, introducerar den lite kodöverföring. Eftersom vi nu är med alla tester på grön, kan vi extrahera löpare skapandet till en inrätta() metod.

privat $ löpare; funktion setUp () $ this-> runner = new Runner ();  funktion testItCanFindCorrectAnswer () $ this-> assertAnswersAreCorrectFor ($ this-> getCorrectAnswerIDs ());  funktion testItCanFindWrongAnswer () $ this-> assertFalse ($ this-> runner-> isCurrentAnswerCorrect (WRONG_ANSWER_ID, WRONG_ANSWER_ID));  funktion testItCanTellIfThereIsNoWinnerWhenACorrectAnswerIsProvided () $ this-> assertTrue ($ this-> runner-> didSomebodyWin ($ this-> aFakeGame (), $ this-> aCorrectAnswer ()));  funktion testItCanTellIfThereIsNoWinnerWhenAWrongAnswerIsProvided () $ this-> assertFalse ($ this-> runner-> didSomebodyWin ($ this-> aFakeGame (), $ this-> aWrongAnswer ()));  privat funktion assertAnswersAreCorrectFor ($ correctAnserIDs) foreach ($ correctAnserIDs som $ id) $ this-> assertTrue ($ this-> runner-> isCurrentAnswerCorrect ($ id, $ id)); 

Trevlig. Alla dessa nya skapelser och refactorings fick mig att tänka. Vi heter vår variabel löpare. Kanske kan vår klass kallas samma. Låt oss refactor det. Det borde vara lätt.

Om du inte kollade "Sök efter texthändelser"i rutan ovanför, glöm inte att ändra din ingår manuellt, eftersom refactoring kommer att byta namn på filen också.

Nu har vi en fil som heter GameRunner.php, en annan som heter Runner.php och en tredje som heter Game.php. Jag vet inte om dig, men det verkar vara mycket förvirrande för mig. Om jag skulle se dessa tre filer för första gången i mitt liv, hade jag ingen aning om vilken man gör vad. Vi måste bli av med minst en av dem.

Anledningen till att vi skapade RunnerFunctions.php fil i de tidiga stadierna av vår refactoring, var att bygga upp ett sätt att inkludera alla metoder och filer för testning. Vi behövde tillgång till allt, men kör inte allt om inte i en beredd miljö i vår gyllene mästare. Vi kan fortfarande göra samma sak, bara springa inte vår kod från GameRunner.php. Vi behöver uppdatera inkludera och skapa en klass inuti, innan vi fortsätter.

require_once __DIR__. '/Display.php'; require_once __DIR__. '/Runner.php'; (nya löpare ()) -> kör ();

Det kommer att göra det. Vi måste inkludera Display.php uttryckligen, så när Löpare försöker skapa en ny CLIDisplay, Det kommer att veta vad som ska genomföras.


Analysera oro

Jag tror att en av de viktigaste egenskaperna hos objektorienterad programmering är att definiera bekymmer. Jag ställer mig alltid till frågor som "gör den här klassen vad namnet säger?", "Är den här metoden av intresse för detta objekt?", "Ska mitt objekt bryr sig om det specifika värdet?"

Förvånansvärt har dessa typer av frågor en stor kraft när det gäller att klargöra både affärsområdet och programvaruarkitekturen. Vi frågar och svarar på dessa typer av frågor i en grupp på Syneto. Många gånger när en programmerare har ett dilemma står han eller hon uppe, frågar om två minuters uppmärksamhet från laget för att hitta vår åsikt om ett ämne. De som känner till kodarkitekturen kommer att svara från en mjukvarusynvinkel, medan andra som är mer bekanta med affärsområdet kan belysa några väsentliga insikter om kommersiella aspekter.

Låt oss försöka tänka på bekymmer i vårt fall. Vi kan fortsätta att fokusera på Löpare klass. Det är mycket mer sannolikt att eliminera eller förvandla denna klass, än Spel.

Först ska en runner bryr sig om hur isCurrentAnswerCorrect () arbetssätt? Skulle en löpare ha någon kunskap om frågor och svar?

Det verkar verkligen som att den här metoden skulle vara bättre i Spel. Jag tror starkt på att a Spel om trivia bör bry sig om ett svar är korrekt eller inte. Jag tror verkligen en Spel måste vara oroad över att ge svaret på svaret på den aktuella frågan.

Det är dags att agera. Vi ska göra en flytta metod refacto. Som vi har sett allt detta tidigare från mina tidigare tutorials, visar jag bara slutresultatet.

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

Det är viktigt att notera att inte bara metoden gick bort, men den konstanta definieringen av svarets gränser också.

Men vad sägs om didSomebodyWin ()? Ska en löpare bestämma när någon har vunnit? Om vi ​​tittar på metodens kropp kan vi se ett problem som lyfter fram som en ficklampa i mörkret.

funktion didSomebodyWin ($ aGame, $ isCurrentAnswerCorrect) om ($ isCurrentAnswerCorrect) return! $ aGame-> wasCorrectlyAnswered ();  annars return! $ aGame-> wrongAnswer (); 

Vad den här metoden gör gör det på en Spel endast objekt. Det verifierar det nuvarande svaret som returneras av spelet. Därefter returnerar det vad ett spelobjekt returnerar i sin wasCorrectlyAnswered () eller fel svar() metoder. Denna metod gör ingenting på egen hand. Allt det bryr sig om är Spel. Detta är ett klassiskt exempel på en kodlucka som heter Feature Envy. En klass gör något som en annan klass borde göra. Dags att flytta den.

klass RunnerFunctionsTest utökar PHPUnit_Framework_TestCase privat $ runner; funktion setUp () $ this-> runner = new Runner (); 

Som vanligt flyttade vi testerna först. TDD? Någon?

Det ger oss inga fler test att köra, så den här filen kan gå nu. Radera är min favorit del av programmeringen.

Och när vi kör våra test får vi ett bra fel.

Felaktigt fel: Ring till odefinierad metod Spel :: didSomebodyWin ()

Det är dags att ändra koden också. Kopiera och klistra in metoden i Spel kommer att göra alla testen magiskt. Både de gamla och de som flyttade till GameTest. Men samtidigt som metoden sitter på rätt plats har det två problem: löparen behöver också ändras och vi skickar in en falsk Spel objekt som vi inte behöver göra längre eftersom det är en del av Spel.

gör $ tärning = rand (0, 5) + 1; $ AGame-> rulle ($ tärningar);  medan (! $ aGame-> didSomebodyWin ($ aGame, $ this-> isCurrentAnswerCorrect ()));

Att fixa löparen är väldigt lätt. Vi ändras bara $ this-> didSomebodyWin (...) in i $ aGame-> didSomebodyWin (...). Vi måste komma tillbaka hit och ändra det igen, efter vårt nästa steg. Testrefaktoreringen.

funktion testItCanTellIfThereIsNoWinnerWhenACorrectAnswerIsProvided () $ aGame = \ Mockery :: mock ('Game [wasCorrectlyAnswered]'); $ AGame-> shouldReceive (wasCorrectlyAnswered) -> en gång () -> andReturn (false); $ This-> assertTrue ($ aGame-> didSomebodyWin ($ this-> aCorrectAnswer ())); 

Det är dags för lite mocking! I stället för att använda vår falska klass, definierad i slutet av våra tester, kommer vi att använda Mockery. Det gör det möjligt för oss att enkelt skriva över en metod Spel, förvänta dig att det heter och returnera det värde vi vill ha. Naturligtvis kan vi göra det genom att göra vår falska klass förlängas Spel och skriv över själva metoden. Men varför gör ett jobb för vilket ett verktyg finns?

funktionstestItCanTellIfThereIsNoWinnerWhenAWrongAnswerIsProvided () $ aGame = \ Mockery :: mock ('Game [wrongAnswer]'); $ AGame-> shouldReceive (wrongAnswer) -> en gång () -> andReturn (true); $ This-> assertFalse ($ aGame-> didSomebodyWin ($ this-> aWrongAnswer ())); 

Efter att vår andra metod har skrivits om, kan vi bli av med den falska spelklassen och alla metoder som initierade den. Problem löses!

Slutgiltiga tankar

Även om vi lyckades tänka på endast Löpare, vi gjorde stora framsteg idag. Vi lärde oss om ansvar, vi identifierade metoder och variabler som tillhör en annan klass. Vi tänkte på en högre nivå och vi utvecklades mot en bättre lösning. I Syneto-teamet finns det en stark tro på att det finns sätt att skriva kod bra och aldrig göra en förändring om det inte gjorde koden åtminstone lite renare. Det här är en teknik som i tid kan leda till en mycket trevligare kodbas, med mindre beroende, fler tester och så småningom mindre buggar.

Tack för din tid.