I den sjette delen av serien snakket vi om å angripe lange metoder ved å utnytte parprogrammering og visningskode fra forskjellige nivåer. Vi zoomet kontinuerlig inn og ut, og observerte både små ting som navngivning samt form og innrykk.
I dag tar vi en annen tilnærming: Vi antar at vi er alene, ingen kollega eller par for å hjelpe oss. Vi vil bruke en teknikk kalt "Extract till you drop" som bryter koden i svært små biter. Vi vil gjøre alt vi kan for å gjøre disse brikkene så enkle å forstå som mulig, slik at fremtiden oss, eller noen annen programmør vil kunne forstå dem lett.
Jeg hørte først om dette konseptet fra Robert C. Martin. Han presenterte ideen i en av hans videoer som en enkel måte å refactor kode som er vanskelig å forstå.
Den grunnleggende ideen er å ta små, forståelige kodestykker og trekke dem ut. Det spiller ingen rolle om du identifiserer fire linjer eller fire tegn som kan trekkes ut. Når du identifiserer noe som kan innkapsles i et klarere konsept, trekker du ut. Du fortsetter denne prosessen både på den opprinnelige metoden og på de nylig ekstraherte stykkene til du ikke finner noe stykke kode som kan innkapsles som et konsept.
Denne teknikken er spesielt nyttig når du arbeider alene. Det tvinger deg til å tenke på både små og større biter av kode. Det har en annen fin effekt: Det gjør at du tenker på koden - mye! I tillegg til ekstraktmetoden eller variabel refactoring nevnt ovenfor, finner du deg selv å omdøpe variabler, funksjoner, klasser og mer.
La oss se et eksempel på noen tilfeldig kode fra Internett. Stackoverflow er et bra sted å finne små stykker koden. Her er en som bestemmer om et tall er førsteklasses:
// Sjekk om et tall er hovedfunksjonen erPrime ($ num, $ pf = null) if (! Is_array ($ pf)) for ($ i = 2; $ iPå dette punktet aner jeg ikke hvordan denne koden fungerer. Jeg har nettopp funnet det på Internett mens du skriver denne artikkelen, og jeg vil oppdage det sammen med deg. Prosessen som følger, er kanskje ikke den reneste. I stedet vil det gjenspeile min resonnement og refactoring som det skjer uten planlegging på forhånd.
Refactoring Prime Number Checker
Ifølge Wikipedia:
Et primaltall (eller en prime) er et naturlig tall større enn 1 som ikke har noen positive divisorer annet enn 1 og seg selv.Som du kan se dette er en enkel metode for et enkelt matematisk problem. Det kommer tilbake
ekte
ellerfalsk
, så det bør også være enkelt å teste.klasse IsPrimeTest utvider PHPUnit_Framework_TestCase funksjon testItCanRecognizePrimeNumbers () $ this-> assertTrue (isPrime (1)); // Sjekk om et tall er primærfunksjonen erPrime ($ num, $ pf = null) // ... innholdet av metoden sett ovenforNår vi bare spiller med eksempelkode, er den enkleste måten å gå å sette alt i en testfil. På denne måten trenger vi ikke å tenke på hvilke filer som skal opprettes, i hvilke kataloger de tilhører, eller hvordan de skal inkluderes i den andre. Dette er bare et enkelt eksempel å bruke for å bli kjent med teknikken før vi bruker den på en av trivia spillmetoder. Så, alt går i en testfil, du kan navnet som du ønsker. Jeg har valgt
IsPrimeTest.php
.Denne testen passerer. Mitt neste instinkt er å legge til noen flere primtal og enn skrive en annen test med ikke primtal.
funksjon testItCanRecognizePrimeNumbers () $ this-> assertTrue (isPrime (1)); $ Dette-> assertTrue (isPrime (2)); $ Dette-> assertTrue (isPrime (3)); $ Dette-> assertTrue (isPrime (5)); $ Dette-> assertTrue (isPrime (7)); $ Dette-> assertTrue (isPrime (11));Det går. Men hva med dette?
funksjon testItCanRecognizeNonPrimes () $ this-> assertFalse (isPrime (6));Dette mislykkes uventet: 6 er ikke et hovednummer. Jeg ventet metoden å returnere
falsk
. Jeg vet ikke hvordan metoden fungerer, eller formålet med$ pf
parameter - jeg var bare forventer at den skulle komme tilbakefalsk
basert på navn og beskrivelse. Jeg aner ikke hvorfor det ikke virker, eller hvordan man kan fikse det.Dette er ganske forvirrende dilemma. Hva skal vi gjøre? Det beste svaret er å skrive tester som går for et anstendig volum tall. Vi må kanskje prøve å gjette, men vi vil i det minste ha en ide om hva metoden gjør. Da kan vi begynne å refactoring den.
funksjonstestFirst20NaturalNumbers () for ($ i = 1; $ i<20;$i++) echo $i . ' - ' . (isPrime($i) ? 'true' : 'false') . "\n";Det utgir noe interessant:
1 - true 2 - true 3 - true 4 - true 5 - true 6 - true 7 - true 8 - true 9 - true 10 - false 11 - true 12 - false 13 - true 14 - false 15 - true 16 - false 17 - true 18 - false 19 - trueEt mønster begynner å dukke opp her. Alle sanne opptil 9, deretter vekslende opp til 19. Men er dette mønsteret gjentatt? Prøv å kjøre den for 100 tall, og du vil umiddelbart se at det ikke er det. Det ser faktisk ut til å jobbe for tall mellom 40 og 99. Det misfires en gang mellom 30-39 ved å nominere 35 som prime. Det samme gjelder i 20-29-serien. 25 regnes som prime.
Denne øvelsen som startet som en enkel kode for å demonstrere en teknikk, viser seg å være mye vanskeligere enn forventet. Jeg bestemte meg for å beholde det fordi det gjenspeiler virkeligheten på en typisk måte.
Hvor mange ganger begynte du å jobbe med en oppgave som så lett ut bare for å oppdage at det er ekstremt vanskelig?Vi ønsker ikke å fikse koden. Uansett hva metoden gjør, bør det fortsette å gjøre det. Vi vil refactor den for å få andre til å forstå det bedre.
Som det ikke forteller hovedtal på riktig måte, vil vi bruke den samme Golden Master-tilnærmingen vi lærte i leksjon 1.
funksjon testGenerateGoldenMaster () for ($ i = 1; $ i<10000;$i++) file_put_contents(__DIR__ . '/IsPrimeGoldenMaster.txt', $i . ' - ' . (isPrime($i) ? 'true' : 'false') . "\n", FILE_APPEND);Kjør dette en gang for å generere Golden Master. Det skal løpe fort. Hvis du trenger å kjøre den igjen, ikke glem å slette filen før du utfører testen. Ellers blir utgangen festet til forrige innhold.
funksjon testMatchesGoldenMaster () $ goldenMaster = fil (__ DIR__. '/IsPrimeGoldenMaster.txt'); for ($ i = 1; $ i<10000;$i++) $actualResult = $i . ' - ' . (isPrime($i) ? 'true' : 'false'). "\n"; $this->assertTrue (in_array ($ actualResult, $ goldenMaster), 'Verdien'. $ actualResult. 'er ikke i den gyldne mesteren.');Skriv nå testen for den gyldne mesteren. Denne løsningen kan ikke være den raskeste, men det er lett å forstå, og det vil fortelle oss nøyaktig hvilket nummer som ikke samsvarer hvis det bryter noe. Men det er en liten duplisering i de to testmetodene vi kunne trekke ut i en
privat
metode.klasse IsPrimeTest utvider PHPUnit_Framework_TestCase funksjon testGenerateGoldenMaster () $ this-> markTestSkipped (); for ($ i = 1; $ i<10000;$i++) file_put_contents(__DIR__ . '/IsPrimeGoldenMaster.txt', $this->getPrimeResultAsString ($ i), FILE_APPEND); funksjonstestMatchesGoldenMaster () $ goldenMaster = fil (__ DIR__. '/IsPrimeGoldenMaster.txt'); for ($ i = 1; $ i<10000;$i++) $actualResult = $this->getPrimeResultAsString ($ i); $ this-> assertTrue (in_array ($ actualResult, $ goldenMaster), 'Verdien'. $ actualResult. 'er ikke i den gyldne mesteren.'); privat funksjon getPrimeResultAsString ($ i) return $ i. '-'. (isPrime ($ i)? 'true': 'false'). "\ N";Nå kan vi flytte un til vår produksjonskode. Testen går i løpet av omtrent to sekunder på datamaskinen min, så det er håndterbart.
Utvinning Alt Vi Kan
Først kan vi trekke ut en
isDivisible ()
metode i første del av koden.hvis (! is_array ($ pf)) for ($ i = 2; $ iDet vil gjøre det mulig for oss å gjenbruke koden i den andre delen slik:
ellers $ pfCount = count ($ pf); for ($ i = 0; $ i<$pfCount;$i++) if(isDivisible($num, $pf[$i])) return false; return true;Og så snart vi begynte å jobbe med denne koden, så vi at det var uforsiktig. Braces er noen ganger på begynnelsen av linjen, andre ganger på slutten.
Noen ganger brukes faner for innrykk, noen ganger mellomrom. Noen ganger er det mellomrom mellom operand og operatør, noen ganger ikke. Og nei, dette er ikke spesielt opprettet kode. Dette er virkeligheten. Real kode, ikke noe kunstig trening.
// Sjekk om et tall er hovedfunksjonen erPrime ($ num, $ pf = null) if (! Is_array ($ pf)) for ($ i = 2; $ i < intval(sqrt($num)); $i++) if (isDivisible($num, $i)) return false; return true; else $pfCount = count($pf); for ($i = 0; $i < $pfCount; $i++) if (isDivisible($num, $pf[$i])) return false; return true;Det ser bedre ut. Umiddelbart de to
hvis
uttalelser ser veldig like ut. Men vi kan ikke trekke dem ut på grunn avkomme tilbake
uttalelser. Hvis vi ikke kommer tilbake, vil vi bryte logikken.Hvis den ekstraherte metoden ville returnere en boolsk og vi sammenligner den for å avgjøre om vi skal eller ikke kommer tilbake fra
isPrime ()
, det ville ikke hjelpe i det hele tatt. Det kan være en måte å pakke ut ved å bruke noen funksjonelle programmeringskonsepter i PHP, men kanskje senere. Vi kan gjøre noe enklere først.funksjon erPrime ($ num, $ pf = null) if (! is_array ($ pf)) return checkDivisorsBetween (2, intval (sqrt ($ num)), $ num); ellers $ pfCount = count ($ pf); for ($ i = 0; $ i < $pfCount; $i++) if (isDivisible($num, $pf[$i])) return false; return true; function checkDivisorsBetween($start, $end, $num) for ($i = $start; $i < $end; $i++) if (isDivisible($num, $i)) return false; return true;Ekstrahering av
til
sløyfen som helhet er litt lettere, men når vi prøver å gjenbruke vår ekstraherte metode i den andre delen avhvis
vi kan se at det ikke vil fungere. Det er dette mystiske$ pf
variabel som vi nesten ikke vet om.Det ser ut til at det kontrolleres om tallet er delbart med et sett av bestemte divisorer i stedet for å ta alle tall opp til den andre magiske verdien bestemt av
intval (sqrt ($ num))
. Kanskje vi kunne gi nytt navn$ pf
inn i$ divisorene
.funksjon erPrime ($ num, $ divisors = null) if (! is_array ($ divisors)) return checkDivisorsBetween (2, intval (sqrt ($ num)), $ num); ellers return checkDivisorsBetween (0, count ($ divisors), $ num, $ divisors); funksjonskontrollDivisorsBetween ($ start, $ end, $ num, $ divisors = null) for ($ i = $ start; $ i < $end; $i++) if (isDivisible($num, $divisors ? $divisors[$i] : $i)) return false; return true;Dette er en måte å gjøre det på. Vi la til en ekstra valgfri parameter til vår kontrollmetode. Hvis den har en verdi, bruker vi den, ellers bruker vi
$ i
.Kan vi trekke ut noe annet? Hva med dette koden:
intval (sqrt ($ num))
?funksjon erPrime ($ num, $ divisors = null) if (! is_array ($ divisors)) return checkDivisorsBetween (2, integerRootOf ($ num), $ num); ellers return checkDivisorsBetween (0, count ($ divisors), $ num, $ divisors); funksjon integerRootOf ($ num) return intval (sqrt ($ num));Er det ikke så bra? Noe. Det er bedre hvis personen som kommer etter oss ikke vet hva
intval ()
ogsqrt ()
gjør det, men det hjelper ikke å gjøre logikken enklere å forstå. Hvorfor slutter vi vårtil
loop på det bestemte nummeret? Kanskje dette er spørsmålet vårt funksjonsnavn skal svare på.[PHP] // Sjekk om et tall er primærfunksjonen erPrime ($ num, $ divisors = null) if (! Is_array ($ divisors)) return checkDivisorsBetween (2, highestPossibleFactor ($ num), $ num); ellers return checkDivisorsBetween (0, count ($ divisors), $ num, $ divisors); funksjon highestPossibleFactor ($ num) return intval (sqrt ($ num)); [PHP]Det er bedre som det forklarer hvorfor vi stopper der. Kanskje i fremtiden kan vi finne en annen formel for å bestemme det nummeret. Navngivelsen introduserte også en liten inkonsekvens. Vi kalte tallfaktorene, som er et synonym for divisors. Kanskje vi burde plukke en og bruke det bare. Jeg vil la deg gjøre omdøpet refactoring som en øvelse.
Spørsmålet er, kan vi trekke ut noe videre? Vel, vi må prøve til vi slipper. Jeg nevnte den funksjonelle programmeringssiden av PHP noen få avsnitt over. Det er to hovedfunksjonelle programmeringsegenskaper som vi enkelt kan bruke i PHP: Førsteklasses funksjoner og rekursjon. Når jeg ser en
hvis
uttalelse med akomme tilbake
inne i atil
loop, som i vårcheckDivisorsBetween ()
metode, tenker jeg på å bruke en eller begge teknikker.funksjonskontrollDivisorerBetween ($ start, $ end, $ num, $ divisors = null) for ($ i = $ start; $ i < $end; $i++) if (isDivisible($num, $divisors ? $divisors[$i] : $i)) return false; return true;Men hvorfor skal vi gå gjennom en så kompleks tankeprosess? Den mest irriterende grunnen er at denne metoden gjør to forskjellige ting: Den sykler og bestemmer. Jeg vil bare at den skal sykle og legge avgjørelsen til en annen metode. En metode bør alltid gjøre en ting og gjøre det bra.
funksjonscheckDivisorerBetween ($ start, $ end, $ num, $ divisors = null) $ numberIsNotPrime = funksjon ($ num, $ divisor) hvis (erDivisible ($ num, $ divisor)) return false; ; for ($ i = $ start; $ i < $end; $i++) $numberIsNotPrime($num, $divisors ? $divisors[$i] : $i); return true;Vårt første forsøk var å trekke ut betingelsen og returoppstillingen i en variabel. Dette er lokalt, for øyeblikket. Men koden virker ikke. Egentlig
til
sløyfe kompliserer ting ganske mye. Jeg har en følelse av at en liten rekursjon vil hjelpe.funksjon checkRecursiveDivisibility ($ current, $ end, $ num, $ divisor) hvis ($ current == $ end) return true;Når vi tenker på rekursivitet, må vi alltid begynne med de unike tilfellene. Vårt første unntak er når vi når slutten av vår rekursjon.
funksjon checkRecursiveDivisibility ($ current, $ end, $ num, $ divisor) hvis ($ current == $ end) return true; hvis (isDivisible ($ num, $ divisor)) return false;Vårt andre eksepsjonelle tilfelle som vil bryte rekursjonen er når nummeret er delbart. Vi ønsker ikke å fortsette. Og det handler om alle de unike tilfellene.
ini_set ('xdebug.max_nesting_level', 10000); funksjonskontrollDivisorsBetween ($ start, $ end, $ num, $ divisors = null) return checkRecursiveDivisibility ($ start, $ end, $ num, $ divisors); funksjon checkRecursiveDivisibility ($ current, $ end, $ num, $ divisors) hvis ($ current == $ end) return true; hvis (erDivisible ($ num, $ divisors? $ divisors [$ current]: $ current)) return false; checkRecursiveDivisibility ($ current ++, $ end, $ num, $ divisors);Dette er et annet forsøk på å bruke rekursjon for vårt problem, men dessverre fører tilbakevendende 10.000 ganger i PHP til et krasj av PHP eller PHPUnit på systemet mitt. Så dette ser ut til å være en annen død. Men hvis det ville vært jobbet, ville det vært en fin erstatning av den opprinnelige logikken.
Utfordring
Da jeg skrev den Gyldne Mester, oversett jeg med vilje noe. La oss bare si at testene ikke dekker så mye kode som de burde. Kan du få øye på problemet? Hvis ja, hvordan vil du nærme deg det?
Siste tanker
"Extract till you drop" er en god måte å dissekere lange metoder. Det tvinger deg til å tenke på små stykker kode og å gi brikkene en hensikt ved å trekke dem ut i metoder. Jeg finner det utrolig hvordan denne enkle prosedyren sammen med hyppig omdøping kan hjelpe meg med å oppdage at noen kode gjør ting jeg aldri trodde var mulig.
I vår neste og siste opplæring om refactoring, vil vi bruke denne teknikken til trivia spillet. Jeg håper du likte denne opplæringen som viste seg å være litt annerledes. I stedet for å snakke om eksempler på lærebok, tok vi noen ekte kode, og vi måtte kjempe med de virkelige problemene vi møter hver dag.