Oude code. Lelijke code. Ingewikkelde code. Spaghetti-code. Gibberistische onzin. In twee woorden, Oude code. Dit is een serie die u zal helpen om ermee te werken en ermee om te gaan.
Ik denk graag aan code, net als aan proza. Lange, geneste, samengestelde zinnen met exotische woorden zijn moeilijk te begrijpen. Van tijd tot tijd heb je er een nodig, maar meestal kun je gewoon simpele woorden gebruiken in korte zinnen. Dit geldt ook voor de broncode. Complexe conditionaliteiten zijn moeilijk te begrijpen. Lange methoden zijn als eindeloze zinnen.
Hier is een "prozaïsch" voorbeeld om je op te vrolijken. Ten eerste de alles-in-één zin. De lelijke.
Als de temperatuur in de serverruimte minder dan vijf graden bedraagt en de luchtvochtigheid meer dan vijftig procent bedraagt, maar onder de tachtig blijft en de luchtdruk stabiel is, moet de senior technicus John, die minstens drie jaar werkervaring heeft op het gebied van netwerken en serverbeheer, worden gewaarschuwd en hij moet midden in de nacht wakker worden, zich verkleden, naar buiten gaan, zijn auto nemen of een taxi bellen als hij geen auto heeft, naar het kantoor rijden, het gebouw betreden, de airconditioning starten en wacht tot de temperatuur meer dan tien graden stijgt en de luchtvochtigheid daalt tot onder de twintig procent.
Als je die paragraaf begrijpt, begrijpt en onthoudt zonder hem opnieuw te lezen, geef ik je een medaille (virtueel natuurlijk). Lange, verwarde paragrafen geschreven in een enkele gecompliceerde zin zijn moeilijk te begrijpen. Helaas ken ik niet genoeg exotische Engelse woorden om dat nog moeilijker te begrijpen.
Laten we een manier vinden om het een beetje te vereenvoudigen. Al het eerste deel, tot het "toen" is een voorwaarde. Ja, het is ingewikkeld, maar we kunnen het als volgt samenvatten: Als omgevingscondities een risico vormen ... ... dan moet er iets gebeuren. De gecompliceerde uitdrukking zegt dat we iemand moeten waarschuwen die aan veel voorwaarden voldoet: meld vervolgens level 3 technische ondersteuning aan. Ten slotte wordt een heel proces beschreven van het wakker maken van de tech-man totdat alles is opgelost: en verwachten dat de omgeving binnen normale parameters wordt hersteld. Laten we het allemaal samenvoegen.
Als de omgevingscondities een risico vormen, meld dan de technische ondersteuning van niveau drie en verwacht dat de omgeving binnen normale parameters wordt hersteld.
Nu, dat is slechts ongeveer 20% in lengte vergeleken met de originele tekst. We kennen de details niet en in de overgrote meerderheid van de gevallen maakt het ons niet uit. En dit geldt ook voor de broncode. Hoeveel keer gaf je om de implementatiedetails van een logInfo ("Een bericht");
methode? Waarschijnlijk één keer, als en wanneer je het geïmplementeerd hebt. Vervolgens wordt het bericht alleen in de categorie "info" vastgelegd. Of als een gebruiker een van uw producten koopt, wilt u hem dan factureren? Nee. Het enige waar je om geeft is als het product is gekocht, gooit u het weg uit de voorraad en factureert u het aan de koper. De voorbeelden kunnen eindeloos zijn. Ze zijn in feite hoe we correcte software schrijven.
In deze sectie proberen we de proeffilosofie toe te passen op onze trivia-game. Eén stap tegelijk Beginnend met complexe conditionals. Laten we beginnen met een eenvoudige code. Gewoon om op te warmen.
Lijn twintig van de GameRunner.php
bestand leest als volgt:
if (rand ($ minAnswerId, $ maxAnswerId) == $ wrongAnswerId)
Hoe zou dat in proza klinken? Als een willekeurig getal tussen minimumantwoord-ID en maximumantwoord-ID gelijk is aan het verkeerde antwoord-ID, dan ...
Dit is niet erg ingewikkeld, maar we kunnen het nog steeds eenvoudiger maken. Hoe zit het met dit? Als een verkeerd antwoord is geselecteerd, dan ... Beter, is het niet?
We hebben een manier, een procedure, een techniek nodig om die voorwaardelijke verklaring ergens anders te verplaatsen. Die bestemming kan gemakkelijk een methode zijn. Of in ons geval, omdat we hier niet in een klas zitten, een functie. Dit verplaatsen van gedrag naar een nieuwe methode of functie wordt de "Extract-methode" refactoring genoemd. Hieronder staan de stappen, zoals gedefinieerd door Martin Fowler in zijn uitstekende boek Refactoring: Improving the Design of Existing Code. Als je dit boek niet hebt gelezen, zou je het nu in je "Te lezen" -lijst moeten plaatsen. Het is een van de meest essentiële boeken voor een moderne programmeur.
Voor onze zelfstudie heb ik de originele stappen genomen en ze een beetje vereenvoudigd om beter aan onze behoeften en ons type zelfstudie te voldoen.
Dit is vrij ingewikkeld. De extractiemethode is echter aantoonbaar de meest gebruikte refactoring, behalve misschien om te hernoemen. Dus je moet de mechanica ervan begrijpen.
Gelukkig voor ons, moderne IDE's zoals PHPStorm bieden geweldige refactoring tools, zoals we hebben gezien in de tutorial PHPStorm: When the IDE Really Matters. We zullen dus de functies gebruiken die we binnen handbereik hebben, in plaats van alles handmatig uit te voeren. Dit is minder foutgevoelig en veel, veel sneller.
Selecteer gewoon het gewenste deel van de code en klik met de rechtermuisknop het.
De IDE begrijpt automatisch dat we drie parameters nodig hebben om onze code uit te voeren en zal de volgende oplossing voorstellen.
// ... // $ minAnswerId = 0; $ maxAnswerId = 9; $ wrongAnswerId = 7; function isCurrentAnswerWrong ($ minAnswerId, $ maxAnswerId, $ wrongAnswerId) return rand ($ minAnswerId, $ maxAnswerId) == $ wrongAnswerId; doen $ dobbelstenen = rand (0, 5) + 1; $ AGame-> roll ($ dobbelstenen); if (isCurrentAnswerWrong ($ minAnswerId, $ maxAnswerId, $ wrongAnswerId)) $ notAWinner = $ aGame-> wrongAnswer (); else $ notAWinner = $ a Spel-> wasCorrectlyAnswered (); while ($ notAWinner);
Hoewel deze code syntactisch correct is, worden onze tests doorbroken. Tussen al het geluid dat ons wordt getoond in rode, blauwe en zwarte kleuren, kunnen we de reden vinden waarom:
Dodelijke fout: Kan niet redeclare isCurrentAnswerWrong () (eerder gedeclareerd in / home / csaba / Personal / Programming / NetTuts / Refactoring Legacy Code - Deel 3: Complexe voorwaardes en lange methoden /Source/trivia/php/GameRunner.php:16) in / home / csaba / Personal / Programmeren / NetTuts / Legacy-code herformuleren - Deel 3: Complexe voorwaardes en lange methoden /Source/trivia/php/GameRunner.php on line 18
Het zegt eigenlijk dat we de functie twee keer willen aangeven. Maar hoe kan dat gebeuren? We hebben het maar één keer in onze GameRunner.php
!
Bekijk de tests. Er is een generateOutput ()
methode die een vereisen()
op onze GameRunner.php
. Het wordt minstens twee keer genoemd. Hier is de oorzaak van de fout.
Nu hebben we een dilemma. Vanwege het zaaien van de willekeurige generator, moeten we deze code met gecontroleerde waarden aanroepen.
private function generateOutput ($ seed) ob_start (); srand ($ zaad); vereisen __DIR__. '/ ... /trivia/php/GameRunner.php'; $ output = ob_get_contents (); ob_end_clean (); return $ output;
Maar er is geen manier om een functie twee keer in PHP te declareren, dus we hebben een andere oplossing nodig. We beginnen de last van onze gouden meester te voelen. Het hele ding 20000 keer uitvoeren, elke keer dat we een stuk code wijzigen, is misschien geen oplossing voor de lange termijn. Afgezien van het feit dat het eeuwen duurt om te draaien, dwingt het ons onze code te veranderen om tegemoet te komen aan de manier waarop we het testen. Dit is meestal een teken van slechte tests. De code moet worden gewijzigd en de test moet nog worden doorstaan, maar de wijzigingen moeten een reden hebben om te wijzigen, alleen afkomstig van de broncode.
Maar genoeg gepraat, we hebben een oplossing nodig, zelfs een tijdelijke oplossing zal het voorlopig doen. Migratie naar eenheidstests begint met onze volgende les.
Een manier om ons probleem op te lossen is om de rest van de code in te nemen GameRunner.php
en zet het in een functie. Laten we zeggen rennen()
include_once __DIR__. '/Game.php'; function isCurrentAnswerWrong ($ minAnswerId, $ maxAnswerId, $ wrongAnswerId) return rand ($ minAnswerId, $ maxAnswerId) == $ wrongAnswerId; function run () $ notAWinner; $ aGame = nieuwe game (); $ AGame-> toe te voegen ( "Chet"); $ AGame-> toe te voegen ( "Pat"); $ AGame-> toe te voegen ( "Sue"); $ minAnswerId = 0; $ maxAnswerId = 9; $ wrongAnswerId = 7; doe $ dobbelstenen = rand (0, 5) + 1; $ AGame-> roll ($ dobbelstenen); if (isCurrentAnswerWrong ($ minAnswerId, $ maxAnswerId, $ wrongAnswerId)) $ notAWinner = $ aGame-> wrongAnswer (); else $ notAWinner = $ a Spel-> wasCorrectlyAnswered (); while ($ notAWinner);
Dit stelt ons in staat om het te testen, maar houd er rekening mee dat als de code van de console wordt uitgevoerd, het spel niet wordt uitgevoerd. We hebben een kleine verandering in gedrag aangebracht. We hebben testbaarheid opgedaan ten koste van een gedragsverandering, wat we in de eerste plaats niet wilden doen. Als je de code van de console wilt gebruiken, heb je nu een ander PHP-bestand nodig dat de runner bevat of vereist en vervolgens expliciet de run-methode erop aanroept. Niet zo'n grote verandering, maar een must om te onthouden, vooral als je externe partijen je bestaande code gebruikt.
Aan de andere kant kunnen we het bestand nu gewoon opnemen in onze test.
vereisen __DIR__. '/ ... /trivia/php/GameRunner.php';
En dan bellen rennen()
in de methode generateOutput ().
private function generateOutput ($ seed) ob_start (); srand ($ zaad); rennen(); $ output = ob_get_contents (); ob_end_clean (); return $ output;
Misschien is dit een goede gelegenheid om na te denken over de structuur van onze mappen en bestanden. Er zijn geen ingewikkelder conditionals in onze GameRunner.php
, maar voordat we doorgaan naar de Game.php
bestand, we mogen geen rommel achter ons laten. Onze GameRunner.php
draait niets meer en we moesten methodes hacken om het testbaar te maken, waardoor onze publieke interface werd verbroken. De reden hiervoor is dat we misschien het verkeerde testen.
Onze testoproepen rennen()
in de GameRunner.php
bestand, inclusief Game.php
, speelt het spel en een nieuw gouden hoofdbestand wordt gegenereerd. Wat als we een ander bestand introduceren? Wij maken de GameRunner.php
om het spel daadwerkelijk uit te voeren door te bellen rennen()
en niets anders. Dus wat als er geen logica in zit die fout zou kunnen gaan en er zijn geen tests nodig, en dan verplaatsen we onze huidige code naar een ander bestand?
Dit is een heel ander verhaal. Nu hebben onze tests toegang tot de code net onder de hardloper. Kortom, onze tests zijn gewoon hardlopers. En natuurlijk in ons nieuwe GameRunner.php
er zal alleen een oproep zijn om het spel uit te voeren. Dit is een echte hardloper, het doet niets anders dan het rennen()
methode. Geen logica betekent dat er geen tests nodig zijn.
require_once __DIR__. '/RunnerFunctions.php'; rennen();
Er zijn nog andere vragen die we ons op dit moment kunnen stellen. Hebben we echt een nodig RunnerFunctions.php
? Kon niet wij enkel de functies van daar nemen en hen bewegen aan Game.php
? Waarschijnlijk wel, maar met ons huidige begrip van welke functie hoort waar? Is niet genoeg. We zullen een plaats vinden voor onze methode in een volgende les.
We hebben ook geprobeerd onze bestanden een naam te geven op basis van wat de code in hen doet. De ene is slechts een aantal functies voor de renner, functies die we op dit moment overwegen bij elkaar te horen, om te voldoen aan de behoeften van de hardloper. Wordt dit op een gegeven moment een klasse in de toekomst? Kan zijn. Misschien niet. Voor nu is het goed genoeg.
Als we de RunnerFunctions.php
bestand, er is een beetje een puinhoop die we hebben geïntroduceerd.
We definiëren:
$ minAnswerId = 0; $ maxAnswerId = 9; $ wrongAnswerId = 7;
… binnen in de rennen()
methode. Ze hebben een enkele reden om te bestaan en een enkele plaats waar ze worden gebruikt. Waarom definieer je ze niet alleen binnen die methode en verwijder je de parameters helemaal?
function isCurrentAnswerWrong () $ minAnswerId = 0; $ maxAnswerId = 9; $ wrongAnswerId = 7; return rand ($ minAnswerId, $ maxAnswerId) == $ wrongAnswerId;
Ok, de tests zijn voorbij en de code is veel leuker. Maar niet goed genoeg.
Het is veel gemakkelijker voor de menselijke geest om een positieve redenering te begrijpen. Dus als je negatieve conditionals kunt vermijden, moet je altijd dat pad volgen. In ons huidige voorbeeld controleert de methode op een verkeerd antwoord. Het zou veel gemakkelijker zijn om een methode te begrijpen die controleert op een geldigheid en die, indien nodig, negeert.
function isCurrentAnswerCorrect () $ minAnswerId = 0; $ maxAnswerId = 9; $ wrongAnswerId = 7; return-rand ($ minAnswerId, $ maxAnswerId)! = $ wrongAnswerId;
We hebben de Rename Method-refactoring gebruikt. Dit is opnieuw behoorlijk gecompliceerd als het met de hand wordt gebruikt, maar in elke IDE is het net zo eenvoudig als slaan CTRL + r, of selecteer de juiste optie in het menu. Om onze tests te laten slagen, moeten we ook onze voorwaardelijke verklaring bijwerken met een ontkenning.
if (! isCurrentAnswerCorrect ()) $ notAWinner = $ aGame-> wrongAnswer (); else $ notAWinner = $ a Spel-> wasCorrectlyAnswered ();
Dit brengt ons een stap dichter bij ons begrip van het voorwaardelijke. Gebruik makend van !
in een als()
verklaring, helpt eigenlijk. Het valt op en benadrukt het feit dat iets daar feitelijk teniet wordt gedaan. Maar kunnen we dit omkeren om negatie volledig te voorkomen? Ja dat kunnen we.
if (isCurrentAnswerCorrect ()) $ notAWinner = $ aGame-> wasCorrectlyAnswered (); else $ notAWinner = $ aGame-> wrongAnswer ();
Nu hebben we geen logische ontkenning door te gebruiken !
, noch lexicale ontkenning door de verkeerde dingen te benoemen en terug te geven. Al deze stappen maakten onze voorwaarden veel, veel gemakkelijker te bevatten.
Game.php
We zijn tot het uiterste vereenvoudigd, RunnerFunctions.php
. Laten we ons aanvallen Game.php
bestand nu. Er zijn verschillende manieren om conditionals te zoeken. Als u wilt, kunt u de code gewoon scannen door ernaar te kijken. Dit is langzamer, maar heeft de toegevoegde waarde dat je wordt gedwongen om het sequentieel te proberen te begrijpen.
De tweede voor de hand liggende manier om te zoeken naar conditionals, is om gewoon een zoekopdracht te doen voor "if" of "if (". Als je je code hebt geformatteerd met de ingebouwde functies van je IDE, kun je er zeker van zijn dat alle voorwaardelijke statements de dezelfde specifieke vorm.In mijn geval is er een spatie tussen de "als" en de haakjes.Als u de ingebouwde zoekfunctie gebruikt, worden de gevonden resultaten gemarkeerd in een schelle kleur, in mijn geval geel.
Nu we allemaal onze code als een kerstboom laten oplichten, kunnen we ze een voor een nemen. We kennen de oefening, we kennen de technieken die we kunnen gebruiken, het is tijd om ze toe te passen.
if ($ this-> inPenaltyBox [$ this-> currentPlayer])
Dit lijkt redelijk. We zouden het in een methode kunnen uitpakken, maar zou er een naam voor die methode zijn om de conditie duidelijker te maken?
if ($ roll% 2! = 0)
Ik wed dat 90% van alle programmeurs het probleem in het bovenstaande kan begrijpen als
uitspraak. We proberen ons te concentreren op wat onze huidige methode doet. En ons brein is verbonden met het domein van het probleem. We willen niet "een nieuwe thread starten" om die wiskundige uitdrukking te berekenen om te begrijpen dat het gewoon controleert of een getal vreemd is. Dit is een van die kleine afleiding die een moeilijke logische afleiding kan bederven. Dus ik zeg, laten we het uitpakken.
if ($ this-> isOdd ($ roll))
Dat is beter omdat het over het domein van het probleem gaat en geen extra hersencapaciteit vereist.
if ($ this-> plaatst [$ this-> currentPlayer]> $ lastPositionOnTheBoard)
Dit lijkt een goede kandidaat te zijn. Het is niet zo moeilijk om dit te begrijpen als een wiskundige uitdrukking, maar nogmaals, het is een uitdrukking die nevenbewerking nodig heeft. Ik vraag mezelf af, wat betekent het als de positie van de huidige speler het einde van het bord bereikt? Kunnen we deze staat niet op een meer beknopte manier uitdrukken? Dat kunnen we waarschijnlijk wel.
if ($ this-> playerRededEndOfBoard ($ lastPositionOnTheBoard))
Dit is beter. Maar wat gebeurt er eigenlijk in de als
? De speler wordt verplaatst aan het begin van het bord. De speler start een nieuwe "ronde" in de race. Wat als we in de toekomst een andere reden hebben om een nieuwe ronde te beginnen? Moet ons als
statement veranderen als we de onderliggende logica in de private methode veranderen? Absoluut niet! Dus laten we deze methode hernoemen in wat de als
vertegenwoordigt, in wat er gebeurt, niet waar we naar zoeken.
if ($ this-> playerShouldStartANewLap ($ lastPositionOnTheBoard))
Wanneer u methoden en variabelen probeert een naam te geven, moet u altijd bedenken wat de code moet doen en niet welke staat of staat deze vertegenwoordigt. Zodra u dit goed hebt gedaan, zal het hernoemen van acties in uw code aanzienlijk verminderen. Maar toch moet zelfs een ervaren programmeur een methode minstens drie tot vijf keer hernoemen voordat hij de juiste naam vindt. Dus wees niet bang om te slaan CTRL + r en vaak hernoemen. Verbind nooit uw wijzigingen in de VCS van het project als u de namen van uw nieuw toegevoegde methoden niet hebt gescand en uw code niet als goed geschreven proza wordt gelezen. Hernoemen is tegenwoordig zo goedkoop dat je dingen kunt hernoemen, gewoon om verschillende versies uit te proberen en terug te draaien met één druk op de knop.
De als
verklaring op regel 90 is hetzelfde als onze vorige. We kunnen onze geëxtraheerde methode gewoon opnieuw gebruiken. Voila, duplicatie geëlimineerd! En vergeet niet om uw tests af en toe uit te voeren, zelfs als u refactor bent door de magie van uw IDE te gebruiken. Dat leidt ons naar onze volgende waarneming. Magie, soms, mislukt. Bekijk regel 65.
$ lastPositionOnTheBoard = 11;
We declareren een variabele en gebruiken deze slechts op één plaats, als een parameter voor onze nieuw geëxtraheerde methode. Dit suggereert sterk dat de variabele binnen de methode zou moeten liggen.
private function playerShouldStartANewLap () $ lastPositionOnTheBoard = 11; return $ this-> places [$ this-> currentPlayer]> $ lastPositionOnTheBoard;
En vergeet niet om de methode zonder parameters in uw te bellen als
statements.
if ($ this-> playerShouldStartANewLap ())
De als
verklaringen in de askQuestion ()
methode lijkt in orde te zijn, evenals die in currentCategory ()
.
if ($ this-> inPenaltyBox [$ this-> currentPlayer])
Dit is een beetje ingewikkelder, maar in het domein en expressief genoeg.
if ($ this-> currentPlayer == count ($ this-> players))
We kunnen hieraan werken. Het is duidelijk dat de vergelijking betekent, als de huidige speler niet gebonden is. Maar zoals we hierboven hebben geleerd, willen we intentie niet verklaren.
if ($ this-> shouldResetCurrentPlayer ())
Dat is veel beter, en we zullen het opnieuw gebruiken op regel 172, 189 en 203. Duplicatie, ik bedoel triplicatie, ik bedoel quadruplicatie, geëlimineerd!
Testen zijn voorbij en allemaal als
uitspraken werden beoordeeld op complexiteit.
Er zijn verschillende lessen die kunnen worden getrokken uit het refactoren van conditionals. Allereerst helpen ze om de bedoeling van de code beter te begrijpen. Als u vervolgens de geëxtraheerde methode een naam geeft die de bedoeling correct weergeeft, vermijdt u toekomstige naamswijzigingen. Het vinden van duplicatie in logica is moeilijker dan het vinden van gedupliceerde regels met eenvoudige code. Je hebt misschien gedacht dat we een bewuste duplicatie zouden moeten doen, maar ik behandel liever duplicatie als ik eenheidstests heb waarmee ik mijn leven kan vertrouwen. De Gouden Meester is goed, maar het is hooguit een vangnet, geen parachute.
Bedankt voor het lezen en blijf op de hoogte voor onze volgende tutorial wanneer we onze eerste unit-tests introduceren.