Gammal kod. Ugly code. Komplicerad kod. Spaghettikod. Jibberish nonsens. I två ord, Legacy Code. Det här är en serie som hjälper dig att arbeta och hantera det.
I en idealisk värld skulle du bara skriva ny kod. Du skulle skriva det vackert och perfekt. Du behöver aldrig återvända till din kod och du kommer aldrig behöva behålla projekt tio år gammal. I en idealisk värld ...
Tyvärr lever vi i en verklighet som inte är idealisk. Vi måste förstå, ändra och förbättra gamla koden. Vi måste arbeta med arvskod. Så vad väntar du på? Låt oss ta oss in i denna första handledning, få koden, förstå den lite och skapa ett säkerhetsnät för våra framtida modifieringar.
Legacy-kod definierades på så många sätt det är omöjligt att hitta en enda, allmänt accepterad definition för den. De få exempel i början av denna handledning är bara toppen av isberget. Så jag kommer inte att ge dig någon officiell definition. I stället kommer jag att citera dig min favorit.
Till mig, arvskod är helt enkelt kod utan test. ~ Michael Feathers
Tja, det är den första formella definitionen av uttrycket arvskod, publicerad av Michael Feathers i sin bok Arbeta effektivt med Legacy Code. Naturligtvis använde industrin uttrycket i åldrar, i princip för någon kod som är svår att förändra. Men denna definition har något annat att säga. Det förklarar problemet mycket tydligt, så att lösningen blir uppenbar. "Svårt att byta" är så vagt. Vad ska vi göra för att göra det enkelt att byta? Vi har ingen aning! "Kod utan test" å andra sidan är mycket konkret. Och svaret på vår tidigare fråga är enkelt, gör koden testbar och testa den. Så låt oss börja.
Denna serie kommer att baseras på den exceptionella Trivia Game av J.B. Rainsberger utformad för Legacy Code Retreat-händelser. Den är gjord för att vara som äkta arvskod och att även erbjuda möjligheter till en mängd olika refactoring på en anständig svårighetsgrad.
Trivia-spelet är värd på GitHub och det är GPLv3 licensierat, så du kan leka med det fritt. Vi börjar denna serie genom att kolla in det officiella förvaret. Koden är också kopplad till denna handledning med alla de ändringar som vi kommer att göra, så om du blir förvirrad vid något tillfälle kan du ta en smekning vid slutresultatet.
$ git klon https://github.com/jbrains/trivia.git Kloning i "trivia" ... fjärrkontroll: Räkna objekt: 429, gjort. fjärrkontroll: Komprimera objekt: 100% (262/262), gjort. fjärrkontroll: Totalt 429 (delta 100), återanvändning 419 (delta 93) Mottagande objekt: 100% (429/429), 848.33 KiB | 305,00 KiB / s, gjort. Upplösande deltag: 100% (100/100), färdig. Kontrollera anslutning ... gjort.
När du öppnar trivia
katalog hittar du vår kod på flera programmeringsspråk. Vi kommer att arbeta i PHP, men du är fri att välja din favorit och tillämpa de tekniker som presenteras här.
Enligt definition är arvskod svår att förstå, särskilt om vi inte ens vet vad den ska göra. Så det första steget är att köra koden och göra någon form av resonemang, vad det handlar om.
Vi har två filer i vår katalog.
$ cd php / $ ls-totalt 20 drwxr-xr-x 2 csaba csaba 4096 mar 10 21:05. drwxr-xr-x 26 csaba csaba 4096 mar 10 21: 05 ... -rw-r - r-- 1 csaba csaba 5568 mar 10 21:05 Game.php -rw-r - r-- 1 csaba csaba 410 Mar 10 21:05 GameRunner.php
GameRunner.php
verkar vara en bra kandidat för vårt försök att köra koden.
$ php ./GameRunner.php Chet blev tillagt De är spelare nummer 1 Pat har lagts till De är spelare nummer 2 Sue har lagts till De är spelare nummer 3 Chet är den nuvarande spelaren De har rullat en 4 Chets nya plats är 4 Kategorin är Pop Pop fråga 0 Svar var corrent !!!! Chet har nu 1 guldmynt. Pat är den nuvarande spelaren De har rullat en 2 Pats nya plats är 2 Kategorin är Sport Sport Fråga 0 Svaret var corrent !!!! Pat har nu 1 guldmynt. Sue är den nuvarande spelaren De har rullat en 1 Sue s nya plats är 1 Kategorin är Science Science Question 0 Svaret var corrent !!!! Sue har nu 1 guldmynt. Chet är den nuvarande spelaren De har rullat en 4 ## Några rader har tagits bort för att hålla handledningen till en rimlig storlek Svaret var corrent !!!! Sue har nu 5 guldmynt. Chet är den nuvarande spelaren De har rullat en 3 Chet kommer ut ur strafflådan Chets nya plats är 11 Kategorin är Rock Rock Question 5 Svaret var korrekt !!!! Chet har nu 5 guldmynt. Pat är den aktuella spelaren De har rullat en 1 Pats nya plats är 10 Kategorin är Sports Sports Question 1 Svar var corrent !!!! Pat har nu 6 guldmynt.
OK. Vår gissning var korrekt. Vår kod sprang och producerade viss produktion. Att analysera denna utmatning hjälper oss att dra nytta av en grundläggande idé om vad koden gör.
Nu är det mycket kunskap. Vi kunde räkna ut det mesta av applikationens grundläggande beteende genom att bara titta på produktionen. I verkliga applikationer kan utmatningen inte vara text på skärmen, men det kan vara en webbsida, en fellogg, en databas, en nätverkskommunikation, en dumpfil och så vidare. I andra fall kan modulen du behöver ändra inte köras separat. Om så är fallet måste du köra det genom andra moduler i den större applikationen. Försök bara lägga till minimumet för att få lite rimlig produktion från ditt äldre kod.
Nu när vi har en uppfattning om vad koden går ut, kan vi börja titta på den. Vi börjar med löpare.
Jag börjar med att köra all kod genom formateringen av min IDE. Detta förbättrar läsbarheten avsevärt genom att koden är bekant med vad jag är van vid. Så det här:
... blir det här:
... vilket är något bättre. Det kan inte vara en stor skillnad med denna lilla mängd kod, men det kommer att finnas på vår nästa fil.
Titta på vår GameRunner.php
fil kan vi enkelt identifiera några viktiga aspekter som vi observerade i produktionen. Vi kan se linjerna som lägger till användarna (9-11), att en roll () -metod heter och en vinnare väljs. Självklart är dessa långt ifrån spelets logik, men åtminstone kan vi börja med att identifiera nyckelmetoder som hjälper oss att upptäcka resten av koden.
Vi borde göra samma formatering på Game.php
fil också.
Den här filen är mycket större; Cirka 200 kodrubriker. De flesta metoderna är anpassade, men vissa är ganska stora och efter formateringen kan vi se att kodinställningen på två ställen går över fyra nivåer. Höga indragningsnivåer betyder vanligtvis massor av komplexa beslut, så för nu kan vi anta att de punkterna i vår kod blir mer komplexa och mer förnuftiga att förändras.
Och tanken på förändring leder oss till vår brist på test. De metoder vi såg in Game.php
är ganska komplexa. Oroa dig inte om du inte förstår dem. Vid denna tidpunkt är de också ett mysterium för mig. Legacy-kod är ett mysterium som vi behöver lösa och förstå. Vi gjorde vårt första steg för att förstå det och det är nu dags för vår andra.
När du arbetar med arvskod är det nästan omöjligt att förstå det och skriva kod som säkert kommer att utöva alla logiska vägar genom koden. För den typen av testning, skulle vi behöva förstå koden, men det gör vi inte ännu. Så vi måste ta ett annat tillvägagångssätt.
Istället för att försöka lista ut vad som ska testas kan vi testa allt, många gånger, så att vi hamnar med en enorm mängd produktionen, som vi nästan kan anta att det producerades genom att utöva alla delar av vårt arv koda. Det rekommenderas att köra koden minst 10 000 (tio tusen) gånger. Vi ska skriva ett test för att köra det dubbelt så mycket och spara utdata.
Vi kan tänka framåt och börja med att skapa en generator och ett test som separata filer för framtida testning, men är det verkligen nödvändigt? Vi vet inte det för viss del. Så varför börja inte bara med en grundläggande testfil som kommer att köra vår kod en gång och bygga vår logik upp därifrån.
Du hittar i det bifogade kodarkivet, inuti källa
mapp men utanför trivia
mappa vår Testa
mapp. I den här mappen skapar vi en fil: GoldenMasterTest.php
.
klass GoldenMasterTest utökar PHPUnit_Framework_TestCase funktion testGenerateOutput () ob_start (); require_once __DIR__. '/ ... /trivia/php/GameRunner.php'; $ output = ob_get_contents (); ob_end_clean (); var_dump ($ output);
Vi kan göra det på många sätt. Vi kan till exempel köra vår kod från konsolen och omdirigera dess utdata till en fil. Att ha det i ett test som enkelt kan köras inne i vår IDE är dock en fördel som vi inte bör ignorera.
Koden är ganska enkel, den buffrar utmatningen och sätter den in i $ utgång
variabel. De require_once ()
kommer också att köra all kod inne i den medföljande filen. I vår vardump kommer vi att se några redan kända utdata.
Men på andra gången kan vi observera något udda:
... utgångarna skiljer sig åt. Trots att vi körde samma kod är utmatningen annorlunda. De rullade siffrorna är olika, spelarnas positioner är olika.
gör $ aGame-> roll (rand (0, 5) + 1); om (rand (0, 9) == 7) $ notAWinner = $ aGame-> wrongAnswer (); else $ notAWinner = $ aGame-> wasCorrectlyAnswered (); medan ($ notAWinner);
Genom att analysera den grundläggande koden från löparen kan vi se att den använder en funktion rand()
att generera slumptal. Vårt nästa stopp är den officiella PHP-dokumentationen för att undersöka detta rand()
fungera.
Slumptalsgenerern utsöndras automatiskt.
Dokumentationen berättar att sådd sker automatiskt. Nu har vi en annan uppgift. Vi måste hitta ett sätt att kontrollera fröet. De srand ()
funktionen kan hjälpa till med det. Här är dess definition från dokumentationen.
Fröjer slumptalsgeneratorn med frö eller med slumpmässigt värde om inget frö ges.
Det berättar om det, om vi kör detta innan vi ringer till rand()
, vi borde alltid sluta med samma resultat.
funktionstestGenerateOutput () ob_start (); srand (1); require_once __DIR__. '/ ... /trivia/php/GameRunner.php'; $ output = ob_get_contents (); ob_end_clean (); var_dump ($ output);
Vi lägger srand (1)
före vår require_once ()
. Nu är utmatningen alltid densamma.
klass GoldenMasterTest utökar PHPUnit_Framework_TestCase funktion testGenerateOutput () file_put_contents ('/ tmp / gm.txt', $ this-> generateOutput ()); $ file_content = file_get_contents ('/ tmp / gm.txt'); $ this-> assertEquals ($ file_content, $ this-> generateOutput ()); privat funktion genereraOutput () ob_start (); srand (1); require_once __DIR__. '/ ... /trivia/php/GameRunner.php'; $ output = ob_get_contents (); ob_end_clean (); returnera $ output;
Denna förändring ser rimlig ut. Höger? Vi extraherade kodgenerationen till en metod, kör den två gånger och förväntat att produktionen var lika. Men de kommer inte att vara.
Anledningen är det require_once ()
kommer inte kräva samma fil två gånger. Det andra samtalet till generateOutput ()
Metoden kommer att producera en tom sträng. Så vad kan vi göra? Vad händer om vi helt enkelt fordra()
? Det borde köras varje gång.
Tja, det leder till ett annat problem: "Kan inte redeclare echoln ()"
. Men var kommer det ifrån? Det är rätt i början av Game.php
fil. Anledningen till att detta fel uppstår är att i GameRunner.php
vi har inkludera __DIR__. '/Game.php';
, som försöker att inkludera spelfilen två gånger, varje gång vi ringer upp generateOutput ()
metod.
include_once __DIR__. '/Game.php';
Använder sig av include_once
i GameRunner.php
kommer att lösa vårt problem. Ja, vi behövde ändra GameRunner.php
utan att ha test för det, ändå! Vi kan dock vara 99% säkra på att vår förändring inte kommer att bryta själva koden. Det är en liten och enkel nog för att inte skrämma oss väldigt mycket. Och viktigast av allt, det gör testen klar.
Nu när vi har kod som vi kan köra många gånger, är det dags att generera lite resultat.
funktion testGenerateOutput () $ this-> generateMany (20, '/tmp/gm.txt'); $ this-> generateMany (20, '/tmp/gm2.txt'); $ file_content_gm = file_get_contents ('/ tmp / gm.txt'); $ file_content_gm2 = file_get_contents ('/ tmp / gm2.txt'); $ this-> assertEquals ($ file_content_gm, $ file_content_gm2); privat funktion generera många ($ gånger, $ filnamn) $ first = true; medan ($ gånger) om ($ först) file_put_contents ($ fileName, $ this-> generateOutput ()); $ först = false; annars file_put_contents ($ fileName, $ this-> generateOutput (), FILE_APPEND); $ gånger--;
Vi extraherade en annan metod här: generateMany ()
. Den har två parametrar. En för hur många gånger vi vill köra vår generator, den andra är en destinationsfil. Det kommer att sätta den genererade produktionen i filerna. På den första körningen tömmer den filerna, och för resten av iterationerna lägger den till data. Du kan titta på filen för att se den genererade produktionen 20 gånger.
Men vänta! Samma spelare vinner varje gång? Är det möjligt?
katt /tmp/gm.txt | grep "har 6 guldmynt." Chet har nu 6 guldmynt. Chet har nu 6 guldmynt. Chet har nu 6 guldmynt. Chet har nu 6 guldmynt. Chet har nu 6 guldmynt. Chet har nu 6 guldmynt. Chet har nu 6 guldmynt. Chet har nu 6 guldmynt. Chet har nu 6 guldmynt. Chet har nu 6 guldmynt. Chet har nu 6 guldmynt. Chet har nu 6 guldmynt. Chet har nu 6 guldmynt. Chet har nu 6 guldmynt. Chet har nu 6 guldmynt. Chet har nu 6 guldmynt. Chet har nu 6 guldmynt. Chet har nu 6 guldmynt. Chet har nu 6 guldmynt. Chet har nu 6 guldmynt.
ja! Det är möjligt! Det är mer än möjligt. Det är en säker sak. Vi har samma frö för vår slumpmässiga funktion. Vi spelar samma spel om och om igen.
Vi måste spela olika spel, annars är det nästan säkert att endast en liten del av vår arvskod faktiskt utövas om och om igen. Omfattningen av den gyllene mästaren är att utöva så mycket som möjligt. Vi behöver återlösa slumpgeneratorn varje gång, men på ett kontrollerat sätt. Ett alternativ är att använda vår räknare som frövärdet.
privat funktion generera många ($ gånger, $ filnamn) $ first = true; medan ($ gånger) if ($ first) file_put_contents ($ fileName, $ this-> generateOutput ($ times)); $ först = false; annars file_put_contents ($ fileName, $ this-> generateOutput ($ times), FILE_APPEND); $ gånger--; privat funktion genereraOutput ($ seed) ob_start (); srand ($ utsäde); kräver __DIR__. '/ ... /trivia/php/GameRunner.php'; $ output = ob_get_contents (); ob_end_clean (); returnera $ output;
Detta håller fortfarande vårt test över, så vi är säkra på att vi genererar samma fullständiga utmatning varje gång, medan utmatningen spelar ett annat spel för varje iteration.
katt /tmp/gm.txt | grep "har 6 guldmynt." Sue har nu 6 guldmynt. Chet har nu 6 guldmynt. Chet har nu 6 guldmynt. Chet har nu 6 guldmynt. Chet har nu 6 guldmynt. Pat har nu 6 guldmynt. Pat har nu 6 guldmynt. Chet har nu 6 guldmynt. Chet har nu 6 guldmynt. Sue har nu 6 guldmynt. Chet har nu 6 guldmynt. Chet har nu 6 guldmynt. Sue har nu 6 guldmynt. Chet har nu 6 guldmynt. Sue har nu 6 guldmynt. Chet har nu 6 guldmynt. Chet har nu 6 guldmynt. Pat har nu 6 guldmynt. Chet har nu 6 guldmynt. Chet har nu 6 guldmynt.
Det finns olika vinnare för spelet på ett slumpmässigt sätt. Det ser bra ut.
Det första du kan försöka är att köra vår kod för 20.000 game iterations.
funktionstestGenerateOutput () $ times = 20000; $ this-> generateMany ($ times, '/tmp/gm.txt'); $ this-> generateMany ($ times, '/tmp/gm2.txt'); $ file_content_gm = file_get_contents ('/ tmp / gm.txt'); $ file_content_gm2 = file_get_contents ('/ tmp / gm2.txt'); $ this-> assertEquals ($ file_content_gm, $ file_content_gm2);
Detta kommer nästan att fungera. Två 55MB-filer kommer att genereras.
ls -alh / tmp / gm * -rw-r - r-- 1 csaba csaba 55M Mar 14 20:38 /tmp/gm2.txt -rw-r - r-- 1 csaba csaba 55M Mar 14 20:38 /tmp/gm.txt
Å andra sidan kommer testet att misslyckas med ett otillräckligt minnefel. Det spelar ingen roll hur mycket RAM du har, det kommer att misslyckas. Jag har 8GB plus en 4GB swap och det misslyckas. De två strängarna är bara för stora för att jämföras i vår påstående.
Med andra ord genererar vi bra filer, men PHPUnit kan inte jämföra dem. Vi behöver en arbetsplats.
$ this-> assertFileEquals ('/ tmp / gm.txt', '/tmp/gm2.txt');
Det verkar vara en bra kandidat, men det misslyckas fortfarande. Vilken skam. Vi behöver undersöka situationen ytterligare.
$ this-> assertTrue ($ file_content_gm == $ file_content_gm2);
Detta fungerar dock.
Det kan jämföra de två strängarna och misslyckas om de är olika. Det har dock ett litet pris. Det kommer inte att kunna exakt berätta vad som är fel när strängarna skiljer sig åt. Det kommer bara att säga "Misslyckades hävdar att falskt är sant."
. Men vi kommer att ta itu med det i en kommande handledning.
Vi är färdiga för denna handledning. Vi har lärt oss en hel del för vår första lektion och vi är på bra start för vårt framtida arbete. Vi mötte koden, vi analyserade det på olika sätt och vi förstod först det viktigaste logiken. Då skapade vi en uppsättning tester för att säkerställa att den utövas så mycket som möjligt. Ja. Testerna är väldigt långsamma. Det tar dem 24 sekunder på min Core i7 CPU för att generera produktionen två gånger. Lyckligtvis i vår framtida utveckling kommer vi att behålla gm.txt
filen orörd och generera en annan en gång per gång. Men 12 sekunder är fortfarande en stor tid för en så liten kodbas.
När vi avslutar den här serien ska våra test springa på mindre än en sekund och testa hela koden ordentligt. Så håll dig uppdaterad för vår nästa handledning när vi ska ta itu med problem som magiska konstanter, magiska strängar och komplexa villkor. Tack för att du läser.