Gammel kode. Ugyldig kode. Komplisert kode. Spaghetti kode. Gibberish tull. I to ord, Legacy Code. Dette er en serie som vil hjelpe deg med å jobbe og håndtere det.
Jeg liker å tenke på kode akkurat som jeg tenker på prosa. Lange, nestede, sammensatte setninger med eksotiske ord er vanskelig å forstå. Fra tid til annen trenger du en, men mesteparten av tiden kan du bare bruke enkle enkle ord i korte setninger. Dette er veldig sant for kildekoden også. Komplekse tilstander er vanskelig å forstå. Lange metoder er som endeløse setninger.
Her er et "prosaisk" eksempel for å oppmuntre deg. Først all-in-one setningen. Den stygge.
Hvis temperaturen i serverrommet er under fem grader, og fuktigheten stiger over femti prosent, men forblir under åtti, og lufttrykket er stabilt, må senior tekniker John, som har minst tre års arbeidserfaring i nettverk og serveradministrasjon bli varslet, og han må våkne midt om natten, kle seg, gå ut, ta bilen eller ringe en drosje hvis han ikke har bil, kjøre til kontoret, gå inn i bygningen, starte klimaanlegget og vent til temperaturen stiger over ti grader og fuktigheten faller under tjue prosent.
Hvis du kan forstå, forstå, og husk at avsnitt uten å lese det igjen, gir jeg deg en medalje (virtuell selvfølgelig). Lange, entanglede avsnitt skrevet i en enkelt komplisert setning er vanskelig å forstå. Dessverre vet jeg ikke nok eksotiske engelske ord for å gjøre det enda vanskeligere å forstå.
La oss finne en måte å forenkle det litt på. Hele sin første del, inntil "da" er en tilstand. Ja, det er komplisert, men vi kan oppsummere det slik: Hvis miljøforholdene representerer en risiko ... ... så skal noe gjøres. Det kompliserte uttrykket sier at vi bør varsle noen som tilfredsstiller mange forhold: deretter varsle nivå tre teknisk støtte. Endelig er en hel prosess beskrevet fra å våkne teknikkannen til alt er løst: og forvent at miljøet skal gjenopprettes innenfor normale parametere. La oss sette alt sammen.
Hvis miljøforholdene utgjør en risiko, informer nivå tre teknisk støtte og forvent at miljøet skal gjenopprettes innenfor normale parametere.
Nå er det bare ca 20% i lengde i forhold til den opprinnelige teksten. Vi vet ikke detaljene, og i det store flertallet av tilfellene bryr vi oss ikke om. Og dette er veldig sant for kildekoden også. Hvor mange ganger bryr du deg om implementeringsdetaljer av a logInfo ("Noen meldinger");
metode? Sannsynligvis en gang, hvis og når du implementerte det. Da logger det bare meldingen inn i kategorien "info". Eller når en bruker kjøper en av produktene dine, bryr du deg om hvordan du fakturerer ham? Nei. Alt du vil bry deg om er Hvis produktet ble kjøpt, kast det fra lager og fakturere det til kjøperen. Eksemplene kan være uendelige. De er i utgangspunktet hvordan vi skriver riktig programvare.
I denne delen vil vi prøve å bruke prosafilosofien til trivia-spillet vårt. Ett skritt av gangen. Starter med komplekse conditionals. La oss begynne med litt enkel kode. Bare for å varme opp.
Linje tjue av GameRunner.php
filen leser slik:
hvis (rand ($ minAnswerId, $ maxAnswerId) == $ feilAnswerId)
Hvordan høres det ut i prosa? Hvis et tilfeldig tall mellom minimum svar ID og maksimal svar ID er lik feil svarets ID, så ...
Dette er ikke veldig komplisert, men vi kan fortsatt gjøre det enklere. Hva med dette? Hvis feil svar er valgt, så ... Bedre, er det ikke?
Vi trenger en måte, en prosedyre, en teknikk for å flytte den betingede utsagnet et annet sted. Det målet kan lett være en metode. Eller i vårt tilfelle, siden vi ikke er inne i en klasse her, en funksjon. Denne bevegelsen av atferd til en ny metode eller funksjon kalles refraktoring av "Extract Method". Nedenfor er trinnene, som definert av Martin Fowler i sin utmerkede bok Refactoring: Forbedre utformingen av eksisterende kode. Hvis du ikke leste denne boken, bør du sette den på listen "For å lese" nå. Det er en av de mest essensielle bøkene til en moderne programmerer.
For vår veiledning har jeg tatt de opprinnelige trinnene og forenklet dem litt for å bedre passe våre behov og vår type opplæring.
Nå er dette ganske komplisert. Imidlertid er utvinningsmetoden utvilsomt den mest brukte refactoring, bortsett fra å omdøpe kanskje. Så du må forstå sin mekanikk.
Heldigvis for oss, tilbyr moderne IDEer som PHPStorm gode refactoringverktøy, som vi har sett i opplæringen PHPStorm: Når IDE virkelig betyr noe. Så vi vil bruke funksjonene vi har med fingertuppene, i stedet for å gjøre alt for hånd. Dette er mindre feilaktig og mye, mye raskere.
Bare velg ønsket del av koden og Høyreklikk den.
IDE vil automatisk forstå at vi trenger tre parametre for å kjøre vår kode, og den vil foreslå følgende løsning.
// // // $ minAnswerId = 0; $ maxAnswerId = 9; $ wrongAnswerId = 7; funksjonen erCurrentAnswerWrong ($ minAnswerId, $ maxAnswerId, $ wrongAnswerId) return rand ($ minAnswerId, $ maxAnswerId) == $ wrongAnswerId; gjør $ terning = rand (0, 5) + 1; $ AGame-> roll ($ terninger); hvis (isCurrentAnswerWrong ($ minAnswerId, $ maxAnswerId, $ wrongAnswerId)) $ notAWinner = $ aGame-> feilAnswer (); ellers $ notAWinner = $ aGame-> wasCorrectlyAnswered (); mens ($ notAWinner);
Selv om denne koden er syntaktisk korrekt, vil den bryte våre tester. Mellom all den støyen som vises til oss i røde, blå og sorte farger, kan vi se hvorfor:
Fatal feil: Kan ikke omklare isCurrentAnswerWrong () (tidligere erklært i / home / csaba / Personal / Programming / NetTuts / Refactoring Legacy Code - Del 3: Komplekse betingede og lange metoder /Source/trivia/php/GameRunner.php:16) i / hjem / csaba / Personlig / Programmering / NetTuts / Refactoring Legacy Code - Del 3: Komplekse betingede og lange metoder / Source / trivia /php/GameRunner.php på linje 18
Det sier i utgangspunktet at vi vil deklarere funksjonen to ganger. Men hvordan kan det skje? Vi har det bare en gang i vår GameRunner.php
!
Ta en titt på testene. Det er en generateOutput ()
metode som gjør en kreve ()
på vår GameRunner.php
. Det kalles minst to ganger. Her er kilden til feilen.
Nå har vi et dilemma. På grunn av sådd av tilfeldig generator, må vi ringe denne koden med kontrollerte verdier.
privat funksjon generereOutput ($ seed) ob_start (); srand ($ frø); krever __DIR__. '/ ... /trivia/php/GameRunner.php'; $ output = ob_get_contents (); ob_end_clean (); returnere $ output;
Men det er ingen måte å erklære en funksjon to ganger i PHP, så vi trenger en annen løsning. Vi begynner å føle byrden av vår gyldne mestertesting. Kjører det hele 20000 ganger, hver gang vi bytter et stykke kode, kan det ikke være en løsning på lang sikt. Foruten det faktum at det tar tid å løpe, tvinger det oss til å endre koden vår for å imøtekomme måten vi tester på. Dette er vanligvis et tegn på dårlige tester. Koden skal endres, men gjør fortsatt testen, men endringene skal ha grunner til å endres, og kommer bare fra kildekoden.
Men nok snakk, vi trenger en løsning, selv en midlertidig vil gjøre for nå. Migrering til enhetstester vil starte med vår neste leksjon.
En måte å løse problemet på er å ta hele resten av koden inn GameRunner.php
og sett den i en funksjon. La oss si løpe()
include_once __DIR__. '/Game.php'; funksjonen erCurrentAnswerWrong ($ minAnswerId, $ maxAnswerId, $ wrongAnswerId) return rand ($ minAnswerId, $ maxAnswerId) == $ wrongAnswerId; funksjonsdrift () $ notAWinner; $ aGame = nytt spill (); $ AGame-> legge til ( "Chet"); $ AGame-> legge til ( "Pat"); $ AGame-> legge til ( "Sue"); $ minAnswerId = 0; $ maxAnswerId = 9; $ wrongAnswerId = 7; gjør $ terning = rand (0, 5) + 1; $ AGame-> roll ($ terninger); hvis (isCurrentAnswerWrong ($ minAnswerId, $ maxAnswerId, $ wrongAnswerId)) $ notAWinner = $ aGame-> feilAnswer (); ellers $ notAWinner = $ aGame-> wasCorrectlyAnswered (); mens ($ notAWinner);
Dette vil tillate oss å teste det, men vær oppmerksom på at kjører koden fra konsollen ikke vil kjøre spillet. Vi gjorde en liten endring i atferd. Vi fikk testbarhet på bekostning av en atferdsendring, som vi ikke ønsket å gjøre i utgangspunktet. Hvis du vil kjøre koden fra konsollen, trenger du nå en annen PHP-fil som inneholder eller krever løperen, og kaller eksplisitt kjøremetoden på den. Ikke så stor av en endring, men et must for å huske, spesielt hvis du har tredjeparter som bruker din eksisterende kode.
På den annen side kan vi nå bare inkludere filen i vår test.
krever __DIR__. '/ ... /trivia/php/GameRunner.php';
Og så ring løpe()
inne i generateOutput () -metoden.
privat funksjon generereOutput ($ seed) ob_start (); srand ($ frø); løpe(); $ output = ob_get_contents (); ob_end_clean (); returnere $ output;
Kanskje dette er en god mulighet til å tenke på strukturen av katalogene og filene våre. Det finnes ikke flere komplekse conditionals i vår GameRunner.php
, men før vi fortsetter å Game.php
fil, vi må ikke legge igjen et rotete bak oss. Våre GameRunner.php
kjører ikke noe lenger, og vi trengte å hacke metoder sammen for å gjøre det testbart, noe som brøt vårt offentlige grensesnitt. Årsaken til dette er at vi kanskje tester feil ting.
Vår test samtaler løpe()
i GameRunner.php
fil, som inkluderer Game.php
, spiller spillet og en ny gylden masterfil genereres. Hva om vi introduserer en annen fil? Vi lager GameRunner.php
å faktisk kjøre spillet ved å ringe løpe()
og ingenting annet. Så hva om det ikke er logikk der som kan gå galt og det ikke trengs noen tester, og så flytter vi vår nåværende kode til en annen fil?
Nå er dette en helt annen historie. Nå har våre tester tilgang til koden like under løperen. I utgangspunktet er våre tester bare løpere. Og selvfølgelig i vår nye GameRunner.php
Det vil bare være et anrop for å kjøre spillet. Dette er en sann løper, det gjør ingenting annet enn å ringe løpe()
metode. Ingen logikk betyr at det ikke er behov for tester.
require_once __DIR__. '/RunnerFunctions.php'; løpe();
Det er andre spørsmål vi kunne stille oss selv på dette tidspunktet. Trenger vi virkelig en RunnerFunctions.php
? Kunne ikke vi bare ta funksjonene derfra og flytte dem til Game.php
? Vi kan nok, men med vår nåværende forståelse av hvilken funksjon hører til hvor? Det er ikke nok. Vi finner et sted for vår metode i en kommende leksjon.
Vi har også prøvd å navngi filene våre etter hva koden i dem gjør. En er bare en gjeng med funksjoner for løperen, funksjoner vi, på dette punktet, anser å være sammen for å tilfredsstille løperens behov. Vil dette bli en klasse på et tidspunkt i fremtiden? Kan være. Kanskje ikke. For nå er det bra nok.
Hvis vi tar en titt på RunnerFunctions.php
fil, det er litt av et rot som vi har introdusert.
Vi definerer:
$ minAnswerId = 0; $ maxAnswerId = 9; $ wrongAnswerId = 7;
… inne i løpe()
metode. De har en enkelt grunn til å eksistere og et enkelt sted der de blir brukt. Hvorfor ikke bare definere dem i den metoden og bli kvitt parametrene helt og holdent?
funksjonen erCurrentAnswerWrong () $ minAnswerId = 0; $ maxAnswerId = 9; $ wrongAnswerId = 7; returnere rand ($ minAnswerId, $ maxAnswerId) == $ feilAnswerId;
Ok, tester passerer og koden er mye hyggeligere. Men ikke bra nok.
Det er mye lettere for det menneskelige sinn å forstå positiv resonnement. Så hvis du kan unngå negative conditionals, bør du alltid ta den banen. I vårt nåværende eksempel sjekker metoden for feil svar. Det ville være mye lettere å forstå en metode som sjekker for en gyldighet og negerer det når det trengs.
funksjonen erCurrentAnswerCorrect () $ minAnswerId = 0; $ maxAnswerId = 9; $ wrongAnswerId = 7; returnere rand ($ minAnswerId, $ maxAnswerId)! = $ wrongAnswerId;
Vi brukte navnet på reflekteringsmetoden. Dette er igjen, ganske komplisert hvis det brukes for hånd, men i noen IDE er det så enkelt som å treffe CTRL + r, eller velg det aktuelle alternativet i menyen. For å få våre tester pass, må vi også oppdatere vår betingede utsagn med en negasjon.
hvis (! erCurrentAnswerCorrect ()) $ notAWinner = $ aGame-> feilAnswer (); ellers $ notAWinner = $ aGame-> wasCorrectlyAnswered ();
Dette bringer oss et skritt nærmere vår forståelse av betingelsene. Ved hjelp av !
i en hvis()
uttalelse, faktisk hjelper. Det skiller seg ut og fremhever det faktum at noe faktisk er negert der. Men kan vi reversere dette for å unngå negasjon helt? Ja vi kan.
hvis (isCurrentAnswerCorrect ()) $ notAWinner = $ aGame-> wasCorrectlyAnswered (); else $ notAWinner = $ aGame-> wrongAnswer ();
Nå har vi ingen logisk negasjon ved å bruke !
, heller ikke leksikalsk negasjon ved å navngi og returnere de gale tingene. Alle disse trinnene gjorde vår betingede mye, mye lettere å forstå.
Game.php
Vi forenklet til det ytterste, RunnerFunctions.php
. La oss angripe vår Game.php
filen nå. Det er flere måter du kan søke etter conditionals. Hvis du foretrekker det, kan du bare skanne koden ved å se på den. Dette er tregere, men har merverdien av å tvinge deg til å prøve å forstå det i rekkefølge.
Den andre åpenbare måten å søke etter conditionals, er å bare gjøre et søk for "hvis" eller "hvis". Hvis du formaterte koden med de innebygde funksjonene til IDE, kan du være sikker på at alle betingede utsagn har Samme spesifikke form. I mitt tilfelle er det mellomrom mellom "if" og parentes. Hvis du bruker det innebygde søket, vil de funnet resultatene bli fremhevet i en sterk farge, i min tilfelle gul.
Nå som vi alle har lyst opp vår kode som et juletre, kan vi ta dem en etter en. Vi kjenner boret, vi vet hvilke teknikker vi kan bruke, det er på tide å bruke dem.
hvis ($ this-> inPenaltyBox [$ this-> currentPlayer])
Dette virker ganske rimelig. Vi kunne trekke den ut i en metode, men ville det være et navn på den metoden for å gjøre tilstanden klarere?
hvis ($ roll% 2! = 0)
Jeg vedder 90% av alle programmerere kan forstå problemet i det ovennevnte hvis
uttalelse. Vi prøver å konsentrere seg om hva vår nåværende metode gjør. Og vår hjerne er koblet til domenet til problemet. Vi ønsker ikke å "starte en annen tråd" for å beregne det matematiske uttrykket for å forstå at det bare sjekker om et tall er merkelig. Dette er en av de små distraksjoner som kan ødelegge et vanskelig, logisk fradrag. Så jeg sier la oss trekke den ut.
hvis ($ this-> isOdd ($ roll))
Det er bedre fordi det handler om problemets domene og krever ingen ekstra hjernekraft.
hvis ($ this-> plasserer [$ this-> currentPlayer]> $ lastPositionOnTheBoard)
Dette ser ut til å være en annen god kandidat. Det er ikke så vanskelig å forstå som et matematisk uttrykk, men igjen er det et uttrykk som trenger sidebehandling. Jeg spør meg selv, hva betyr det om nåværende spillers posisjon nådde slutten av styret? Kan vi ikke uttrykke denne tilstanden på en mer konsistent måte? Vi kan sannsynligvis.
hvis ($ this-> playerReachedEndOfBoard ($ lastPositionOnTheBoard))
Dette er bedre. Men hva skjer faktisk inne i hvis
? Spilleren er reposisjonert i begynnelsen av styret. Spilleren starter en ny "runde" i løpet. Hva om vi i fremtiden vil ha en annen grunn til å starte et nytt runde? Skal våre hvis
erklæringen endres når vi endrer den underliggende logikken i den private metoden? Absolutt ikke! Så, la oss omdøpe denne metoden til hva hvis
representerer, i hva som skjer, ikke det vi søker etter.
hvis ($ this-> playerShouldStartANewLap ($ lastPositionOnTheBoard))
Når du prøver å navngi metoder og variabler, tenk alltid på hva koden skal gjøre, og ikke hvilken tilstand eller tilstand den representerer. Når du får det riktig, vil omdøping av handlinger i koden din synke betydelig. Men fortsatt, selv en erfaren programmør må omdøpe en metode minst tre til fem ganger før han finner sitt riktige navn. Så vær ikke redd for å treffe CTRL + r og omdøpe ofte. Gjør aldri endringene i prosjektets VCS hvis du ikke skann navnene på de nylig lagt til metodene, og koden din leser ikke som godt skrevet prosa. Omdøping er så billig i våre dager, at du kan endre navn på ting bare for å prøve ut forskjellige versjoner og gå tilbake med en enkelt knapptrykking.
De hvis
uttalelse på linje 90 er det samme som vår forrige. Vi kan bare gjenbruke vår ekstraherte metode. Voila, duplisering eliminert! Og ikke glem å kjøre tester nå og da, selv når du refactor ved å bruke din IDEs magi. Som fører oss til vår neste observasjon. Magisk, noen ganger, mislykkes. Sjekk ut linje 65.
$ lastPositionOnTheBoard = 11;
Vi erklærer en variabel og bruker den bare på et enkelt sted, som en parameter til vår nylig hentede metode. Dette tyder sterkt på at variabelen skal være inne i metoden.
privatfunksjon playerShouldStartANewLap () $ lastPositionOnTheBoard = 11; returnere $ this-> steder [$ this-> currentPlayer]> $ lastPositionOnTheBoard;
Og ikke glem å ringe metoden uten noen parametere i din hvis
uttalelser.
hvis ($ this-> playerShouldStartANewLap ())
De hvis
uttalelser i spør spørsmål()
Metoden synes å være ok, så vel som de som er i currentCategory ()
.
hvis ($ this-> inPenaltyBox [$ this-> currentPlayer])
Dette er litt mer komplisert, men i domenet og uttrykksfulle nok.
hvis ($ this-> currentPlayer == count ($ this-> spillere))
Vi kan jobbe med denne. Det er åpenbart at sammenligningen betyr at den nåværende spilleren er ute av bundet. Men som vi lærte over, ønsker vi ikke intensjon.
hvis ($ this-> shouldResetCurrentPlayer ())
Det er mye bedre, og vi vil gjenbruke det på linje 172, 189 og 203. Duplisering, jeg mener treplikasjon, mener jeg firedobling, eliminert!
Tester går forbi og alt hvis
uttalelser ble vurdert for kompleksitet.
Det er flere leksjoner som kan læres av refactoring conditionals. Først av alt hjelper de bedre med å forstå kodenes hensikt. Så hvis du heter den ekstraherte metoden for å representere hensikten riktig, vil du unngå fremtidige navneendringer. Å finne duplisering i logikk er vanskeligere enn å finne dupliserte linjer med enkel kode. Du har kanskje trodd at vi skulle gjøre en bevisst duplisering, men jeg foretrekker å håndtere duplisering når jeg har enhetstester som jeg kan stole på mitt liv med. Den gylne mesteren er god, men det er på det meste et sikkerhetsnett, ikke en fallskjerm.
Takk for at du leser og hold deg innstilt for vår neste opplæring når vi skal introdusere våre første enhetstester.