Refactoring Legacy Code Del 7 - Identifiering av presentationslagret

Gammal kod. Ugly code. Komplicerad kod. Spaghetti kod. Gibberish nonsens. I två ord, Legacy Code. Det här är en serie som hjälper dig att arbeta och hantera det.

I det här sjunde kapitlet i våra refactoring-tutorials kommer vi att göra en annan typ av refactoring. Vi observerade i de senaste lektionerna att det finns presentationsrelaterad kod spridda över vår arvskod. Vi kommer att försöka identifiera alla presentationsrelaterade kod som vi kan och vi kommer då att vidta nödvändiga åtgärder för att skilja den från affärslogiken.

Körkraften

När vi gör en refactoringändring till vår kod, så gör vi det enligt vissa principer. Dessa principer och regler hjälper oss att identifiera problemen och i många fall pekar de på oss i rätt riktning för att göra koden bättre.

Principen om ansvarsområde (SRP)

SRP är en av SOLID-principerna som vi pratade om i detalj i en tidigare handledning: SOLID: Del 1 - Principen om ensam ansvar. Om du vill dyka in i detaljerna rekommenderar jag att du läser artikeln, annars fortsätter du att läsa vidare och se en sammanfattning av principen om ensam ansvar.

SRP säger i grund och botten att någon modul, klass eller metod ska ha ett enda ansvar. Ett sådant ansvar definieras som en förändringsaxel. En förändringsaxel är en riktning, en anledning att förändras. Så, SRP betyder att vår klass borde ha en enda anledning att ändra.

Medan det låter ganska enkelt, hur definierar du en "orsak till förändring"? Vi måste tänka på det från användarna av vår kod, både vanliga slutanvändare och olika programvaruavdelningar. Dessa användare kan representeras som aktörer. När en skådespelare vill att vi ska ändra vår kod, är det en orsak till förändring som bestämmer en förändringsaxel. En sådan begäran bör endast påverka en av våra moduler, klasser eller till och med metoder om möjligt.

Ett mycket uppenbart exempel skulle vara om vårt UI-designteam skulle kräva att vi tillhandahåller all information som måste presenteras på ett sätt som vår ansökan kan levereras via en HTML-webbsida, i stället för vårt nuvarande kommandoradsgränssnitt.

Som vår kod står idag kunde vi bara skicka all text till ett externt smart objekt som skulle omvandla det till HTML. Men det kan bara fungera eftersom HTML är mestadels textbaserad. Vad händer om vårt UI-team vill presentera vårt trivia-spel som skrivbords-gränssnitt, med fönster, knappar och olika tabeller?

Vad händer om våra användare vill se spelet på ett virtuellt spelkort som representeras som en stad med gator, och spelarna som människor går runt blocket?

Vi kunde identifiera dessa personer som UI-aktören. Och vi måste inse att som vår kod står idag, skulle vi behöva ändra vår trivia-klass och nästan alla dess metoder. Låter det logiskt att ändra wasCorrectlyAnswered () metod från Spel klass om jag vill fixa ett typsnitt på skärmen i en text, eller om jag vill presentera vår trivia-programvara som ett virtuellt spelkort? Nej. Svaret är absolut inte.

Ren arkitektur

Ren arkitektur är ett koncept främst främst av Robert C. Martin. I grund och botten står det att vår affärslogik bör vara väldefinierad och tydligt åtskilda av gränser från andra moduler som inte är relaterade till kärnfunktionaliteten i vårt system. Detta leder till avkopplad och högt testbar kod.

Du kanske har sett denna ritning i hela mina tutorials och kurser. Jag anser det vara så viktigt att jag aldrig skriver kod eller pratar om kod utan att tänka på det. Det förändrade helt hur vi skriver kod på Syneto, och hur vårt projekt ser ut. Innan vi hade all vår kod i en MVC-ram, med affärslogik i Modellerna. Det var både svårt att förstå och svårt att testa. Dessutom var affärslogiken helt kopplad till den specifika MVC-ramen. Medan det här kan fungera med små husdjursprojekt, när det gäller ett stort projekt som framtiden för ett företag beror på, inklusive alla sina anställda på ett sätt, måste du sluta leka med MVC-ramar, och du måste börja tänka på hur man organiserar din kod När du gör det här och får det rätt, kommer du aldrig vilja återvända till de sätt du konstruerade dina projekt förut.

Observation Belonging

Vi har redan börjat separera vår affärslogik från presentation i de föregående handledningarna. Vi observerade ibland vissa utskriftsfunktioner och extraherade dem till privata metoder i samma Spel klass. Det var vårt omedvetna sinne som berättade för oss att driva fram presentation av affärslogik på metodnivå.

Nu är det dags att analysera och observera.

Detta är listan över alla variabler, metoder och funktioner från vår Game.php fil. De saker som markeras med en orange "f" är variabler. Den röda "m" betyder metod. Om det följs av ett grönt lås är det offentligt. Det följs av röda lås det är privat. Och från den listan är allt som vi är intresserade av, följande del.

Alla valda metoder har något gemensamt. Alla deras namn börjar med "display" ... något. De är alla metoder för att skriva ut saker på skärmen. De identifierades alla av oss i tidigare handledning och sömlöst extraherades, en i taget. Nu måste vi observera att de är en grupp metoder som hör samman. En grupp som gör en viss sak uppfyller ett enda ansvar, visar information på skärmen.

Extract Class Refactoring

Bäst exemplifieras och förklaras i Refactoring - Förbättra designen av befintlig kod av Martin Fowler, är grundtanken i Extract Class Refactoring att efter att du inser att din klass fungerar och att det ska ske i två klasser, vidtar du åtgärder för att göra två klasser. Det finns specifika mekanik för detta, som förklaras i citatet nedan från ovan nämnda bok.

  • Bestäm hur du delar upp ansvaret för klassen.
  • Skapa en ny klass för att uttrycka uppdelningsansvaret.
    • Om den gamla klassens ansvar inte längre matchar sitt namn, byt namn på den gamla klassen.
  • Gör en länk från den gamla till den nya klassen.
    • Du kan behöva en tvåvägs länk. Men gör inte tillbaka länken tills du finner att du behöver det.
  • Använd Flytta fält på varje fält du vill flytta.
  • Kompilera och testa efter varje drag.
  • Använd Flytta Metod för att flytta metoder över från gammalt till nytt. Börja med metoder på lägre nivå (kallad snarare än att ringa) och bygg till högre nivå.
  • Kompilera och testa efter varje drag.
  • Granska och minska gränsytorna för varje klass.
    • Om du hade en tvåvägs länk, undersök för att se om det kan göras på ett sätt.
  • Bestäm om huruvida den nya klassen ska exponeras. Om du exponerar klassen bestämmer du om du vill exponera den som ett referensobjekt eller som ett obestridligt värdeobjekt.

Applicera utdragsklassen

Tyvärr när du skriver den här artikeln finns det ingen IDE i PHP som kan göra en extraktklass genom att bara välja en grupp metoder och tillämpa ett alternativ från menyn.

Eftersom det aldrig gör ont för att känna mekanismerna för de processer som innebär att de arbetar med kod, tar vi stegen ovan, en efter en och tillämpar dem på vår kod.

Besluta hur man delar upp ansvaret

Vi vet redan detta. Vi vill bryta presentationen från affärslogiken. Vi vill ta ut, visa funktioner och annan kod och flytta dem någon annanstans.

Skapa en ny klass

Vår första åtgärd är att skapa en ny, tom klass.

klassskärm  

Japp. Det var allt tills vidare. Att hitta ett riktigt namn för det var också ganska enkelt. Visa är ordet alla våra metoder vi är intresserade av att börja med. Det är den gemensamma nämnaren av deras namn. Det är ett mycket kraftfullt förslag om sitt vanliga beteende, beteendet därpå vi heter vår nya klass.

Om du föredrar och ditt programmeringsspråk stöder det, gör PHP det, du kan skapa den nya klassen i samma fil som den gamla. Eller du kan skapa en ny fil för det från början. Personligen fann jag ingen definitiv anledning att gå någonstans eller att förbjuda något av de två sätten. Så det är upp till dig. Bara bestämma och gå vidare.

Länkar från den gamla klassen till den nya klassen

Det här steget kanske inte låter mycket bekant. Vad det innebär är att deklarera en klassvariabel i den gamla klassen och göra den till en förekomst av den nya.

require_once __DIR__. '/Display.php'; funktion echoln ($ string) echo $ string. "\ N";  klassspel static $ minimumNumberOfPlayers = 2; statiskt $ numberOfCoinsToWin = 6; privat $ display; // // // // __construct () // ... // $ this-> display = new Display ();  // ... alla andra metoder ... //

Enkel. Är det inte I Spels konstruktör vi just initierade en privat klass variabel som vi heter samma som den nya klassen, visa. Vi behövde också inkludera Display.php filen i vår Game.php fil. Vi har ännu inte en autoloader. Kanske i en framtida handledning introducerar vi en om det behövs.

Och som vanligt, glöm inte att köra dina tester. Enhetstest är tillräckligt i det här läget, för att se till att det inte finns några skrivfel i den nyligen tillagda koden.

Flyttningsfältet och kompilera / testa

Låt oss ta dessa två steg på en gång. Vilka fält kan vi identifiera som ska gå från Spel till Visa?

Genom att bara titta på listan ...

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

... vi kan inte hitta någon variabel / fält som måste tillhöra Visa. Kanske kommer vissa att dyka upp i tid. Så inget att göra för detta steg. Och om testerna, körde vi redan dem för en stund sedan. Dags att gå vidare.

Flytta metoder till den nya klassen

Detta är i sig en annan refactoring. Du kan göra det på flera sätt och du hittar en fin definition av den i samma bok som vi pratade om tidigare.

Som nämnts ovan bör vi börja med den lägsta nivån av metoder. De som inte ringer andra metoder. Istället kallas de.

privat funktion displayPlayersNewLocation () echoln ($ this-> spelare [$ this-> currentPlayer]. "s nya plats är". $ this-> places [$ this-> currentPlayer]); 

displayPlayersNewLocation () verkar vara en bra kandidat. Låt oss analysera vad det gör.

Vi kan se att det inte kallar andra metoder på Spel. Istället använder den tre fält: spelare, currentPlayer, och platser. De kan bli två eller tre parametrar. Hittills ganska trevligt. Men vad sägs om echoln (), det enda funktionssamtalet i vår metod? Var är detta echoln () kommer från?

Det ligger högst upp i vårt Game.php fil, utanför Spel klassen själv.

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

Det gör definitivt vad det säger. Echoes en sträng med en ny linje i slutet. Och det här är ren presentation. Det borde gå in i Visa klass. Så låt oss kopiera det där borta.

klass Display funktion echoln ($ string) echo $ string. "\ N"; 

Kör våra tester igen. Vi kan hålla den gyllene mästaren inaktiverad tills vi slutar att extrahera hela presentationen till den nya Visa klass. När som helst, om du känner att utsignalen kan ha ändrats, kör du igen de gyllene mastertesterna. Vid denna tidpunkt kommer testen att intyga att vi inte införde typsnitt eller dubbla funktionsdeklarationer eller några andra fel för den delen genom att kopiera funktionen till dess nya plats.

Nu, gå och ta bort echoln () från Game.php fil, kör våra test och förvänta dem att misslyckas.

PHP Felaktigt fel: Ring till odefinierad funktion echoln () i / ... /Game.php på rad 55

Trevlig! Vårt enhetstest är till stor hjälp här. Det går mycket snabbt och det berättar för oss om den exakta positionen av problemet. Vi går till linje 55.

Se! Där är en echoln () ring dit. Tester ljuger aldrig. Låt oss fixa det genom att ringa ($ This-> dipslay-> echoln) istället.

funktion add ($ playerName) array_push ($ this-> spelare, $ playerName); $ This-> setDefaultPlayerParametersFor ($ this-> howManyPlayers ()); $ this-> display-> echoln ($ playerName. "var tillagt"); echoln ("De är spelarnummer". count ($ this-> players)); återvänd sant; 

Det gör att testet passerar genom linje 55 och misslyckas med 56.

PHP Felaktigt fel: Ring till odefinierad funktion echoln () i / ... /Game.php på rad 56

Och lösningen är uppenbar. Det här är en tråkig process, men det är åtminstone lätt.

funktion add ($ playerName) array_push ($ this-> spelare, $ playerName); $ This-> setDefaultPlayerParametersFor ($ this-> howManyPlayers ()); $ this-> display-> echoln ($ playerName. "var tillagt"); $ this-> display-> echoln ("De är spelarnummer". count ($ this-> players)); återvänd sant; 

Det gör faktiskt de första tre testerna och berättar för oss nästa plats där det finns ett samtal som vi borde byta.

PHP Felaktigt fel: Ring till odefinierad funktion echoln () i / ... /Game.php på rad 169

Det är i fel svar().

funktionen felAnswer () echoln ("Frågan svarades felaktigt"); echoln ($ this-> spelare [$ this-> currentPlayer]. "skickades till strafffältet"); $ this-> inPenaltyBox [$ this-> currentPlayer] = true; $ This-> currentPlayer ++; om ($ this-> shouldResetCurrentPlayer ()) $ this-> currentPlayer = 0;  returnera sant; 

Om du fixar dessa två samtal trycker vi vårt fel ner till linje 228.

privatfunktionsdisplayCurrentPlayer () echoln ($ this-> spelare [$ this-> currentPlayer]. "är den aktuella spelaren"); 

en visa metod! Kanske bör detta vara vår första metod att flytta. Vi försöker göra en liten testdriven utveckling (TDD) här. Och när testerna misslyckas får vi inte skriva någon mer produktionskod som inte är absolut nödvändig för att provet ska gå över. Och allt som medför ändrar bara echoln () samtal tills alla våra enhetstester passerar.

Du kan påskynda processen genom att använda din IDE: s eller redaktörens sök- och ersättningsfunktion. Kör bara alla tester, inklusive den gyllene mästaren när du är klar med denna ersättning. Våra testsatser täcker inte hela koden, och alla echoln () samtal.

Vi kan börja med första kandidaten, displayCurrentPlayer (). Kopiera den till Visa och kör dina test.

Gör det sedan offentligt på Visa och i displayCurrentPlayer () i Spel ring upp $ This-> display-> displayCurrentPlayer () istället för att direkt göra en echoln (). Slutligen kör dina test.

De kommer att misslyckas. Men genom att göra förändringen på detta sätt har vi försäkrat oss om att vi bara ändrade en sak som kunde misslyckas. Alla andra metoder ringer fortfarande Spel's displayCurrentPlayer (). Och det här är den som delegerar till Visa.

 Odefinierad egendom: Display :: $ display

Vår metod använder klassfält. Dessa måste göras parametrar till funktionen. Om du följer dina testfel borde du sluta med något så här i Spel.

privatfunktionsdisplayCurrentPlayer () $ this-> display-> displayCurrentPlayer ($ this-> spelare [$ this-> currentPlayer]); 

Och det här in Visa.

funktionsdisplayCurrentPlayer ($ currentPlayer) $ this-> echoln ($ currentPlayer. "är den aktuella spelaren"); 

Byt samtal i Spel till den lokala metoden med den i Visa. Glöm inte att flytta parametrarna upp en nivå, också.

privatfunktionsdisplayStatusAfterRoll ($ rolledNumber) $ this-> display-> displayCurrentPlayer ($ this-> spelare [$ this-> currentPlayer]); $ This-> displayRolledNumber ($ rolledNumber); 

Slutligen ta bort den oanvända metoden från Spel. Och kör dina test för att se till att allt är OK.

Detta är en tråkig process. Du kan påskynda det lite genom att ta flera metoder på en gång och använda vad som helst som din IDE kan göra för att hjälpa till att flytta och ersätta kod mellan klasser. Resten av metoderna kommer att förbli en övning för dig, eller du kan läsa mer om detta kapitel med processens höjdpunkter. Den färdiga koden som bifogas denna artikel innehåller fullständig Visa klass.

Åh, och glöm inte koden som ännu inte är extraherad i "visnings" -metoderna inuti Spel. Du kan flytta dem echoln () samtal för att visa direkt. Vårt mål är att inte ringa echoln () alls från Spel, och gör det privat på Visa.

Efter bara en halvtimme eller så om arbete, Visa börjar se bra ut.

Alla visningsmetoder från Spel är i Visa. Nu kan vi leta efter alla echoln samtal som var kvar i Spel och flytta dem också. Tester passerar förstås.

Men så snart vi står inför frågar frågan() metod vi inser att det bara är presentationen kod också. Och det betyder att de olika frågeställningarna också ska gå till Visa.

klass Visa privat $ popQuestions = []; privat $ scienceQuestions = []; privat $ sportsQuestions = []; privat $ rockQuestions = []; funktion __construct () $ this-> initializeQuestions ();  // // Privata funktioner initieraQuestions () $ categorySize = 50; för ($ i = 0; $ i < $categorySize; $i++)  array_push($this->popQuestions, "Pop Question". i $); array_push ($ this-> scienceQuestions, ("Science Question". $ i)); array_push ($ this-> sportsQuestions, ("Sportfråga". $ i)); array_push ($ this-> rockQuestions, "Rock Question". $ i); 

Det ser passande ut. Frågor är bara strängar, vi presenterar dem och de passar bättre här. När vi gör denna typ av refactoring, är det också ett bra tillfälle att refactor den nyligen flyttade koden. Vi definierade initialvärden i fältdeklarationen, vi gjorde dem också privata och skapade en metod med koden som måste utföras så att den inte bara dröjer i konstruktören. I stället är det dolt längst ner i klassen, ur vägen.

Efter att ha tagit fram de två följande metoderna inser vi att det är trevligare att namnge dem, inuti Visa klass, utan prefixet "display".

funktion correctAnswer () $ this-> echoln ("Svar var korrekt !!!!");  funktion playerCoins ($ currentPlayer, $ playerCoins) $ this-> echoln ($ currentPlayer. "har nu". $ playerCoins. "Gold Coins."); 

Med våra test gröna och bra, kan vi nu refactor och byta namn på våra metoder. PHPStorm kan hantera omdämningsreaktorerna ganska bra. Det kommer att omdöpa funktionssamtal i Spel följaktligen. Då finns det här kodstycke.

Titta noggrant på den valda linjen, 119. Det ser ut som vår nyligen extraherade metod i Visa.

funktion correctAnswer () $ this-> echoln ("Svar var korrekt !!!!"); 

Men om vi kallar det istället för koden, kommer testet att misslyckas. ja! Det finns en typsnitt. Och nej! Du borde inte fixa det. Vi refactoring. Vi måste hålla funktionaliteten oförändrad, även om det finns en bugg.

Resten av metoden representerar ingen speciell utmaning.

Granska och minska gränssnittet

Nu när alla presentationsfunktioner finns i Visa, Vi måste granska metoderna och hålla offentliga bara de som används Spel. Detta steg motiveras också av gränssnittssegregationsprincipen som vi pratade om i en tidigare handledning.

I vårt fall är det enklaste sättet att räkna ut vilka metoder som behöver vara offentliga eller privata, att bara göra var och en privat åt gången, kör testen, och om de misslyckas återgår till allmänheten.

Eftersom gyllene mastertester går långsamt kan vi också lita på vår IDE för att hjälpa oss att påskynda processen. PHPStorm är tillräckligt smart för att ta reda på om en metod är oanvänd. Om vi ​​gör en metod privat, och det blir plötsligt oanvänd, är det klart att den användes utanför Visa och måste förbli offentligt.

Slutligen kan vi omorganisera Visa så att privata metoder är i slutet av klassen.

Slutgiltiga tankar

Nu är det sista steget i Extract Class Refactoring-principen irrelevant i vårt fall. Så med det här avslutas handledningen, men det här avslutar inte serien ännu. Håll kontakten med vår nästa artikel där vi kommer att arbeta vidare mot en ren arkitektur och invertera beroende.