Refactoring Legacy Code Del 1 - The Golden Master

Gammel kode. Ugyldig kode. Komplisert kode. Spaghetti kode. Jibberish tull. I to ord, Legacy Code. Dette er en serie som vil hjelpe deg med å jobbe og håndtere det.

I en ideell verden skriver du bare ny kode. Du ville skrive det vakkert og perfekt. Du trenger aldri å revidere koden din, og du vil aldri behøve å opprettholde prosjekter ti år gammel. I en ideell verden ...

Dessverre lever vi i en realitet som ikke er ideell. Vi må forstå, endre og forbedre gamle kode. Vi må jobbe med arvskoden. Så hva venter du på? La oss få hodet inn i denne første opplæringen, få koden, forstå den litt, og opprett et sikkerhetsnett for våre fremtidige modifikasjoner.

Definisjon av Legacy Code

Eldre kode ble definert på så mange måter det er umulig å finne en enkelt, allment akseptert definisjon for den. De få eksemplene i begynnelsen av denne opplæringen er bare toppen av isfjellet. Så jeg vil ikke gi deg noen offisiell definisjon. I stedet vil jeg sitere deg min favoritt.

Til meg, arvskode er ganske enkelt kode uten tester. ~ Michael Feathers

Vel, det er den første formelle definisjonen av uttrykket arvskode, publisert av Michael Feathers i sin bok Arbeid effektivt med Legacy Code. Selvfølgelig brukte næringen uttrykket i evigheter, i utgangspunktet for noen kode som er vanskelig å endre. Men denne definisjonen har noe annet å fortelle. Det forklarer problemet veldig tydelig, slik at løsningen blir åpenbar. "Vanskelig å forandre" er så vag. Hva skal vi gjøre for å gjøre det enkelt å endre? Vi har ingen anelse om! "Kode uten tester" derimot er veldig konkret. Og svaret på vårt forrige spørsmål er enkelt, gjør kode testbar og test det. Så la oss komme i gang.

Få vår eldre kode

Denne serien vil være basert på den eksepsjonelle Trivia Game av J.B. Rainsberger designet for Legacy Code Retreat hendelser. Det er laget for å være som ekte arvskode og å tilby muligheter for et bredt spekter av refactoring, på et anstendig vanskelighetsgrad.

Sjekk ut kildekoden

Trivia-spillet er vert for GitHub, og det er GPLv3 lisensiert, så du kan leke med det fritt. Vi starter denne serien ved å sjekke ut det offisielle depotet. Koden er også knyttet til denne opplæringen med alle de modifikasjonene vi vil gjøre, så hvis du blir forvirret på et tidspunkt, kan du ta en snakk på sluttresultatet.

 $ git klone https://github.com/jbrains/trivia.git Kloning til 'trivia' ... fjernkontroll: Telle gjenstander: 429, ferdig. fjernkontroll: Komprimering av objekter: 100% (262/262), ferdig. fjernkontroll: Totalt 429 (delta 100), gjenbrukt 419 (delta 93) Motta objekter: 100% (429/429), 848.33 KiB | 305,00 KiB / s, ferdig. Løse deltakere: 100% (100/100), ferdig. Kontrollerer tilkobling ... ferdig.

Når du åpner trivia katalog vil du se vår kode i flere programmeringsspråk. Vi vil jobbe i PHP, men du er fri til å velge din favoritt og bruke teknikkene som presenteres her.

Forstå koden

Etter definisjon er arvskode vanskelig å forstå, spesielt hvis vi ikke engang vet hva den skal gjøre. Så det første trinnet er å kjøre koden og lage en slags resonnement, hva det handler om.

Vi har to filer i katalogen vår.

$ 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 ser ut til å være en god kandidat for vårt forsøk på å kjøre koden.

$ php ./GameRunner.php Chet ble lagt til De er spiller nummer 1 Pat ble lagt til De er spiller nummer 2 Sue ble lagt til De er spiller nummer 3 Chet er den nåværende spilleren De har rullet en 4 Chets nye plassering er 4 kategorien er Pop Pop spørsmål 0 Svar var corrent! Chet har nå 1 gullmynt. Pat er den nåværende spilleren De har rullet en 2 Pat's nye plassering er 2 Kategorien er Sports Sports Question 0 Svar var corrent! Pat har nå 1 gullmynt. Sue er den nåværende spilleren De har rullet en 1 Sue sin nye plassering er 1 Kategorien er Science Science Spørsmål 0 Svar var corrent! Sue har nå 1 gullmynter. Chet er den nåværende spilleren De har rullet en 4 ## Noen linjer fjernet for å holde opplæringen til en rimelig størrelse Svaret var corrent! Sue har nå 5 gullmynter. Chet er den nåværende spilleren De har rullet en 3 Chet kommer ut av straffeboksen Chets nye plassering er 11 Kategorien er Rock Rock Question 5 Svaret var riktig! Chet har nå 5 gullmynter. Pat er den nåværende spilleren De har rullet en 1 Pat's nye plassering er 10 Kategorien er Sports Sports Spørsmål 1 Svar var corrent! Pat har nå 6 gullmynter.

OK. Vårt gjetning var riktig. Koden vår løp og produserte litt produksjon. Ved å analysere denne utdataen kan vi avlede noen grunnleggende ideer om hva koden gjør.

  1. Vi vet at det er et Trivia-spill. Vi visste det da vi sjekket ut kildekoden.
  2. Vårt eksempel har tre spillere: Chet, Pat og Sue.
  3. Det er en slags rulling av en terning eller lignende konsept.
  4. Det er en nåværende plassering for en spiller. Muligens på en slags brett?
  5. Det er ulike kategorier hvorfra spørsmål blir spurt.
  6. Brukere svarer på spørsmål.
  7. Riktig svar gir spillerne gull.
  8. Feil svar send spillere til straffeboksen.
  9. Spillere kan komme seg ut av straffebok, basert på noen ikke helt klart logikk.
  10. Det virker som at brukeren som først når seks gullmynter vinner.

Nå er det mye kunnskap. Vi kunne finne ut det meste av den grunnleggende oppførselen til applikasjonen ved bare å se på utgangen. I virkelige applikasjoner kan utdataene ikke være tekst på skjermen, men det kan være en nettside, en feillogg, en database, en nettverkskommunikasjon, en dumpfil og så videre. I andre tilfeller kan modulen du trenger å endre ikke kjøres isolert. I så fall må du kjøre den gjennom andre moduler av større applikasjon. Bare prøv å legge til minimum, for å få litt rimelig utgang fra din arvskode.

Skanner koden

Nå som vi har en ide om hva koden gir, kan vi begynne å se på den. Vi starter med løperen.

The Game Runner

Jeg liker å begynne med å kjøre all koden gjennom formateringen av min IDE. Dette forbedrer lesbarheten sterkt ved å gjøre kodens form kjent med det jeg er vant til. Så dette:

... blir dette:

... som er noe bedre. Det kan ikke være en stor forskjell med denne lille mengden kode, men det kommer til å være på vår neste fil.

Ser på vår GameRunner.php fil, kan vi enkelt identifisere noen viktige aspekter vi observerte i produksjonen. Vi kan se linjene som legger til brukerne (9-11), at en roll () -metode kalles og en vinner er valgt. Selvfølgelig er disse langt fra de indre hemmelighetene i logikken i spillet, men i det minste kan vi begynne å identifisere viktige metoder som vil hjelpe oss med å oppdage resten av koden.

Spillfilen

Vi bør gjøre samme formatering på Game.php fil også.

Denne filen er mye større; Omtrent 200 kodelinjer. De fleste metodene er hensiktsmessig dimensjonert, men noen av dem er ganske store, og etter formateringen kan vi se at kodeinntrykket på to steder går over fire nivåer. Høye nivåer av innrykk betyr vanligvis mange komplekse beslutninger, så for nå kan vi anta at disse punktene i koden vår vil være mer komplekse og mer fornuftige å forandre.

Den Gyldne Mesteren

Og tanken på endring fører oss til mangel på tester. Metodene vi så i Game.php er ganske komplekse. Ikke bekymre deg hvis du ikke forstår dem. På dette punktet er de også et mysterium for meg. Eldre kode er et mysterium som vi trenger å løse og forstå. Vi gjorde vårt første skritt for å forstå det, og det er nå tid for vår andre.

Så hva er denne gylne mesteren?

Når du arbeider med arvskode, er det nesten umulig å forstå det og skrive kode som sikkert vil utøve alle de logiske banene gjennom koden. For den typen testing, må vi forstå koden, men det er vi ennå ikke. Så vi må ta en annen tilnærming.

I stedet for å prøve å finne ut hva som skal testes, kan vi teste alt, mange ganger, slik at vi ender med mye produksjon, som vi nesten kan anta at den ble produsert ved å utøve alle deler av arven vår kode. Det anbefales å kjøre koden minst 10 000 (ti tusen) ganger. Vi skal skrive en test for å kjøre den dobbelt så mye og lagre utdataene.

Skriver Golden Master Generator

Vi kan tenke fremover og begynne med å lage en generator og en test som separate filer for fremtidig testing, men er det virkelig nødvendig? Vi vet det ikke for sikkert. Så hvorfor ikke bare starte med en grunnleggende testfil som kjører vår kode en gang og bygger vår logikk opp derfra.

Du finner i vedlagte kodearkiv, inne i kilde mappe men utenfor trivia mappe vår Test mappe. I denne mappen lager vi en fil: GoldenMasterTest.php.

klassen GoldenMasterTest utvider PHPUnit_Framework_TestCase funksjon testGenerateOutput () ob_start (); require_once __DIR__. '/ ... /trivia/php/GameRunner.php'; $ output = ob_get_contents (); ob_end_clean (); var_dump ($ utgang); 

Vi kunne gjøre dette på mange måter. Vi kan for eksempel kjøre vår kode fra konsollen og omdirigere utdataene til en fil. Men å ha det i en test som er lett å kjøre inne i vår IDE, er en fordel vi ikke bør overse.

Koden er ganske enkel, den bufferer utdataene og legger den inn i $ utgang variabel. De require_once () vil også kjøre all koden inne i den medfølgende filen. I vår vardump ser vi allerede kjent utgang.

Men på et andre løp kan vi observere noe merkelig:

... utgangene er forskjellige. Selv om vi kjørte samme kode, er utgangen forskjellig. De rullede tallene er forskjellige, spillernes posisjoner er forskjellige.

Seeding den tilfeldige generatoren

gjør $ aGame-> rull (rand (0, 5) + 1); hvis (rand (0, 9) == 7) $ notAWinner = $ aGame-> wrongAnswer ();  ellers $ notAWinner = $ aGame-> wasCorrectlyAnswered ();  mens ($ notAWinner);

Ved å analysere den essensielle koden fra løperen, kan vi se at den bruker en funksjon rand () å generere tilfeldige tall. Vår neste stopp er den offisielle PHP-dokumentasjonen for å undersøke dette rand () funksjon.

Slumptallgeneratoren blir podet automatisk.

Dokumentasjonen forteller oss at såing skjer automatisk. Nå har vi en annen oppgave. Vi må finne en måte å kontrollere frøet på. De srand () funksjonen kan hjelpe med det. Her er dens definisjon fra dokumentasjonen.

Frøer tilfeldig talgeneratoren med frø eller med en tilfeldig verdi dersom ingen frø er gitt.

Det forteller oss at hvis vi kjører dette før noen anrop til rand (), vi bør alltid ende opp med de samme resultatene.

funksjonstestGenerateOutput () ob_start (); srand (1); require_once __DIR__. '/ ... /trivia/php/GameRunner.php'; $ output = ob_get_contents (); ob_end_clean (); var_dump ($ utgang); 

Vi putter srand (1) før vår require_once (). Nå er produksjonen alltid den samme.

Sett utdataene i en fil

klassen GoldenMasterTest utvider PHPUnit_Framework_TestCase funksjon testGenerateOutput () file_put_contents ('/ tmp / gm.txt', $ this-> generateOutput ()); $ file_content = file_get_contents ('/ tmp / gm.txt'); $ this-> assertEquals ($ file_content, $ this-> generateOutput ());  privat funksjon generereOutput () ob_start (); srand (1); require_once __DIR__. '/ ... /trivia/php/GameRunner.php'; $ output = ob_get_contents (); ob_end_clean (); returnere $ output; 

Denne endringen ser rimelig ut. Ikke sant? Vi tok ut kodegenerasjonen i en metode, kjør den to ganger, og ventet at produksjonen var like. Men de vil ikke være.

Årsaken er at require_once () vil ikke kreve samme fil to ganger. Den andre anrop til generateOutput () Metoden vil produsere en tom streng. Så, hva kan vi gjøre? Hva om vi bare kreve ()? Det bør kjøres hver gang.

Vel, det fører til et annet problem: "Kan ikke redeclare echoln ()". Men hvor kommer det fra? Det er rett i begynnelsen av Game.php fil. Grunnen til at denne feilen oppstår er fordi i GameRunner.php vi har inkludere __DIR__. '/Game.php';, som forsøker å inkludere spillfilen to ganger, hver gang vi ringer til generateOutput () metode.

include_once __DIR__. '/Game.php';

Ved hjelp av include_once i GameRunner.php vil løse vårt problem. Ja, vi trengte å endre GameRunner.php uten å ha tester for det, ennå! Vi kan imidlertid være 99% sikre på at vår endring ikke vil bryte selve koden. Det er en liten og enkel nok forandring for å ikke skremme oss veldig mye. Og viktigst, det gjør testene bestått.

Kjør det flere ganger

Nå som vi har kode som vi kan kjøre mange ganger, er det på tide å generere litt utdata.

funksjon 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 funksjon generere mange ($ ganger, $ filnavn) $ first = true; mens ($ ganger) if ($ first) file_put_contents ($ fileName, $ this-> generateOutput ()); $ first = false;  ellers file_put_contents ($ fileName, $ this-> generateOutput (), FILE_APPEND);  $ ganger--; 

Vi har hentet ut en annen metode her: generateMany (). Den har to parametere. En for antall ganger vi ønsker å kjøre vår generator, den andre er en destinasjonsfil. Det vil sette den genererte produksjonen i filene. På første runde tømmer det filene, og for resten av iterasjonene, legger det til dataene. Du kan se på filen for å se den genererte produksjonen 20 ganger.

Men vent! Den samme spilleren vinner hver gang? Er det mulig?

katt /tmp/gm.txt | grep "har 6 gullmynter." Chet har nå 6 gullmynter. Chet har nå 6 gullmynter. Chet har nå 6 gullmynter. Chet har nå 6 gullmynter. Chet har nå 6 gullmynter. Chet har nå 6 gullmynter. Chet har nå 6 gullmynter. Chet har nå 6 gullmynter. Chet har nå 6 gullmynter. Chet har nå 6 gullmynter. Chet har nå 6 gullmynter. Chet har nå 6 gullmynter. Chet har nå 6 gullmynter. Chet har nå 6 gullmynter. Chet har nå 6 gullmynter. Chet har nå 6 gullmynter. Chet har nå 6 gullmynter. Chet har nå 6 gullmynter. Chet har nå 6 gullmynter. Chet har nå 6 gullmynter.

Ja! Det er mulig! Det er mer enn mulig. Det er en sikker ting. Vi har samme frø for vår tilfeldige funksjon. Vi spiller det samme spillet igjen og igjen.

Kjør det annerledes hver gang

Vi må spille forskjellige spill, ellers er det nesten sikkert at bare en liten del av vår arvskode faktisk utøves igjen og igjen. Omfanget av den gyldne mesteren er å trene så mye som mulig. Vi må re-seed den tilfeldige generatoren hver gang, men på en kontrollert måte. Ett alternativ er å bruke vår teller som frøverdien.

privat funksjon generere mange ($ ganger, $ filnavn) $ first = true; mens ($ ganger) if ($ first) file_put_contents ($ fileName, $ this-> generateOutput ($ ganger)); $ first = false;  ellers file_put_contents ($ fileName, $ this-> generateOutput ($ ganger), FILE_APPEND);  $ ganger--;  privat funksjon generereOutput ($ seed) ob_start (); srand ($ frø); krever __DIR__. '/ ... /trivia/php/GameRunner.php'; $ output = ob_get_contents (); ob_end_clean (); returnere $ output; 

Dette holder fortsatt testen vår, så vi er sikker på at vi genererer samme fullverdige utgang hver gang, mens utgangen spiller et annet spill for hver iterasjon.

katt /tmp/gm.txt | grep "har 6 gullmynter." Sue har nå 6 gullmynter. Chet har nå 6 gullmynter. Chet har nå 6 gullmynter. Chet har nå 6 gullmynter. Chet har nå 6 gullmynter. Pat har nå 6 gullmynter. Pat har nå 6 gullmynter. Chet har nå 6 gullmynter. Chet har nå 6 gullmynter. Sue har nå 6 gullmynter. Chet har nå 6 gullmynter. Chet har nå 6 gullmynter. Sue har nå 6 gullmynter. Chet har nå 6 gullmynter. Sue har nå 6 gullmynter. Chet har nå 6 gullmynter. Chet har nå 6 gullmynter. Pat har nå 6 gullmynter. Chet har nå 6 gullmynter. Chet har nå 6 gullmynter.

Det er ulike vinnere for spillet på en tilfeldig måte. Dette ser bra ut.

Kommer til 20.000

Det første du kan prøve er å kjøre vår kode for 20.000 spillteerasjoner.

funksjonstestGenerateOutput () $ 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); 

Dette vil nesten fungere. To 55 MB filer vil bli generert.

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

På den annen side vil testen mislykkes med en utilstrekkelig minnefeil. Det spiller ingen rolle hvor mye RAM du har, dette vil mislykkes. Jeg har 8GB pluss en 4GB bytte og det mislykkes. De to strengene er bare for store til å bli sammenlignet i vår påstand.

Med andre ord genererer vi gode filer, men PHPUnit kan ikke sammenligne dem. Vi trenger en arbeidsplass.

$ this-> assertFileEquals ('/ tmp / gm.txt', '/tmp/gm2.txt');

Det synes å være en god kandidat, men det mislykkes fortsatt. Så synd. Vi må undersøke situasjonen ytterligere.

$ this-> assertTrue ($ file_content_gm == $ file_content_gm2);

Dette fungerer imidlertid.

Det kan sammenligne de to strengene og mislykkes hvis de er forskjellige. Det har imidlertid en liten pris. Det vil ikke kunne fortelle hva som er galt når strengene er forskjellige. Det vil bare si "Mislyktes hevdet at falsk er sant.". Men vi vil takle det i en kommende opplæring.

Siste tanker

Vi er ferdige for denne opplæringen. Vi har lært mye for vår første leksjon, og vi er på god start for vårt fremtidige arbeid. Vi møtte koden, vi analyserte den på forskjellige måter, og vi forstod det meste sin viktige logikk. Deretter opprettet vi et sett med tester for å sikre at den utøves så mye som mulig. Ja. Testen er veldig treg. Det tar dem 24 sekunder på min Core i7 CPU for å generere produksjonen to ganger. Heldigvis i vår fremtidige utvikling, vil vi beholde gm.txt fil uberørt og generer en annen bare én gang per runde. Men 12 sekunder er fortsatt en stor mengde tid for en så liten kodebase.

Når vi er ferdig med denne serien, bør testene våre gå på mindre enn et sekund og teste all koden riktig. Så hold deg innstilt for vår neste opplæring når vi skal takle problemer som magiske konstanter, magiske strenger og komplekse conditionals. Takk for at du leste.