I den sjätte delen av vår serie pratade vi om att angripa långa metoder genom att utnyttja parprogrammering och visningskod från olika nivåer. Vi zoomade kontinuerligt in och ut och observerade både små saker som namn och form och inryckning.
Idag tar vi ett annat tillvägagångssätt: Vi antar att vi är ensamma, ingen kollega eller par för att hjälpa oss. Vi använder en teknik som heter "Extract till you drop" som bryter upp koden i mycket små bitar. Vi kommer att göra alla ansträngningar vi kan för att göra dessa stycken så lätt att förstå som möjligt så att framtiden oss, eller någon annan programmerare kommer att kunna förstå dem lätt.
Jag hörde först om detta koncept från Robert C. Martin. Han presenterade idén i en av hans videor som ett enkelt sätt att refactor kod som är svår att förstå.
Grundtanken är att ta små, förståeliga bitar av kod och extrahera dem. Det spelar ingen roll om du identifierar fyra rader eller fyra tecken som kan extraheras. När du identifierar något som kan inkapslas i ett tydligare koncept, extraherar du. Du fortsätter denna process både på den ursprungliga metoden och på de nyligen extraherade bitarna tills du inte hittar någon kod som kan inkapslas som ett koncept.
Denna teknik är särskilt användbar när du arbetar ensam. Det tvingar dig att tänka på både små och större bitar av kod. Det har en annan fin effekt: Det får dig att tänka på koden - mycket! Förutom extraktmetoden eller variabel refactoring som nämns ovan kommer du att hitta dig själv omdirigering av variabler, funktioner, klasser och mer.
Låt oss se ett exempel på en del slumpmässig kod från Internet. Stackoverflow är ett bra ställe att hitta små bitar av kod. Här är en som bestämmer om ett tal är prime:
// Kontrollera om ett tal är primära funktionen är Prime ($ num, $ pf = null) if (! Is_array ($ pf)) for ($ i = 2; $ iVid denna tidpunkt har jag ingen aning om hur den här koden fungerar. Jag hittade det på Internet medan du skrev den här artikeln, och jag kommer att upptäcka den tillsammans med dig. Processen som följer kanske inte är den renaste. Istället kommer det att återspegla min resonemang och refactoring som det händer utan uppskjutande planering.
Refactoring Prime Number Checker
Enligt Wikipedia:
Ett primärt tal (eller en primär) är ett naturligt tal större än 1 som inte har några positiva divisorer andra än 1 och sig själv.Som du kan se är det en enkel metod för ett enkelt matematiskt problem. Det återvänder
Sann
ellerfalsk
, så det ska också vara lätt att testa.klassen IsPrimeTest utökar PHPUnit_Framework_TestCase funktion testItCanRecognizePrimeNumbers () $ this-> assertTrue (isPrime (1)); // Kontrollera om ett tal är huvudfunktionen isPrime ($ num, $ pf = null) // ... innehållet i metoden som sett ovanNär vi bara spelar med exempelkod är det enklaste sättet att gå att sätta allt i en testfil. På detta sätt behöver vi inte tänka på vilka filer som ska skapas, i vilka kataloger de tillhör eller hur de ska inkluderas i den andra. Detta är bara ett enkelt exempel att använda för att bekanta oss med tekniken innan vi tillämpar den på en av trivia-spelmetoderna. Så, allt går i en testfil, du kan namnge som du önskar. jag har valt
IsPrimeTest.php
.Detta test passerar. Min nästa instinkt är att lägga till några fler primtal och än skriva ett annat test med inte primtal.
funktionstestItCanRecognizePrimeNumbers () $ this-> assertTrue (isPrime (1)); $ This-> assertTrue (isPrime (2)); $ This-> assertTrue (isPrime (3)); $ This-> assertTrue (isPrime (5)); $ This-> assertTrue (isPrime (7)); $ This-> assertTrue (isPrime (11));Det passerar. Men vad sägs om detta?
funktion testItCanRecognizeNonPrimes () $ this-> assertFalse (isPrime (6));Detta misslyckas oväntat: 6 är inte ett huvudtal. Jag väntade metoden att återvända
falsk
. Jag vet inte hur metoden fungerar, eller syftet med$ pf
parameter - jag förväntade mig bara att den skulle återvändafalsk
baserat på dess namn och beskrivning. Jag har ingen aning om varför det inte fungerar eller hur man fixar det.Detta är ganska förvirrande dilemma. Vad ska vi göra? Det bästa svaret är att skriva tester som passerar för en anständig volym antal. Vi kan behöva försöka gissa, men vi kommer åtminstone att ha en aning om vad metoden gör. Då kan vi börja refactoring det.
funktionstestFirst20NaturalNumbers () for ($ i = 1; $ i<20;$i++) echo $i . ' - ' . (isPrime($i) ? 'true' : 'false') . "\n";Det ger något intressant:
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 - sant 18 - falskt 19 - santEtt mönster börjar dyka upp här. Alla sanna upp till 9, sedan växla upp till 19. Men upprepar detta mönster? Försök att köra den för 100 nummer och du kommer genast se att det inte är det. Det verkar faktiskt fungera för siffror mellan 40 och 99. Det misfires en gång mellan 30-39 genom att nominera 35 som prime. Samma gäller i 20-29-serien. 25 anses vara prime.
Denna övning som startade som en enkel kod för att visa en teknik visar sig vara mycket svårare än förväntat. Jag bestämde mig för att behålla det eftersom det speglar det verkliga livet på ett typiskt sätt.
Hur många gånger började du arbeta med en uppgift som såg lätt ut för att upptäcka att det är extremt svårt?Vi vill inte fixa koden. Vad som än gör, borde det fortsätta att göra det. Vi vill refactor det för att få andra att förstå det bättre.
Eftersom det inte talar primtal på ett korrekt sätt använder vi samma Golden Master-tillvägagångssätt som vi lärde oss i lektion 1.
funktion testGenerateGoldenMaster () for ($ i = 1; $ i<10000;$i++) file_put_contents(__DIR__ . '/IsPrimeGoldenMaster.txt', $i . ' - ' . (isPrime($i) ? 'true' : 'false') . "\n", FILE_APPEND);Kör detta en gång för att generera Golden Master. Det borde springa fort. Om du behöver åter köra den, glöm inte att ta bort filen innan du kör testet. Annars kommer utmatningen att kopplas till föregående innehåll.
funktion testMatchesGoldenMaster () $ goldenMaster = fil (__ DIR__. '/IsPrimeGoldenMaster.txt'); för ($ i = 1; $ i<10000;$i++) $actualResult = $i . ' - ' . (isPrime($i) ? 'true' : 'false'). "\n"; $this->assertTrue (in_array ($ actualResult, $ goldenMaster), 'Värdet'. $ actualResult. 'finns inte i den gyllene mästaren.');Skriv nu testet för den gyllene mästaren. Den här lösningen kan inte vara den snabbaste, men det är lätt att förstå och det kommer att berätta för oss exakt vilket nummer som inte matchar om bryta något. Men det finns en liten dubbelarbete i de två testmetoderna vi kan extrahera till en
privat
metod.klassen IsPrimeTest utökar PHPUnit_Framework_TestCase funktion testGenerateGoldenMaster () $ this-> markTestSkipped (); för ($ i = 1; $ i<10000;$i++) file_put_contents(__DIR__ . '/IsPrimeGoldenMaster.txt', $this->getPrimeResultAsString ($ i), FILE_APPEND); funktionstestMatchesGoldenMaster () $ goldenMaster = fil (__ DIR__. '/IsPrimeGoldenMaster.txt'); för ($ i = 1; $ i<10000;$i++) $actualResult = $this->getPrimeResultAsString ($ i); $ this-> assertTrue (in_array ($ actualResult, $ goldenMaster), 'Värdet'. $ actualResult. 'är inte i den gyllene mästaren.'); privat funktion getPrimeResultAsString ($ i) returnera $ i. '-'. (isPrime ($ i)? 'true': 'false'). "\ N";Nu kan vi flytta un till vår produktionskod. Testet körs i ungefär två sekunder på min dator, så det är hanterbart.
Extrahera allt vi kan
Först kan vi extrahera en
isDivisible ()
metod i kodens första del.om (! is_array ($ pf)) för ($ i = 2; $ iDet gör det möjligt för oss att återanvända koden i den andra delen så här:
annars $ pfCount = count ($ pf); för ($ i = 0; $ i<$pfCount;$i++) if(isDivisible($num, $pf[$i])) return false; return true;Och så snart vi började arbeta med den här koden såg vi att det är oslagbart i linje med varandra. Hängslen är ibland vid början av linjen, andra gånger i slutet.
Ibland används flikar för indryckning, ibland mellanrum. Ibland finns det mellanrum mellan operand och operatör, ibland inte. Och nej, det här är inte särskilt skapad kod. Detta är verkligheten. Verklig kod, inte någon artificiell övning.
// Kontrollera om ett tal är primära funktionen är Prime ($ 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 bättre ut. Omedelbart de två
om
uttalanden ser väldigt likartade ut. Men vi kan inte extrahera dem på grund avlämna tillbaka
uttalanden. Om vi inte kommer tillbaka kommer vi att bryta logiken.Om den extraherade metoden skulle returnera en booleska och vi jämför det för att bestämma om vi ska eller inte återvända från
isPrime ()
, det skulle inte alls hjälpa. Det kan finnas ett sätt att extrahera det genom att använda några funktionella programmeringskoncept i PHP, men kanske senare. Vi kan göra någonting enklare först.funktionen ärPrime ($ num, $ pf = null) if (! is_array ($ pf)) return checkDivisorsBetween (2, intval (sqrt ($ num)), $ num); annars $ pfCount = count ($ pf); för ($ 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;Extraktionen av
för
slingan som helhet är lite lättare, men när vi försöker återanvända vår extraherade metod i den andra delen avom
vi kan se att det inte kommer att fungera. Det är här mystiskt$ pf
variabel som vi nästan inte vet om.Det verkar som om det kontrolleras om numret är delbart med en uppsättning specifika divisorer istället för att ta alla siffror upp till det andra magiska värdet bestämt av
intval (sqrt ($ num))
. Kanske kan vi byta namn$ pf
in i$ delare
.funktionen är Premium ($ num, $ divisors = null) if (! is_array ($ divisors)) return checkDivisorsBetween (2, intval (sqrt ($ num)), $ num); annars return checkDivisorsBetween (0, count ($ divisors), $ num, $ divisors); funktionskontrollDivisorerBetween ($ start, $ end, $ num, $ divisors = null) for ($ i = $ start; $ i < $end; $i++) if (isDivisible($num, $divisors ? $divisors[$i] : $i)) return false; return true;Detta är ett sätt att göra det. Vi lade till en ytterligare valfri parameter till vår kontrollmetod. Om det har ett värde använder vi det, annars använder vi
$ i
.Kan vi extrahera något annat? Vad sägs om den här koden:
intval (sqrt ($ num))
?funktionen är Premium ($ num, $ divisors = null) if (! is_array ($ divisors)) return checkDivisorsBetween (2, integerRootOf ($ num), $ num); annars return checkDivisorsBetween (0, count ($ divisors), $ num, $ divisors); funktion integerRootOf ($ num) return intval (sqrt ($ num));Är inte det bättre? Något. Det är bättre om den som kommer efter oss inte vet vad
intval ()
ochsqrt ()
gör det, men det hjälper inte att göra logiken lättare att förstå. Varför avslutar vi vårför
slinga vid det specifika numret? Kanske är det här frågan vårt funktionsnamn ska svara på.[PHP] // Kontrollera om ett tal är den främsta funktionen är Prime ($ num, $ divisors = null) if (! Is_array ($ divisors)) return checkDivisorsBetween (2, highestPossibleFactor ($ num), $ num); annars return checkDivisorsBetween (0, count ($ divisors), $ num, $ divisors); funktion highestPossibleFactor ($ num) return intval (sqrt ($ num)); [PHP]Det är bättre som det förklarar varför vi slutar där. Kanske i framtiden kan vi uppfinna en annan formel för att bestämma det numret. Namngivningen introducerade också en liten inkonsekvens. Vi kallade talfaktorerna, vilket är en synonym för divisorer. Kanske borde vi välja en och bara använda det. Jag låter dig göra omprövningen som en övning.
Frågan är, kan vi extrahera längre? Tja, vi måste försöka tills vi släpper. Jag nämnde PHP: s funktionella programmeringssida några stycken ovan. Det finns två huvudsakliga funktionella programmeringsegenskaper som vi enkelt kan tillämpa i PHP: förstklassiga funktioner och rekursion. När jag ser en
om
uttalande med alämna tillbaka
inuti aför
slinga, som i vårcheckDivisorsBetween ()
metod, jag tänker på att tillämpa en eller båda teknikerna.funktion checkDivisorsBetween ($ start, $ end, $ num, $ divisors = null) for ($ i = $ start; $ i < $end; $i++) if (isDivisible($num, $divisors ? $divisors[$i] : $i)) return false; return true;Men varför ska vi gå igenom en så komplex tankeprocess? Den mest irriterande orsaken är att den här metoden gör två olika saker: det cyklar och det bestämmer. Jag vill att den bara ska cykla och lämna beslutet till en annan metod. En metod ska alltid göra en enda sak och göra det bra.
funktion $ ($ num, $ divisor) if (isDivisible ($ num, $ divisor)) return false; ; för ($ i = $ start; $ i < $end; $i++) $numberIsNotPrime($num, $divisors ? $divisors[$i] : $i); return true;Vårt första försök var att extrahera villkoret och avkastningen i en variabel. Detta är lokalt, för tillfället. Men koden fungerar inte. Egentligen
för
loop komplicerar saker ganska. Jag har en känsla av att en liten rekursion kommer att hjälpa.funktion checkRecursiveDivisibility ($ current, $ end, $ num, $ divisor) om ($ current == $ end) return true;När vi tänker på rekursivitet måste vi alltid börja med de exceptionella fallen. Vårt första undantag är när vi når slutet av vår rekursion.
funktion checkRecursiveDivisibility ($ current, $ end, $ num, $ divisor) om ($ current == $ end) return true; om (isDivisible ($ num, $ divisor)) return false;Vår andra exceptionella fall som kommer att bryta rekursionen är när numret är delbart. Vi vill inte fortsätta. Och det handlar om alla exceptionella fall.
ini_set ('xdebug.max_nesting_level', 10000); funktion checkDivisorsBetween ($ start, $ end, $ num, $ divisors = null) return checkRecursiveDivisibility ($ start, $ end, $ num, $ divisors); funktion checkRecursiveDivisibility ($ current, $ end, $ num, $ divisors) om ($ current == $ end) return true; om (isDivisible ($ num, $ divisors? $ divisors [$ current]: $ current)) return false; checkRecursiveDivisibility ($ current ++, $ end, $ num, $ divisors);Detta är ett annat försök att använda rekursion för vårt problem, men tyvärr återkommer 10 000 gånger i PHP till en krasch av PHP eller PHPUnit på mitt system. Så det verkar vara ett annat slut. Men om det skulle ha fungerat skulle det ha varit en bra ersättning för den ursprungliga logiken.
Utmaning
När jag skrev den gyllene mästaren, förbisedde jag med avsikt något. Låt oss bara säga att testen inte täcker så mycket kod som de borde. Kan du upptäcka problemet? Om ja, hur skulle du närma dig den?
Slutgiltiga tankar
"Extract till you drop" är ett bra sätt att dissekera långa metoder. Det tvingar dig att tänka på små bitar av kod och att ge bitarna ett syfte genom att extrahera dem till metoder. Jag tycker att det är fantastiskt hur det här enkla förfarandet, tillsammans med frekvent byte namn, kan hjälpa mig att upptäcka att någon kod gör saker som jag aldrig trodde var möjligt.
I vår nästa och sista handledning om refactoring kommer vi att tillämpa denna teknik på trivia-spelet. Jag hoppas att du gillade den här handledningen som visade sig vara lite annorlunda. I stället för att prata i läroboksexemplar tog vi en riktig kod och vi måste kämpa med de verkliga problemen vi möter varje dag.