Refactoring Legacy Code Del 7 - Identifisering av presentasjonslaget

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

I dette syvende kapittelet i våre refactoring opplæringsprogrammer, vil vi gjøre en annen type refactoring. Vi har i de siste leksjonene observert at det er presentasjonsrelatert kode spredt over vår arvskode. Vi vil prøve å identifisere all presentasjonsrelatert kode som vi kan, og vi vil da ta de nødvendige skritt for å skille den fra forretningslogikk.

Drivkraften

Når vi gjør en refactoring-endring i koden, gjør vi det slik at det blir styrt av noen prinsipper. Disse prinsippene og reglene hjelper oss med å identifisere problemene og i mange tilfeller peker de oss i riktig retning for å gjøre koden bedre.

Prinsipp for enkeltansvar (SRP)

SRP er et av SOLID-prinsippene vi snakket om i detalj i en tidligere opplæring: SOLID: Del 1 - Prinsippet om enkelt ansvar. Hvis du vil dykke inn i detaljene, anbefaler jeg at du leser artikkelen, ellers bare fortsett å lese videre og se et sammendrag av prinsippet om enkelt ansvar.

SRP sier i utgangspunktet at en hvilken som helst modul, klasse eller metode burde ha et enkelt ansvar. Et slikt ansvar er definert som en forandringsakse. En forandringsaks er en retning, en grunn til å forandre seg. Så, SRP betyr at vår klasse skal ha en enkelt grunn til å forandre seg.

Mens det høres ganske enkelt ut, hvordan definerer du en "grunn til forandring"? Vi må tenke på det fra brukerne av koden, både vanlige sluttbrukere og ulike programvareavdelinger. Disse brukerne kan bli representert som skuespillere. Når en skuespiller ønsker at vi skal endre koden, er det en grunn for endring som bestemmer en forandringsakse. En slik forespørsel skal bare påvirke en av våre moduler, klasser eller til og med metoder hvis det er mulig.

Et veldig åpenbart eksempel ville være hvis vårt UI-designteam ville kreve at vi skal gi all informasjon som må presenteres på en måte som vår søknad kan leveres over en HTML-webside, i stedet for vår nåværende kommandolinjegrensesnitt.

Som vår kode står i dag, kunne vi bare sende all teksten til en ekstern smart objekt som ville forvandle den til HTML. Men det kan bare fungere fordi HTML er mest tekstbasert. Hva om vårt UI-team ønsker å presentere vårt trivia-spill som et desktop-grensesnitt, med vinduer, knapper og ulike tabeller?

Hva om våre brukere vil se spillet på et virtuelt spillbrett representert som en by med gater, og spillerne som folk går rundt blokken?

Vi kunne identifisere disse menneskene som UI-skuespilleren. Og vi må innse at som vår kode står i dag, må vi endre vår trivia klasse og nesten alle sine metoder. Lyder det logisk å endre wasCorrectlyAnswered () metode fra Spill klasse hvis jeg vil fikse en skrivefeil på skjermen i en tekst, eller hvis jeg vil presentere vår trivia-programvare som et virtuelt spillbrett? Nei. Svaret er absolutt ikke.

Ren arkitektur

Ren arkitektur er et konsept som for det meste fremmes av Robert C. Martin. I utgangspunktet står det at vår forretningslogikk burde være godt definert og tydelig skilt av grenser fra andre moduler som ikke er relatert til kjernefunksjonaliteten til systemet vårt. Dette fører til avkoblet og testbar kode.

Du har kanskje sett denne tegningen gjennom hele mine opplæringsprogrammer og kurs. Jeg anser det så viktig at jeg aldri skriver kode eller snakker om kode uten å tenke på det. Det endret helt slik vi skriver kode på Syneto, og hvordan vårt prosjekt ser ut. Før vi hadde all vår kode i et MVC-rammeverk, med forretningslogikk i Modeller. Dette var både vanskelig å forstå og vanskelig å teste. I tillegg var forretningslogikken helt koblet til den spesifikke MVC-rammen. Selv om dette gjør det, kan det fungere med små kjæledyrprosjekter, når det gjelder et stort prosjekt som et selskaps fremtid avhenger, inkludert alle sine ansatte på en måte, må du slutte å leke med MVC-rammer, og du må begynne å tenke på hvordan organisere koden din Når du gjør dette, og får det riktig, vil du aldri gå tilbake til måtene du konstruerte prosjektene dine før.

Observerer tilhørighet

Vi har allerede begynt å skille vår forretningslogikk fra presentasjon i de forrige veiledningene. Vi observerte noen ganger noen utskriftsfunksjoner og hentet dem inn i private metoder i det samme Spill klasse. Dette var vårt bevisstløse sinn som fortalte oss å presse presentasjonen ut av forretningslogikk på metodnivå.

Nå er det på tide å analysere og observere.

Dette er listen over alle variabler, metoder og funksjoner fra vår Game.php fil. Tingene merket med en oransje "f" er variabler. Den røde "m" betyr metode. Hvis det etterfølges av en grønn lås, er den offentlig. Det er det etterfulgt av rød lås det er privat. Og fra den listen er alt det vi er interessert i, følgende del.

Alle de valgte metodene har noe til felles. Alle navnene deres begynner med "display" ... noe. De er alle metoder knyttet til utskrift av ting på skjermen. De ble alle identifisert av oss i tidligere opplæringsprogrammer og sømløst hentet, en om gangen. Nå må vi observere at de er en gruppe metoder som tilhører sammen. En gruppe som gjør en bestemt ting, tilfredsstiller et enkelt ansvar, viser informasjon på skjermen.

The Extract Class Refactoring

Best eksemplifisert og forklart i Refactoring - Forbedre utformingen av eksisterende kode av Martin Fowler, er grunnleggende ideen om Extract Class-refactoring at etter at du har innsett at klassen din fungerer og at den skal gjøres i to klasser, tar du tiltak for å lage to klasser. Det er spesifikk mekanikk til dette, som forklart i sitatet nedenfor fra ovennevnte bok.

  • Bestem hvordan du deler opp ansvaret for klassen.
  • Opprett en ny klasse for å uttrykke splitteansvaret.
    • Hvis ansvaret til den gamle klassen ikke lenger stemmer overens med navnet, omdøpe den gamle klassen.
  • Lag en lenke fra den gamle til den nye klassen.
    • Du må kanskje ha en toveis-kobling. Men gjør ikke tilbakekoblingen til du finner det du trenger.
  • Bruk Flytt felt på hvert felt du ønsker å flytte.
  • Kompil og test etter hvert trekk.
  • Bruk Flytt Metode til å flytte metoder over fra gammelt til nytt. Start med lavere nivå metoder (kalt i stedet for å ringe) og bygge til høyere nivå.
  • Kompil og test etter hvert trekk.
  • Gjennomgå og reduser grensesnittene til hver klasse.
    • Hvis du hadde en toveis-kobling, undersøk for å se om den kan gjøres på en måte.
  • Bestem om å utsette den nye klassen. Hvis du utsettes for klassen, bestemmer du om du skal eksponere det som et referanseobjekt eller som et objekt som ikke kan utføres.

Bruk av uttrekksklassen

Dessverre i øyeblikket for å skrive denne artikkelen, er det ingen IDE i PHP som kan gjøre en utdragsklasse ved bare å velge en gruppe metoder og bruke et alternativ fra menyen.

Da det aldri gjør vondt for å kjenne mekanikken til prosessene som innebærer å jobbe med kode, vil vi ta trinnene ovenfor, en etter en, og bruke dem til vår kode.

Bestemmer hvordan man deler opp ansvaret

Vi vet allerede dette. Vi ønsker å bryte presentasjonen fra forretningslogikken. Vi ønsker å ta ut, vise funksjoner og annen kode, og flytte dem et annet sted.

Opprett en ny klasse

Vår første handling er å skape en ny, tom klasse.

klassevisning  

Jepp. Det er alt for nå. Og det var også ganske enkelt å finne et riktig navn for det. Vise er ordet alle våre metoder vi er interessert i å begynne med. Det er fellesnevneren av navnene sine. Det er et veldig kraftig forslag til deres vanlige oppførsel, oppførselen, hvorefter vi heter vår nye klasse.

Hvis du foretrekker, og ditt programmeringsspråk støtter det, gjør PHP det, du kan opprette den nye klassen i samme fil som den gamle. Eller du kan opprette en ny fil for det fra starten. Jeg personlig fant ingen definitiv grunn til å gå enten eller for å forby noen av de to måtene. Så det er opp til deg. Bare velg og gå videre.

Kobling fra den gamle klassen til den nye klassen

Dette trinnet kan ikke høres veldig kjent ut. Hva det betyr er å erklære en klassevariabel i den gamle klassen og gjøre den til en forekomst av den nye.

require_once __DIR__. '/Display.php'; funksjon echoln ($ string) echo $ string. "\ N";  klassespill statisk $ minimumNumberOfPlayers = 2; statisk $ numberOfCoinsToWin = 6; privat $ display; // ... // funksjon __construct () // ... // $ this-> display = new Display ();  // ... alle de andre metodene ... //

Enkel. Er det ikke? I SpillKonstruktør vi bare initialiserte en privat klasse variabel som vi kalte det samme som den nye klassen, vise. Vi trengte også å inkludere Display.php filen i vår Game.php fil. Vi har ennå ikke en autoloader. Kanskje i en fremtidig opplæring vil vi introdusere en om nødvendig.

Og som vanlig, ikke glem å kjøre testene dine. Enhetstester er nok på dette stadiet, bare for å være sikker på at det ikke er noen typoer i den nylig lagt til koden.

Flyttingsfeltet og kompilere / teste

La oss ta disse to trinnene på en gang. Hvilke felt kan vi identifisere som skal gå fra Spill til Vise?

Ved å bare se på listen ...

statisk $ minimumNumberOfPlayers = 2; statisk $ numberOfCoinsToWin = 6; privat $ display; var $ spillere; var $ steder; var $ purses; var $ inPenaltyBox; var $ popQuestions; var $ scienceQuestions; var $ sportsQuestions; var $ rockQuestions; var $ currentPlayer = 0; var $ isGettingOutOfPenaltyBox; 

... vi kan ikke finne noen variabel / felt som må tilhøre Vise. Kanskje noen kommer opp i tide. Så ingenting å gjøre for dette trinnet. Og om testene, kjørte vi dem allerede et øyeblikk. På tide å komme seg videre.

Flytt metoder til den nye klassen

Dette er i seg selv en annen refactoring. Du kan gjøre det på flere måter, og du finner en fin definisjon av den i samme bok som vi snakket om tidligere.

Som nevnt ovenfor, bør vi begynne med det laveste nivået av metoder. De som ikke ringer andre metoder. I stedet blir de kalt.

privat funksjon displayPlayersNewLocation () echoln ($ this-> spillere [$ this-> currentPlayer]. 's nye plassering er ". $ this-> places [$ this-> currentPlayer]); 

displayPlayersNewLocation () ser ut til å være en god kandidat. La oss analysere hva den gjør.

Vi kan se at det ikke kaller andre metoder på Spill. I stedet bruker den tre felt: spillere, currentPlayer, og steder. De kan bli til to eller tre parametere. Så langt ganske fint. Men hva med echoln (), det eneste funksjonssamtalen i vår metode? Hvor er dette echoln () kommer fra?

Det er på toppen av vår Game.php fil, utenfor Spill klassen selv.

funksjon echoln ($ string) echo $ string. "\ N"; 

Det gjør absolutt det som står. Ekko en streng med en ny linje på slutten. Og dette er ren presentasjon. Det skal gå inn i Vise klasse. Så la oss kopiere den der borte.

klasse Display funksjon echoln ($ string) echo $ string. "\ N"; 

Kjør testene våre igjen. Vi kan holde den gyldne mesteren deaktivert til vi er ferdig med å trekke ut hele presentasjonen til den nye Vise klasse. Når som helst, hvis du føler at produksjonen kan ha blitt endret, kan du også kjøre de gyldne mestertestene igjen. På dette tidspunktet vil testene attestere at vi ikke introduserte skrivefeil eller duplikatfunksjonserklæringer, eller andre feil for den saks skyld, ved å kopiere funksjonen til sitt nye sted.

Nå, gå og slett echoln () fra Game.php fil, kjør testene våre, og forvent at de skal mislykkes.

PHP Fatal feil: Ring til udefinert funksjon echoln () i / ... /Game.php på linje 55

Hyggelig! Vår enhetstest er til stor hjelp her. Den går veldig fort og forteller oss nøyaktig plasseringen av problemet. Vi går til linje 55.

Se! Det er en echoln () ring det. Tester lyver aldri. La oss fikse det ved å ringe $ Dette-> dipslay-> echoln () i stedet.

funksjon add ($ playerName) array_push ($ this-> spillere, $ playerName); $ Dette-> setDefaultPlayerParametersFor ($ dette-> howManyPlayers ()); $ this-> display-> echoln ($ playerName. "ble lagt til"); echoln ("De er spiller nummer". count ($ this-> spillere)); returnere sant; 

Det gjør at testen passerer gjennom linje 55 og mislykkes på 56.

PHP Fatal feil: Ring til udefinert funksjon echoln () i / ... /Game.php på linje 56

Og løsningen er åpenbar. Dette er en kjedelig prosess, men det er i det minste lett.

funksjon add ($ playerName) array_push ($ this-> spillere, $ playerName); $ Dette-> setDefaultPlayerParametersFor ($ dette-> howManyPlayers ()); $ this-> display-> echoln ($ playerName. "ble lagt til"); $ this-> display-> echoln ("De er spiller nummer". count ($ this-> spillere)); returnere sant; 

Det gjør faktisk de første tre testene, og forteller oss det neste stedet der det er et anrop som vi bør bytte.

PHP Fatal feil: Ring til udefinert funksjon echoln () i / ... /Game.php på linje 169

Det er i feil svar().

funksjon feilAnswer () echoln ("Spørsmål ble feil besvart"); echoln ($ this-> spillere [$ this-> currentPlayer]. "ble sendt til straffefeltet"); $ this-> inPenaltyBox [$ this-> currentPlayer] = true; $ Dette-> currentPlayer ++; hvis ($ this-> shouldResetCurrentPlayer ()) $ this-> currentPlayer = 0;  returnere sann; 

Ved å fikse disse to anropene, skyver vi vår feil ned til linje 228.

privat funksjon displayCurrentPlayer () echoln ($ this-> spillere [$ this-> currentPlayer]. "er den nåværende spilleren"); 

EN vise metode! Kanskje dette burde være vår første metode for å flytte. Vi prøver å gjøre en liten testdrevet utvikling (TDD) her. Og når testene feiler, har vi ikke lov til å skrive mer produksjonskode som ikke er helt nødvendig for å gjøre testen pass. Og alt som innebærer endrer bare echoln () Ringer til alle enhetstestene våre passerer.

Du kan øke hastigheten på denne prosessen ved å bruke IDE-en eller redigerens søk og erstatte funksjonalitet. Bare kjør alle tester, inkludert den gyldne mesteren, etter at du er ferdig med denne utskiftningen. Våre enhetstester dekker ikke hele koden, og alle echoln () samtaler.

Vi kan starte med første kandidat, displayCurrentPlayer (). Kopier det over til Vise og kjør testene dine.

Så gjør det offentlig på Vise og i displayCurrentPlayer () i Spill anrop $ Dette-> Skjerm> displayCurrentPlayer () i stedet for direkte å gjøre en echoln (). Endelig kjør testene dine.

De vil mislykkes. Men ved å gjøre endringen på denne måten har vi sikret at vi bare endret en ting som kunne mislykkes. Alle andre metoder ringer fortsatt Spill's displayCurrentPlayer (). Og dette er den som delegerer til Vise.

 Udefinert eiendom: Display :: $ display

Vår metode bruker klassefelt. Disse må gjøres parametere til funksjonen. Hvis du følger testfeilene dine, bør du ende opp med noe som dette i Spill.

privatfunksjonsdisplayCurrentPlayer () $ this-> display-> displayCurrentPlayer ($ this-> spillere [$ this-> currentPlayer]); 

Og dette i Vise.

funksjonsvisningCurrentPlayer ($ currentPlayer) $ this-> echoln ($ currentPlayer. "er den nåværende spilleren"); 

Erstatt anrop i Spill til den lokale metoden med den i Vise. Ikke glem å flytte parametrene opp ett nivå, også.

privatfunksjonsdisplayStatusAfterRoll ($ rolledNumber) $ this-> display-> displayCurrentPlayer ($ this-> spillere [$ this-> currentPlayer]); $ Dette-> displayRolledNumber ($ rolledNumber); 

Til slutt fjern den ubrukte metoden fra Spill. Og kjør testene dine for å sikre at alt er OK.

Dette er en kjedelig prosess. Du kan øke hastigheten litt ved å ta flere metoder på en gang og bruke hva som helst IDE kan gjøre for å flytte og erstatte kode mellom klasser. Resten av metodene vil forbli en øvelse for deg, eller du kan lese mer om dette kapittelet med høydepunktene i prosessen. Den ferdige koden som er vedlagt denne artikkelen, inneholder den komplette Vise klasse.

Ah, og ikke glem koden som ennå ikke er hentet i "display" -metodene inni Spill. Du kan flytte dem echoln () samtaler for å vise direkte. Vårt mål er å ikke ringe echoln () i det hele tatt fra Spill, og gjør det privat på Vise.

Etter bare en halv time eller så med jobb, Vise begynner å se fint ut.

Alle visningsmetoder fra Spill er i Vise. Nå kan vi se etter alle echoln samtaler som ble igjen i Spill og flytt dem også. Tester går forbi, selvfølgelig.

Men så snart vi står overfor spør spørsmål() metode, innser vi at det bare er presentasjonskode også. Og det betyr at de ulike spørsmålene også skal gå til Vise.

klasse Vis private $ popQuestions = []; privat $ scienceQuestions = []; private $ sportsQuestions = []; privat $ rockQuestions = []; funksjon __construct () $ this-> initializeQuestions ();  // // privat funksjon initializeQuestions () $ categorySize = 50; for ($ i = 0; $ i < $categorySize; $i++)  array_push($this->popQuestions, "Pop Question". $ I); array_push ($ this-> scienceQuestions, ("Science Question". $ i)); array_push ($ this-> sportsQuestions, ("Sports Question". $ i)); array_push ($ this-> rockQuestions, "Rock Question". $ i); 

Det ser passende ut. Spørsmål er bare strenger, vi presenterer dem og de passer bedre her. Når vi gjør denne typen refactoring, er det også en god mulighet til å refactor den nylig flyttede koden. Vi definerte innledende verdier i feltdeklarasjonen, vi har også gjort dem private, og opprettet en metode med koden som må utføres slik at den ikke bare dveler i konstruktøren. I stedet er den skjult nederst i klassen, ut av veien.

Etter å ha hentet de to neste metodene, innser vi at det er bedre å nevne dem, inne i Vise klasse, uten "display" prefiks.

funksjon correctAnswer () $ this-> echoln ("Svar var riktig!");  funksjons playerCoins ($ currentPlayer, $ playerCoins) $ this-> echoln ($ currentPlayer. "har nå". $ playerCoins. "Gold Coins."); 

Med våre tester grønne og bra, kan vi nå refactor og omdøpe våre metoder. PHPStorm kan håndtere omdøpsreaktorene ganske bra. Det vil gi nytt navn til funksjonssamtaler i Spill tilsvarende. Så er det dette koden.

Se nøye på den valgte linjen, 119. Det ser akkurat ut som vår nylig hentede metode i Vise.

funksjon correctAnswer () $ this-> echoln ("Svar var riktig!"); 

Men hvis vi kaller det i stedet for koden, vil testen mislykkes. Ja! Det er en skrivefeil. Og nei! Du bør ikke fikse det. Vi er refactoring. Vi må holde funksjonaliteten uendret, selv om det er en feil.

Resten av metoden representerer ingen spesiell utfordring.

Gjennomgå og redusere grensesnittene

Nå som all presentasjonsfunksjonalitet er inne Vise, Vi må gjennomgå metodene og holde offentlig bare de som brukes i Spill. Dette trinnet er også motivert av Interface Segregation Principle som vi snakket om i en tidligere opplæring.

I vårt tilfelle er den enkleste måten å finne ut hvilke metoder som skal være offentlige eller private, å bare gjøre hver enkelt privat om gangen, kjøre testene, og hvis de ikke klarer å gå tilbake til det offentlige.

Fordi gylne mestertester går sakte, kan vi også stole på vår IDE for å hjelpe oss med å fremskynde prosessen. PHPStorm er smart nok til å finne ut om en metode er ubrukt. Hvis vi gjør en metode privat, og det plutselig blir ubrukt, er det klart det ble brukt utenfor Vise og må forbli offentlig.

Til slutt kan vi omorganisere Vise slik at private metoder er på slutten av klassen.

Siste tanker

Nå er det siste trinnet i Extract Class Refactoring-prinsippet irrelevant i vårt tilfelle. Så med dette avsluttes opplæringen, men dette avslutter ikke serien enda. Hold deg oppdatert på vår neste artikkel, hvor vi vil arbeide videre mot en ren arkitektur og inverte avhengigheter.