Refactoring Legacy Code Del 8 - Inverterande beroende för en ren arkitektur

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.

Nu är det dags att prata om arkitektur och hur vi organiserar våra nyfunna lag av kod. Det är dags att ta vår ansökan och försöka kartlägga den till teoretisk arkitektonisk design.

Ren arkitektur

Det här har vi sett igenom våra artiklar och handledning. Ren arkitektur.

På en hög nivå ser det ut som schemat ovan och jag är säker på att du redan är bekant med den. Det är en föreslagen arkitektonisk lösning av Robert C. Martin.

I centrum av vår arkitektur är vår affärslogik. Dessa är de klasser som representerar de affärsprocesser som vår ansökan försöker lösa. Dessa är de enheter och interaktioner som representerar domänen i vårt problem.

Sedan finns det flera andra typer av moduler eller klasser kring vår affärslogik. Dessa kan ses som enkla att hjälpa till med extra moduler. De har olika syften och de flesta är oumbärliga. De ger anslutningen mellan användaren och vår ansökan via en leveransmekanism. I vårt fall är detta ett kommandoradsgränssnitt. Det finns en annan uppsättning av hjälpklasser som förbinder vår affärslogik med vårt persistenslager och alla data i det lagret, men vi har inte ett sådant lager i vår applikation. Därefter hjälper det klasser som fabriker och byggare som bygger och ger nya objekt till vår affärslogik. Slutligen finns det klasserna som representerar ingången till vårt system. I vårat fall, GameRunner kan betraktas som en sådan klass, eller alla våra tester är också ingångspunkter på egen väg.

Det som är viktigast att märka på diagrammet är beroendet. Alla hjälpklasser beror på affärslogiken. Affärslogiken är inte beroende av något annat. Om alla objekt i vår affärslogik skulle kunna visas magiskt, med all data i dem, och vi kunde se vad som händer inom vår dator direkt, borde de kunna fungera. Vår affärslogik måste kunna fungera utan gränssnitt eller utan ett uthållighetslager. Vår affärslogik måste finnas isolerad, i en bubbla i ett logiskt universum.

Dependensinversionsprincipen

A. Högnivåmoduler bör inte bero på lågnivåmoduler. Båda borde bero på abstraktioner.
B. Abstraktioner bör inte bero på detaljer. Detaljer bör bero på abstraktioner.

Det här är den, den sista SOLID-principen och förmodligen den som har störst effekt på din kod. Det är både ganska enkelt att förstå och ganska enkelt att implementera.

Enkelt sagt står det att konkreta saker alltid bör bero på abstrakta saker. Din databas är mycket konkret, så det borde bero på något mer abstrakt. Din användargränssnitt är väldigt konkret, så det borde bero på något mer abstrakt. Dina fabriker är mycket konkreta igen. Men hur är din affärslogik. Inom din affärslogik bör du fortsätta att tillämpa dessa idéer, så att klasserna som ligger närmare gränserna beror på mer abstrakta klasser, mer i centrum för din affärslogik.

En ren affärslogik representerar på ett abstrakt sätt processer och beteenden hos en definierad domän eller affärsmodell. En sådan affärslogik innehåller inga specifika saker (konkreta saker) som värden, pengar, kontonamn, lösenord, storleken på en knapp eller antalet fält i en form. Affärslogiken ska inte bry sig om konkreta saker. Det bör bara bry sig om dina affärsprocesser.

Det tekniska tricket

Så säger Dependency Inversion Princip (DIP) att vi ska invertera våra beroende närhelst det finns kod som beror på något konkret. Just nu ser vår beroendestruktur ut så här.

GameRunner, använder funktionerna i RunnerFunctions.php skapar en Spel klass och använder sedan den. Å andra sidan vår Spel klass, som representerar vår affärslogik, skapar och använder a Visa objekt.

Så beror löparen på vår affärslogik. Det är korrekt. Å andra sidan vår Spel beror på Visa, vilket inte är bra. Vår affärslogik borde aldrig bero på vår presentation.

Det enklaste tekniska tricket vi kan göra är att utnyttja de abstrakta konstruktionerna i vårt programmeringsspråk. En traditionell klass är mer konkret än en abstrakt klass, som är mer konkret än ett gränssnitt.

En Abstrakt klass är en speciell typ som inte kan initieras. Den innehåller bara definitioner och partiella implementeringar. En abstrakt basklass har vanligtvis flera barnklasser. Dessa barnklasser arverar den gemensamma partiella funktionaliteten från den abstrakta föräldern, de lägger till sitt eget utökade beteende, och de måste genomföra alla metoder som definieras i abstrakt förälder men inte implementeras i det.

En Gränssnitt är en speciell typ som endast tillåter definition av metoder och variabler. Det är den mest abstrakta konstruktionen i objektorienterad programmering. Eventuellt genomförande måste alltid genomföra alla metoder i sitt modergränssnitt. En konkret klass kan genomföra flera gränssnitt.

Förutom de C-objektobjektorienterade språken tillåter de andra som Java eller PHP inte flera arv. Så en konkret klass kan förlänga en abstrakt klass, men den kan genomföra flera gränssnitt, även vid behov om det behövs. Eller från ett annat perspektiv kan en enda abstrakt klass ha många implementeringar, medan många gränssnitt kan ha många implementeringar.

För en mer fullständig förklaring av DIP, läs handledning för denna SOLID-princip.

Inverterande beroende med hjälp av ett gränssnitt

PHP stöder hela gränssnittet. Från och med Visa klass som vår modell kunde vi definiera ett gränssnitt med de offentliga metoderna alla klasser som är ansvariga för att visa data måste implementeras.

Tittar på Visas lista över metoder, det finns 12 offentliga metoder, inklusive konstruktören. Detta är ganska stort gränssnitt, du borde hålla detta nummer så lågt som möjligt, exponera gränssnitt som kunder behöver dem. Gränssnittet Segregeringsprincipen har några bra idéer om detta. Kanske kommer vi att försöka hantera detta problem i en framtida handledning.

Det vi vill uppnå nu är en arkitektur som den nedan.

På så sätt, istället för Spel beroende på mer konkreta Visa, de beror båda på det mycket abstrakta gränssnittet. Spel använder gränssnittet, medan Visa implementerar den.

Namngivande gränssnitt

Phil Karlton sa, "Det finns bara två svåra saker i datavetenskap: cache invalidation och namngivning saker."

Medan vi inte bryr oss om cacher behöver vi namnge våra klasser, variabler och metoder. Namngivning av gränssnitt kan vara en ganska utmaning.

I den gamla ungerska notationen hade vi gjort det på så sätt.

För det här diagrammet använde vi själva klass / filnamnen och den faktiska kapitaliseringen. Gränssnittet heter "IDisplay" med en huvudstad "I" framför "Display". Det fanns faktiskt programmeringsspråk som krävde en sådan namngivning för gränssnitt. Jag är säker på att det finns några läsare som fortfarande använder dem och ler just nu.

Problemet med det här namngivningssystemet är den felplacerade oroen. Gränssnitt tillhör sina kunder. Vårt gränssnitt hör till Spel. Således Spel Måste inte veta att det använder ett gränssnitt eller ett verkligt objekt. Spel får inte vara oroad över det genomförande det faktiskt får. Från Spels synvinkel, det använder bara en "Display", det är allt.

Detta löser Spel till Visa namngivningsproblem Genom att använda Impl-suffixet för genomförandet är det något bättre. Det hjälper till att eliminera oro från Spel.

Det är också mycket mer effektivt för oss. Tänk på Spel som det ser ut just nu. Det använder en Visa objekt och vet hur man använder den. Om vi ​​heter vårt gränssnitt "Display", kommer vi att minska antalet ändringar som behövs Spel.

Men fortfarande är denna namngivning bara marginellt bättre än den tidigare. Det tillåter bara ett genomförande för Visa och namnet på genomförandet kommer inte att berätta för oss vilken typ av visning vi pratar om.

Nu är det betydligt bättre. Vårt genomförande namngavs "CLIDplay", eftersom det går ut på CLI. Om vi ​​vill ha en HTML-utskrift eller ett Windows-gränssnitt, kan vi enkelt lägga till allt det i vår arkitektur.

Visa mig koden

Eftersom vi har två typer av test, den långsamma gyllene mästaren och de snabba enhetstesterna, vill vi lita på enhetstester så mycket vi kan, och på gyllene mästare så lite som möjligt. Så låt oss markera våra gyllene mästertester som hoppas över och försöka förlita oss på våra testsatser. De passerar just nu och vi vill göra en förändring som kommer att hålla dem passerar. Men hur kan vi göra något sådant, utan att göra alla de föreslagna ändringarna ovan?

Finns det ett sätt att testa det som gör det möjligt för oss att ta ett mindre steg?

Mocking sparar dagen

Det finns ett sådant sätt. Vid testning finns ett koncept som kallas "Mocking".

Wikipedia definierar Mocking som sådan: "I objektorienterad programmering är mocka föremål simulerade objekt som efterliknar verkliga föremåls beteende på kontrollerade sätt."

Ett sådant objekt skulle vara till stor hjälp för oss. Faktum är att vi inte ens behöver något så komplicerat som att simulera all beteende. Allt vi behöver är ett falskt, dumt objekt som vi kan skicka till Spel istället för den verkliga visningslogiken.

Skapa gränssnittet

Låt oss skapa ett gränssnitt som heter Visa med alla offentliga metoder i den nuvarande konkreta klassen.

Som du kan observera, den gamla Display.php döptes till DisplayOld.php. Detta är bara ett tillfälligt steg, som gör det möjligt för oss att ta det ur vägen och koncentrera oss på gränssnittet.

gränssnitt Display  

Det är allt som finns att skapa ett gränssnitt. Du kan se att den definieras som "gränssnitt" och inte som en "klass". Låt oss lägga till metoderna.

gränssnitt Display funktion statusAfterRoll ($ rolledNumber, $ currentPlayer); funktion playerSentToPenaltyBox ($ currentPlayer); funktion spelareStaysInPenaltyBox ($ currentPlayer); funktionsstatusAfterNonPenalizedPlayerMove ($ currentPlayer, $ currentPlace, $ currentCategory); funktion statusAfterPlayerGettingOutOfPenaltyBox ($ currentPlayer, $ currentPlace, $ currentCategory); funktion playerAdded ($ playerName, $ numberOfPlayers); funktion askQuestion ($ currentCategory); funktion correctAnswer (); funktionen korrektAnswerWithTypo (); funktionen felaktigAnswer (); funktion playerCoins ($ currentPlayer, $ playerCoins);  

Ja. Ett gränssnitt är bara en massa funktionsdeklarationer. Föreställ dig det som en C-headerfil. Inga implementeringar, bara deklarationer. Det kan inte innehålla ett genomförande alls. Om du försöker implementera någon av metoderna kommer det att resultera i ett fel.

Men dessa mycket abstrakta definitioner tillåter oss något underbart. Vår Spel klassen beror nu på dem, i stället för en konkret genomförande. Men om vi försöker köra våra test kommer de att misslyckas.

Dödsfall: Kan inte inställa gränssnittskärmen

Det beror på att Spel försöker skapa en ny bildskärm på egen hand på rad 25, i konstruktören.

Vi vet att vi inte kan göra det. Ett gränssnitt eller en abstrakt klass kan inte ordnas. Vi behöver ett riktigt objekt.

Beroende på injektion

Vi behöver ett dummyobjekt som ska användas i våra test. En enkel klass, genomföra alla metoder i Visa gränssnitt, men gör ingenting. Låt oss skriva det direkt i vårt test. Om ditt programmeringsspråk inte tillåter flera klasser i samma fil, var god att skapa en ny fil för din dummy-klass.

klass DummyDisplay implementerar Display function statusAfterRoll ($ rolledNumber, $ currentPlayer) // TODO: Implementera statusAfterRoll () -metoden.  funktion playerSentToPenaltyBox ($ currentPlayer) // TODO: Implementer playerSentToPenaltyBox () -metoden.  funktion playerStaysInPenaltyBox ($ currentPlayer) // TODO: Implementera PlayerStaysInPenaltyBox () -metoden.  funktion statusAfterNonPenalizedPlayerMove ($ currentPlayer, $ currentPlace, $ currentCategory) // TODO: Implementera statusAfterNonPenalizedPlayerMove () -metoden.  funktion statusAfterPlayerGettingOutOfPenaltyBox ($ currentPlayer, $ currentPlace, $ currentCategory) // TODO: Implementera statusAfterPlayerGettingOutOfPenaltyBox () -metoden.  funktion spelareAdded ($ playerName, $ numberOfPlayers) // TODO: Implement playerAdded () -metoden.  funktion askQuestion ($ currentCategory) // TODO: Implementera askQuestion () -metoden.  funktionen korrektAnswer () // TODO: Implementera correctAnswer () -metoden.  funktionen korrektAnswerWithTypo () // TODO: Implementera correctAnswerWithTypo () -metoden.  funktionen felaktigAnswer () // TODO: Implementera felaktigtAnswer () -metoden.  funktion playerCoins ($ currentPlayer, $ playerCoins) // TODO: Implement playerCoins () -metoden. 

Så snart du säger att din klass implementerar ett gränssnitt, tillåter IDE dig att automatiskt fylla i de saknade metoderna. Detta gör att sådana objekt snabbt skapas på bara några sekunder.

Låt oss nu använda den Spel genom att initiera den i sin konstruktör.

funktion __construct () $ this-> players = array (); $ this-> places = array (0); $ this-> purses = array (0); $ this-> inPenaltyBox = array (0); $ this-> display = nytt DummyDisplay (); 

Detta gör testet klar, men introducerar ett stort problem. Spel måste veta om sitt test. Vi vill verkligen inte detta. Ett test är bara en annan ingångspunkt. De DummyDisplay är bara ett annat användargränssnitt. Vår affärslogik, den Spel klass, bör inte bero på användargränssnittet. Så låt oss göra det beror bara på gränssnittet.

funktion __construct (Display $ display) $ this-> players = array (); $ this-> places = array (0); $ this-> purses = array (0); $ this-> inPenaltyBox = array (0); $ this-> display = $ display; 

Men för att testa Spel, vi måste skicka in dummy displayen från våra test.

funktion setUp () $ this-> game = new Game (nytt DummyDisplay ()); 

Det är allt. Vi behövde ändra en enda rad i våra enhetstester. I inställningen ska vi, som en parameter, skicka in en ny instans av DummyDisplay. Det är en beroendeinjektion. Användning av gränssnitts- och beroendeinsprutning hjälper särskilt om du arbetar i ett lag. Vi på Syneto observerade att en gränssnittstyp anges för en klass och injicerar den, vilket hjälper oss att kommunicera mycket bättre avsikten med klientkoden. Den som tittar på klienten vet vilken typ av objekt som används i parametrarna. Och en cool bonus är att din IDE kommer att autofullständiga metoderna för dessa parametrar eftersom det kan avgöra deras typer.

En verklig implementering för Golden Master

Det gyllene mästertestet kör vår kod som i den verkliga världen. För att klara det måste vi förvandla vår gamla bildskärmsklass till en riktig implementering av gränssnittet och skicka in den till vår affärslogik. Här är ett sätt att göra det.

klass CLIDisplay implementerar Display // ... //

Byt namn på den till CLIDisplay och få det att genomföra Visa.

funktionskörning () $ display = nytt CLIDplay (); $ aGame = nytt spel ($ display); $ AGame-> lägga ( "Chet"); $ AGame-> lägga ( "Pat"); $ AGame-> lägga ( "Sue"); gör $ tärning = rand (0, 5) + 1; $ AGame-> rulle ($ tärningar);  medan (! gjordeSomebodyWin ($ aGame, isCurrentAnswerCorrect ())); 

I RunnerFunctions.php, i springa() funktion, skapa en ny skärm för CLI och skicka den till Spel när den är skapad.

Oöverträffad och kör dina gyllene mästertester. De kommer att passera.

Slutgiltiga tankar

Denna lösning leder effektivt till en arkitektur som i diagrammet nedan.

Så nu skapar vår spelrunner, som är ingången till vår ansökan, en konkret CLIDisplay och beror på det. CLIDisplay beror endast på gränssnittet som sitter på gränsen mellan presentation och affärslogik. Vår löpare beror också direkt på affärslogiken. Så här ser vår applikation ut när vi projiceras på den rena arkitekturen som vi startade den här artikeln med.

Tack för att du läste och missa inte nästa handledning när vi kommer att prata om mocking och klassinteraktion i mer detaljer.