In het zesde deel van onze serie hadden we het over het aanvallen van lange methoden door gebruik te maken van paarprogrammering en weergavecode van verschillende niveaus. We zoomden voortdurend in en uit en observeerden zowel kleine dingen zoals naamgeving als vorm en inkeping.
Vandaag zullen we een andere aanpak hanteren: we gaan ervan uit dat we alleen zijn, geen collega of paar om ons te helpen. We zullen een techniek gebruiken genaamd "Extract till you drop" die code breekt in hele kleine stukjes. We zullen ons uiterste best doen om deze stukken zo eenvoudig mogelijk te begrijpen, zodat de toekomst ons of een andere programmeur deze gemakkelijk kan begrijpen..
Ik hoorde voor het eerst over dit concept van Robert C. Martin. Hij presenteerde het idee in een van zijn video's als een eenvoudige manier om moeilijk leesbare code bij te werken.
Het basisidee is om kleine, begrijpelijke stukjes code te nemen en uit te pakken. Het maakt niet uit of u vier of vier tekens identificeert die kunnen worden geëxtraheerd. Wanneer u iets identificeert dat kan worden ingekapseld in een duidelijker concept, extraheer u. U gaat door met dit proces, zowel op de originele methode als op de nieuw geëxtraheerde stukken totdat u geen stuk code kunt vinden dat als een concept kan worden ingekapseld.
Deze techniek is vooral handig als u alleen werkt. Het dwingt je om na te denken over zowel kleine als grotere stukjes code. Het heeft nog een leuk effect: het doet je nadenken over de code - veel! Naast de hierboven genoemde extractmethode of variabele refactoring, zult u merken dat u variabelen, functies, klassen en meer hernoemt.
Laten we een voorbeeld bekijken van een willekeurige code van internet. StackOverflow is een goede plaats om kleine stukjes code te vinden. Hier is er een die bepaalt of een getal priem is:
// Controleer of een nummer de primaire functie isPrime ($ num, $ pf = null) if (! Is_array ($ pf)) for ($ i = 2; $ iOp dit moment heb ik geen idee hoe deze code werkt. Ik vond het net op het internet tijdens het schrijven van dit artikel, en ik zal het samen met jou ontdekken. Het volgende proces is mogelijk niet de schoonste. In plaats daarvan zal het mijn redenering en refactoring weerspiegelen zoals het gebeurt, zonder voorafgaande planning.
Refactoring van de Prime Number Checker
Volgens Wikipedia:
Een priemgetal (of een priemgetal) is een natuurlijk getal groter dan 1 dat geen positieve delers anders dan 1 en zichzelf heeft.Zoals je kunt zien is dit een eenvoudige methode voor een eenvoudig wiskundig probleem. Het komt terug
waar
ofvals
, dus het moet ook gemakkelijk te testen zijn.klasse IsPrimeTest breidt PHPUnit_Framework_TestCase uit function testItCanRecognizePrimeNumbers () $ this-> assertTrue (isPrime (1)); // Controleer of een nummer de primaire functie isPrime ($ num, $ pf = null) // ... de inhoud van de methode zoals hierboven te zienWanneer we alleen met voorbeeldcode spelen, is de gemakkelijkste manier om alles in een testbestand te plaatsen. Op deze manier hoeven we niet na te denken over welke bestanden we moeten maken, in welke mappen ze horen of hoe ze in de andere mappen moeten worden opgenomen. Dit is slechts een eenvoudig voorbeeld om te gebruiken om vertrouwd te raken met de techniek voordat we deze toepassen op een van de trivia-spelmethoden. Dus, alles gaat in een testbestand, je kunt een naam geven zoals je wilt. ik heb gekozen
IsPrimeTest.php
.Deze test slaagt. Mijn volgende instinct is om nog een paar priemgetallen toe te voegen en dan een nieuwe test te schrijven met geen priemgetallen.
function testItCanRecognizePrimeNumbers () $ this-> assertTrue (isPrime (1)); $ This-> assertTrue (isPrime (2)); $ This-> assertTrue (isPrime (3)); $ This-> assertTrue (isPrime (5)); $ This-> assertTrue (isPrime (7)); $ This-> assertTrue (isPrime (11));Dat gaat voorbij. Maar hoe zit dit??
function testItCanRecognizeNonPrimes () $ this-> assertFalse (isPrime (6));Dit mislukt onverwacht: 6 is geen priemgetal. Ik verwachtte dat de methode zou terugkeren
vals
. Ik weet niet hoe de methode werkt, of het doel van de$ pf
parameter - Ik verwachtte gewoon dat het zou terugkerenvals
op basis van de naam en beschrijving. Ik heb geen idee waarom het niet werkt, noch hoe het te repareren.Dit is een nogal verwarrend dilemma. Wat moeten we doen? Het beste antwoord is om tests te schrijven die slagen voor een behoorlijk aantal nummers. We moeten misschien proberen te raden, maar we hebben tenminste een idee over wat de methode doet. Dan kunnen we beginnen met het refactoren ervan.
function testFirst20NaturalNumbers () for ($ i = 1; $ i<20;$i++) echo $i . ' - ' . (isPrime($i) ? 'true' : 'false') . "\n";Dat levert iets interessants op:
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 - trueHier begint een patroon op te duiken. Alles klopt tot 9, en dan afwisselend tot 19. Maar herhaalt dit patroon zich? Probeer het voor 100 nummers uit te voeren en je zult meteen zien dat het dat niet is. Het lijkt in feite te werken voor getallen tussen 40 en 99. Het gaat mis tussen een keer 30-39 door 35 als prime te nomineren. Hetzelfde geldt voor het bereik van 20-29. 25 wordt als prime beschouwd.
Deze oefening die begon als een eenvoudige code om een techniek te demonstreren, blijkt veel moeilijker dan verwacht. Ik besloot echter om het te behouden omdat het op een typische manier het echte leven weerspiegelt.
Hoe vaak ben je begonnen aan een taak die eenvoudig leek om erachter te komen dat het extreem moeilijk is?We willen de code niet repareren. Wat de methode ook doet, hij moet dit blijven doen. We willen het refactiveren om anderen het beter te laten begrijpen.
Omdat priemgetallen niet op de juiste manier worden verteld, gebruiken we dezelfde Golden Master-benadering die we in les één hebben geleerd.
function testGenerateGoldenMaster () for ($ i = 1; $ i<10000;$i++) file_put_contents(__DIR__ . '/IsPrimeGoldenMaster.txt', $i . ' - ' . (isPrime($i) ? 'true' : 'false') . "\n", FILE_APPEND);Voer dit een keer uit om de Gouden Meester te genereren. Het zou snel moeten gaan. Als u het opnieuw moet uitvoeren, vergeet dan niet om het bestand te verwijderen voordat u de test uitvoert. Anders wordt de uitvoer aan de vorige inhoud gekoppeld.
function testMatchesGoldenMaster () $ goldenMaster = bestand (__ DIR__. '/IsPrimeGoldenMaster.txt'); voor ($ i = 1; $ i<10000;$i++) $actualResult = $i . ' - ' . (isPrime($i) ? 'true' : 'false'). "\n"; $this->assertTrue (in_array ($ actualResult, $ goldenMaster), 'The value'. $ actualResult. 'staat niet in de gouden meester.');Schrijf nu de test voor de gouden meester. Deze oplossing is misschien niet de snelste, maar het is gemakkelijk te begrijpen en het zal ons precies vertellen welk nummer niet overeenkomt als iets breekt. Maar er is een kleine duplicatie in de twee testmethoden die we kunnen uitpakken in een
privaat
methode.klasse IsPrimeTest breidt PHPUnit_Framework_TestCase uit function testGenerateGoldenMaster () $ this-> markTestSkipped (); voor ($ i = 1; $ i<10000;$i++) file_put_contents(__DIR__ . '/IsPrimeGoldenMaster.txt', $this->getPrimeResultAsString ($ i), FILE_APPEND); function testMatchesGoldenMaster () $ goldenMaster = bestand (__ DIR__. '/IsPrimeGoldenMaster.txt'); voor ($ i = 1; $ i<10000;$i++) $actualResult = $this->getPrimeResultAsString ($ i); $ this-> assertTrue (in_array ($ actualResult, $ goldenMaster), 'The value'. $ actualResult. 'staat niet in de gouden meester.'); persoonlijke functie getPrimeResultAsString ($ i) return $ i. '-'. (isPrime ($ i)? 'true': 'false'). "\ N";Nu kunnen we ons verplaatsen naar onze productiecode. De test werkt in ongeveer twee seconden op mijn computer, dus het is beheersbaar.
Extractie van alles wat we kunnen
Eerst kunnen we een
isDivisible ()
methode in het eerste deel van de code.if (! is_array ($ pf)) for ($ i = 2; $ iDat zal ons in staat stellen om de code in het tweede deel als volgt te hergebruiken:
else $ pfCount = count ($ pf); voor ($ i = 0; $ i<$pfCount;$i++) if(isDivisible($num, $pf[$i])) return false; return true;En zodra we met deze code begonnen te werken, merkten we dat deze onzorgvuldig is uitgelijnd. Bretels staan soms aan het begin van de lijn, andere keren aan het einde.
Soms worden tabbladen gebruikt voor inspringen, soms spaties. Soms zijn er spaties tussen operand en operator, soms niet. En nee, dit is niet speciaal gemaakte code. Dit is het echte leven. Echte code, geen kunstmatige oefening.
// Controleer of een nummer de primaire functie isPrime ($ 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;Dat ziet er beter uit. Meteen de twee
als
uitspraken lijken erg op elkaar. Maar we kunnen ze niet extraheren vanwege deterugkeer
statements. Als we niet terugkeren, breken we de logica.Als de geëxtraheerde methode een booleaanse waarde oplevert en we deze vergelijken om te beslissen of we wel of niet moeten terugkeren
isPrime ()
, dat zou helemaal niet helpen. Er kan een manier zijn om het uit te pakken door enkele functionele programmeerconcepten in PHP te gebruiken, maar misschien later. We kunnen eerst iets eenvoudiger doen.function isPrime ($ num, $ pf = null) if (! is_array ($ pf)) return checkDivisorsBtween (2, intval (sqrt ($ num)), $ num); else $ pfCount = count ($ pf); voor ($ 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;De .extracten
voor
loop als geheel is een beetje eenvoudiger, maar als we onze uitgepakte methode opnieuw proberen te gebruiken in het tweede deel van deals
we kunnen zien dat het niet werkt. Er is een mysterieus$ pf
variabele waarover we bijna niets weten.Het lijkt erop dat het controleert of het nummer deelbaar is door een reeks specifieke delers in plaats van alle getallen op te nemen tot de andere magische waarde bepaald door
intval (sqrt ($ num))
. Misschien kunnen we de naam wijzigen$ pf
in$ delers
.function isPrime ($ num, $ divisors = null) if (! is_array ($ divisors)) return checkDivisorsBtween (2, intval (sqrt ($ num)), $ num); else return checkDivisorsBetween (0, count ($ divisors), $ num, $ delers); functie checkDivisorsBetween ($ start, $ end, $ num, $ divisors = null) for ($ i = $ start; $ i < $end; $i++) if (isDivisible($num, $divisors ? $divisors[$i] : $i)) return false; return true;Dit is een manier om het te doen. We hebben een vierde, optionele parameter aan onze controlemethode toegevoegd. Als het een waarde heeft, gebruiken we het, anders gebruiken we
$ i
.Kunnen we nog iets extraheren? Hoe zit het met dit stuk code:
intval (sqrt ($ num))
?function isPrime ($ num, $ divisors = null) if (! is_array ($ divisors)) return checkDivisorsBtween (2, integerRootOf ($ num), $ num); else return checkDivisorsBetween (0, count ($ divisors), $ num, $ delers); function integerRootOf ($ num) return intval (sqrt ($ num));Is dat niet beter? Iets. Het is beter als de persoon die achter ons aan komt weet niet wat
intval ()
ensqrt ()
doen, maar het helpt niet om de logica begrijpelijker te maken. Waarom eindigen we onzevoor
loop op dat specifieke nummer? Misschien is dit de vraag die onze functienaam zou moeten beantwoorden.[PHP] // Controleer of een nummer de primaire functie isPrime ($ num, $ divisors = null) if (! Is_array ($ delers)) return checkDivisorsBetween (2, highestPossibleFactor ($ num), $ num); else return checkDivisorsBetween (0, count ($ divisors), $ num, $ delers); function highestPossibleFactor ($ num) return intval (sqrt ($ num)); [PHP]Dat is beter omdat het verklaart waarom we daar stoppen. Misschien kunnen we in de toekomst een andere formule uitvinden om dat aantal te bepalen. De naamgeving introduceerde ook een beetje inconsistentie. We noemden de getallenfactoren, wat een synoniem is van delers. Misschien moeten we er een kiezen en die alleen gebruiken. Ik zal je de hernoemende refactoring laten maken als een oefening.
De vraag is, kunnen we verder nog extraheren? Welnu, we moeten het proberen tot we erbij neervallen. Ik noemde de functionele programmeerzijde van PHP enkele paragrafen hierboven. Er zijn twee belangrijke functionele programmeerkenmerken die we eenvoudig kunnen toepassen in PHP: eersteklasfuncties en recursie. Wanneer ik een zie
als
verklaring met eenterugkeer
in eenvoor
loop, zoals in onzecheckDivisorsBetween ()
methode, denk ik aan het toepassen van een of beide technieken.function checkDivisorsTussen ($ start, $ end, $ num, $ divisors = null) for ($ i = $ start; $ i < $end; $i++) if (isDivisible($num, $divisors ? $divisors[$i] : $i)) return false; return true;Maar waarom zouden we door zo'n complex denkproces gaan? De meest vervelende reden is dat deze methode twee verschillende dingen doet: het cycli en het beslist. Ik wil dat het alleen maar fietst en laat de beslissing over aan een andere methode. Een methode zou altijd één ding moeten doen en het goed doen.
functie checkDivisorsTussen ($ start, $ end, $ num, $ delers = null) $ numberIsNotPrime = function ($ num, $ divisor) if (isDivisible ($ num, $ deler)) return false; ; voor ($ i = $ start; $ i < $end; $i++) $numberIsNotPrime($num, $divisors ? $divisors[$i] : $i); return true;Onze eerste poging was om de voorwaarde en de return-verklaring in een variabele te extraheren. Dit is momenteel lokaal. Maar de code werkt niet. Eigenlijk het
voor
lus compliceert dingen best een beetje. Ik heb het gevoel dat een beetje recursie zal helpen.functie checkRecursiveDivisibility ($ current, $ end, $ num, $ divisor) if ($ current == $ end) return true;Als we nadenken over recursiviteit, moeten we altijd beginnen met de uitzonderlijke gevallen. Onze eerste uitzondering is wanneer we het einde van onze recursie hebben bereikt.
functie checkRecursiveDivisibility ($ current, $ end, $ num, $ divisor) if ($ current == $ end) return true; if (isDivisible ($ num, $ deler)) return false;Ons tweede uitzonderlijke geval dat de recursie zal doorbreken, is wanneer het getal deelbaar is. We willen niet doorgaan. En dat gaat over alle uitzonderlijke gevallen.
ini_set ('xdebug.max_nesting_level', 10000); function checkDivisorsTussen ($ start, $ end, $ num, $ delers = null) return checkRecursiveDivisibility ($ start, $ end, $ num, $ delers); function checkRecursiveDivisibility ($ current, $ end, $ num, $ divisors) if ($ current == $ end) return true; if (isDivisible ($ num, $ delers? $ delers [$ current]: $ current)) return false; checkRecursiveDivisibility ($ current ++, $ end, $ num, $ delers);Dit is een andere poging om recursie voor ons probleem te gebruiken, maar helaas, 10.000 keer terugkerende in PHP leidt tot een crash van PHP of PHPUnit op mijn systeem. Dus dit lijkt weer een doodlopende weg te zijn. Maar als het zou hebben gewerkt, zou het een mooie vervanging van de oorspronkelijke logica zijn geweest.
Uitdaging
Toen ik de Gouden Meester schreef, heb ik bewust iets over het hoofd gezien. Laten we zeggen dat de tests niet zoveel code bevatten als zou moeten. Zie je het probleem? Zo ja, hoe zou u het benaderen??
Laatste gedachten
"Extract till you drop" is een goede manier om lange methoden te ontleden. Het dwingt je om na te denken over kleine stukjes code en om de stukjes een doel te geven door ze te extraheren in methoden. Ik vind het verbazingwekkend hoe deze eenvoudige procedure, samen met frequente hernoemen, mij kan helpen ontdekken dat sommige codes dingen doen die ik nooit voor mogelijk had gehouden.
In onze volgende en laatste tutorial over refactoring passen we deze techniek toe op het Trivia-spel. Ik hoop dat je deze tutorial leuk vond die een beetje anders bleek te zijn. In plaats van te praten over tekstvoorbeelden, hebben we een aantal echte codes gebruikt en moesten we vechten tegen de echte problemen waarmee we elke dag worden geconfronteerd.