Nacalculatie van oude code deel 9 - Bezorgdheid analyseren

In deze tutorial zullen we ons blijven concentreren op onze bedrijfslogica. We zullen evalueren of RunnerFunctions.php behoort tot een klasse en zo ja, tot welke klas? We zullen nadenken over zaken en waar methoden bij horen. Ten slotte zullen we iets meer leren over het concept van spot. Dus waar wacht je op? Lees verder.


RunnerFunctions - Van procedureel naar objectgericht

Ook al hebben we de meeste van onze code in objectgeoriënteerde vorm, mooi georganiseerd in klassen, sommige functies zitten gewoon in een bestand. We moeten enkele nemen om de functies te geven RunnerFunctions.php in een meer objectgericht aspect.

const WRONG_ANSWER_ID = 7; const MIN_ANSWER_ID = 0; const MAX_ANSWER_ID = 9; function isCurrentAnswerCorrect ($ minAnswerId = MIN_ANSWER_ID, $ maxAnswerId = MAX_ANSWER_ID) return rand ($ minAnswerId, $ maxAnswerId)! = WRONG_ANSWER_ID;  function run () $ display = new CLIDisplay (); $ aGame = nieuwe game ($ display); $ AGame-> toe te voegen ( "Chet"); $ AGame-> toe te voegen ( "Pat"); $ AGame-> toe te voegen ( "Sue"); doe $ dobbelstenen = rand (0, 5) + 1; $ AGame-> roll ($ dobbelstenen);  while (! didSomebodyWin ($ aGame, isCurrentAnswerCorrect ()));  function didSomebodyWin ($ aGame, $ isCurrentAnswerCorrect) if ($ isCurrentAnswerCorrect) ga terug! $ AGame-> wasCorrectlyAnswered ();  else terug! $ AGame-> wrongAnswer (); 

Mijn eerste instinct is om ze gewoon in een klas in te pakken. Dit is niets geniaal, maar het is iets waardoor we dingen gaan veranderen. Laten we eens kijken of het idee echt kan werken.

const WRONG_ANSWER_ID = 7; const MIN_ANSWER_ID = 0; const MAX_ANSWER_ID = 9; class RunnerFunctions function isCurrentAnswerCorrect ($ minAnswerId = MIN_ANSWER_ID, $ maxAnswerId = MAX_ANSWER_ID) return rand ($ minAnswerId, $ maxAnswerId)! = WRONG_ANSWER_ID;  function run () // ... // function didSomebodyWin ($ aGame, $ isCurrentAnswerCorrect) // ... //

Als we dat doen, moeten we onze tests en onze GameRunner.php om de nieuwe klasse te gebruiken. We noemden de klas voorlopig iets generiek, het hernoemen ervan zal gemakkelijk zijn wanneer dat nodig is. We weten niet eens of deze klasse alleen zal bestaan ​​of zal worden geassimileerd Spel. Maak je dus geen zorgen over de naamgeving.

private function generateOutput ($ seed) ob_start (); srand ($ zaad); (nieuwe RunnerFunctions ()) -> run (); $ output = ob_get_contents (); ob_end_clean (); return $ output; 

In onze GoldenMasterTest.php bestand, moeten we de manier wijzigen waarop we onze code uitvoeren. De functie is generateOutput () en de derde regel moet worden gewijzigd om een ​​nieuw object en een oproep te maken rennen() ben ermee bezig. Maar dit mislukt.

PHP Fatale fout: aanroep van de ongedefinieerde functie didSomebodyWin () in ... 

We moeten nu onze nieuwe klasse verder aanpassen.

doe $ dobbelstenen = rand (0, 5) + 1; $ AGame-> roll ($ dobbelstenen);  while (! $ this-> didSomebodyWin ($ aGame, $ this-> isCurrentAnswerCorrect ()));

We hoefden alleen de staat van de te wijzigen terwijl verklaring in de rennen() methode. De nieuwe code roept didSomebodyWin () en isCurrentAnswerCorrect () van de huidige klas, door vooruit te gaan $ This-> naar hen.

Dit maakt de gouden meester pass, maar het remt de runnertests.

PHP Fatale fout: aanroep van de ongedefinieerde functie isCurrentAnswerCorrect () in / ... / RunnerFunctionsTest.php on line 25

Het probleem zit erin assertAnswersAreCorrectFor (), maar eenvoudig te repareren door eerst een runner-object aan te maken.

private function assertAnswersAreCorrectFor ($ correctAnserIDs) $ runner = new RunnerFunctions (); foreach ($ correctAnserIDs als $ id) $ this-> assertTrue ($ runner-> isCurrentAnswerCorrect ($ id, $ id)); 

Ditzelfde probleem moet ook in drie andere functies worden aangepakt.

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

Hoewel dit de code laat passeren, introduceert het een beetje codeduplicatie. Omdat we nu met alle tests op groen bezig zijn, kunnen we de runner-creatie tot een opstelling() methode.

privé $ runner; function setUp () $ this-> runner = new Runner ();  function testItCanFindCorrectAnswer () $ this-> assertAnswersAreCorrectFor ($ this-> getCorrectAnswerIDs ());  function testItCanFindWrongAnswer () $ this-> assertFalse ($ this-> runner-> isCurrentAnswerCorrect (WRONG_ANSWER_ID, WRONG_ANSWER_ID));  function testItCanTellIfThereIsNoWinnerWhenACorrectAnswerIsProvided () $ this-> assertTrue ($ this-> runner-> didSomebodyWin ($ this-> aFakeGame (), $ this-> aCorrectAnswer ()));  function testItCanTellIfThereIsNoWinnerWhenAWrongAnswerIsProvided () $ this-> assertFalse ($ this-> runner-> didSomebodyWin ($ this-> aFakeGame (), $ this-> aWrAnAnswer ()));  private functie assertAnswersAreCorrectFor ($ correctAnserIDs) foreach ($ correctAnserIDs as $ id) $ this-> assertTrue ($ this-> runner-> isCurrentAnswerCorrect ($ id, $ id)); 

Leuk. Al deze nieuwe creaties en refactorings hebben me aan het denken gezet. We hebben onze variabele genoemd loper. Misschien kan onze klas hetzelfde worden genoemd. Laten we het refactiveren. Het zou makkelijk moeten zijn.

Als je niet hebt gecontroleerd "Zoeken naar tekstvoorvallen"Vergeet in het bovenstaande vak niet om je handleidingen handmatig te wijzigen, omdat de refactoring het bestand ook hernoemt.

Nu hebben we een bestand met de naam GameRunner.php, een andere genaamd Runner.php en een derde genaamd Game.php. Ik weet niets over u, maar dit lijkt mij buitengewoon verwarrend. Als ik deze drie bestanden voor het eerst in mijn leven zou zien, zou ik geen idee hebben wie wat doet. We moeten minstens één van hen kwijtraken.

De reden dat we de RunnerFunctions.php bestand in de vroege stadia van onze refactoring, was om een ​​manier op te bouwen om alle methoden en bestanden voor testen op te nemen. We hadden toegang tot alles nodig, maar niet alles, behalve in een voorbereide omgeving in onze gouden meester. We kunnen nog steeds hetzelfde doen, gewoon onze code niet uitvoeren GameRunner.php. We moeten de include bijwerken en een klasse binnen maken, voordat we verder gaan.

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

Dat zal het doen. We moeten opnemen Display.php expliciet, dus wanneer loper probeert een nieuw te maken CLIDisplay, het zal weten wat te implementeren.


Problemen analyseren

Ik geloof dat een van de belangrijkste kenmerken van objectgeoriënteerd programmeren bezorgdheid definieert. Ik stel mezelf altijd vragen als: "doet deze klas wat zijn naam zegt?", "Is deze methode van zorg voor dit object?", "Moet mijn object om die specifieke waarde geven?"

Verrassend genoeg hebben dit soort vragen een grote kracht bij het verduidelijken van zowel de zakelijke als de softwarearchitectuur. We stellen dit soort vragen in een groep bij Syneto en beantwoorden deze. Vaak wanneer een programmeur een dilemma heeft, staat hij of zij gewoon op en vraagt ​​hij om twee minuten aandacht van het team om onze mening over een onderwerp te vinden. Degenen die bekend zijn met de code-architectuur zullen antwoorden vanuit een software-oogpunt, terwijl anderen, die meer vertrouwd zijn met het bedrijfsdomein, licht kunnen werpen op enkele essentiële inzichten over commerciële aspecten.

Laten we in ons geval proberen na te denken over zorgen. We kunnen ons blijven concentreren op de loper klasse. Het is veel waarschijnlijker om deze klasse te elimineren of te transformeren dan Spel.

Ten eerste, zou een hardloper zich moeten bekommeren om hoe isCurrentAnswerCorrect () werkt? Moet een hardloper enige kennis hebben over vragen en antwoorden?

Het lijkt echt alsof deze methode beter af zou zijn Spel. Ik ben ervan overtuigd dat een Spel over trivia zou het belangrijk zijn als een antwoord juist is of niet. Ik geloof echt een Spel moet zich zorgen maken over het resultaat van het antwoord voor de huidige vraag.

Het is tijd om te handelen. We zullen a verplaats methode refactoring. Zoals we al eerder in mijn vorige tutorials hebben gezien, laat ik je het eindresultaat zien.

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

Het is essentieel om op te merken dat niet alleen de methode wegging, maar ook dat de constante de grenzen van het antwoord definieerde.

Maar hoe zit het met didSomebodyWin ()? Moet een hardloper beslissen wanneer iemand heeft gewonnen? Als we naar het lichaam van de methode kijken, kunnen we een probleem zien als een zaklamp in het donker.

function didSomebodyWin ($ aGame, $ isCurrentAnswerCorrect) if ($ isCurrentAnswerCorrect) return! $ aGame-> wasCorrectlyAnswered ();  else return! $ aGame-> wrongAnswer (); 

Wat deze methode ook doet, hij doet het op a Spel alleen object. Het verifieert het huidige antwoord geretourneerd door het spel. Daarna geeft het terug wat een game-object in zijn wasCorrectlyAnswered () of wrongAnswer () methoden. Deze methode doet op zichzelf niets. Het geeft alleen maar om Spel. Dit is een klassiek voorbeeld van een code geur genoemd Feature Envy. Een klas doet iets dat een andere klas zou moeten doen. Tijd om het te verplaatsen.

class RunnerFunctionsTest breidt PHPUnit_Framework_TestCase uit private $ runner; function setUp () $ this-> runner = new Runner (); 

Zoals gewoonlijk hebben we de tests eerst verplaatst. TDD? Iedereen?

Dit laat ons geen tests meer uitvoeren, dus dit bestand kan nu gaan. Verwijderen is mijn favoriete onderdeel van programmeren.

En als we onze tests uitvoeren, krijgen we een mooie foutmelding.

Fatale fout: aanroep op ongedefinieerde methode Game :: didSomebodyWin ()

Het is nu tijd om de code ook te wijzigen. Kopieer en plak de methode in Spel zal op magische wijze alle tests laten passeren. Zowel de oude als de oude GameTest. Maar terwijl dit de methode op de juiste plaats plaatst, heeft het twee problemen: de hardloper moet ook worden veranderd en we sturen een nep in Spel object dat we niet meer hoeven te doen omdat het onderdeel is van Spel.

doe $ dobbelstenen = rand (0, 5) + 1; $ AGame-> roll ($ dobbelstenen);  while (! $ aGame-> didSomebodyWin ($ aGame, $ this-> isCurrentAnswerCorrect ()));

De loper repareren is heel eenvoudig. We veranderen gewoon $ this-> didSomebodyWin (...) in $ aGame-> didSomebodyWin (...). We zullen hier terug moeten komen en het opnieuw moeten wijzigen, na onze volgende stap. De testrefactoring.

function testItCanTellIfThereIsNoWinnerWhenACorrectAnswerIsProvided () $ aGame = \ Mockery :: mock ('Game [wasCorrectlyAnswered]'); $ AGame-> shouldReceive ( 'wasCorrectlyAnswered') -> één keer () -> andReturn (false); $ This-> assertTrue ($ aGame-> didSomebodyWin ($ this-> aCorrectAnswer ())); 

Het is tijd voor wat spot! In plaats van onze valse klasse te gebruiken, die aan het einde van onze tests is gedefinieerd, gebruiken we Mockery. Hiermee kunnen we eenvoudig een methode overschrijven Spel, verwachten dat het wordt gebeld en de gewenste waarde teruggeven. Natuurlijk kunnen we dit doen door onze nepklasse uit te breiden Spel en overschrijf de methode zelf. Maar waarom doet u een baan waarvoor een tool bestaat??

function testItCanTellIfThereIsNoWinnerWhenAWrongAnswerIsProvided () $ aGame = \ Mockery :: mock ('Game [wrongAnswer]'); $ AGame-> shouldReceive ( 'wrongAnswer') -> één keer () -> andReturn (true); $ This-> assertFalse ($ aGame-> didSomebodyWin ($ this-> aWrongAnswer ())); 

Nadat onze tweede methode is herschreven, kunnen we de nep-gameklasse en alle methoden die het hebben geïnitialiseerd, verwijderen. Problemen opgelost!

Laatste gedachten

Ook al zijn we erin geslaagd om alleen aan de loper, we hebben grote vooruitgang geboekt vandaag. We leerden over verantwoordelijkheden, we identificeerden methoden en variabelen die tot een andere klasse behoren. We dachten op een hoger niveau en we evolueerden naar een betere oplossing. In het Syneto-team is er een sterke overtuiging dat er manieren zijn om code goed te schrijven en nooit een verandering te plegen tenzij het de code op zijn minst een beetje schoner maakt. Dit is een techniek die op den duur kan leiden tot een veel leukere codebase, met minder afhankelijkheden, meer tests en uiteindelijk minder bugs.

Bedankt voor je tijd.