Refactoring Legacy Code Del 8 - Inverterende avhengigheter for en ren arkitektur

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.

Det er nå på tide å snakke om arkitektur og hvordan vi organiserer våre nylig funnet lag av kode. Det er på tide å ta søknaden vår og prøve å kartlegge den til teoretisk arkitektonisk design.

Ren arkitektur

Dette er noe vi har sett gjennom våre artikler og opplæringsprogrammer. Ren arkitektur.

På et høyt nivå ser det ut som skjemaet ovenfor, og jeg er sikker på at du allerede er kjent med den. Det er en foreslått arkitektonisk løsning av Robert C. Martin.

I sentrum av vår arkitektur er vår forretningslogikk. Dette er klassene som representerer forretningsprosessene som søknaden vår forsøker å løse. Dette er enhetene og samspillet som representerer domenet til vårt problem.

Deretter er det flere andre typer moduler eller klasser rundt vår forretningslogikk. Disse kan ses som enkle hjelpemoduler. De har ulike formål og de fleste av dem er uunnværlige. De gir sammenhengen mellom brukeren og vår søknad gjennom en leveringsmekanisme. I vårt tilfelle er dette et kommandolinjegrensesnitt. Det er et annet sett med hjelpeklasser som forbinder vår forretningslogikk med vårt vedvarende lag og alle dataene i det laget, men vi har ikke et slikt lag i vår søknad. Deretter hjelper det klasser som fabrikker og byggere som bygger og gir nye gjenstander til vår forretningslogikk. Til slutt er det klassene som representerer inngangspunktet til vårt system. I vårt tilfelle, GameRunner kan betraktes som en slik klasse, eller alle våre tester er også inngangspunkter på egen måte.

Det som er viktigst å merke seg på diagrammet, er avhengighetsretningen. Alle hjelpeklassene avhenger av forretningslogikken. Forretningslogikken er ikke avhengig av noe annet. Hvis alle objektene i vår forretningslogikk kunne vises magisk, med alle dataene i dem, og vi kunne se hva som skjer inni vår datamaskin direkte, burde de kunne fungere. Vår forretningslogikk må kunne fungere uten et brukergrensesnitt eller uten et vedvarende lag. Vår forretningslogikk må eksistere isolert, i en boble av et logisk univers.

Dependency Inversion Principle

A. Høyt nivå moduler bør ikke avhenge av lavt nivå moduler. Begge bør avhenge av abstraksjoner.
B. Abstraksjoner bør ikke avhenge av detaljer. Detaljer bør avhenge av abstraksjoner.

Dette er det, det siste SOLID-prinsippet og sannsynligvis den som har størst effekt på koden din. Det er både ganske enkelt å forstå og ganske enkelt å implementere.

Enkelt sagt står det at konkrete ting alltid skal stole på abstrakte ting. Databasen din er veldig konkret, så det bør avhenge av noe mer abstrakt. Din brukergrensesnitt er veldig konkret, så det bør avhenge av noe mer abstrakt. Fabrikkene dine er veldig konkrete igjen. Men hva med forretningslogikken din. Innenfor forretningslogikken din bør du fortsette å bruke disse ideene, slik at klassene som er nærmere grensene, er avhengig av klasser som er mer abstrakte, mer sentrale i forretningslogikken din.

En ren forretningslogikk, representerer på en abstrakt måte prosessene og atferdene til et definert domene eller forretningsmodell. En slik forretningslogikk inneholder ikke spesifikasjoner (konkrete ting) som verdier, penger, kontonavn, passord, størrelsen på en knapp eller antall felt i et skjema. Forretningslogikken bør ikke bryr seg om konkrete ting. Det bør bare bryr seg om forretningsprosessene dine.

Teknisk knep

Så sier Dependency Inversion Principle (DIP) at vi skal reversere våre avhengigheter når det er kode som avhenger av noe konkret. Akkurat nå ser vår avhengighetsstruktur ut slik.

GameRunner, bruker funksjonene i RunnerFunctions.php lager en Spill klassen og bruker den. På den annen side, vår Spill klasse, som representerer vår forretningslogikk, oppretter og bruker a Vise gjenstand.

Så avhenger løperen av vår forretningslogikk. Det er riktig. På den annen side, vår Spill kommer an på Vise, som ikke er bra. Vår forretningslogikk bør aldri avhenge av vår presentasjon.

Det enkleste tekniske trikset vi kan gjøre er å gjøre bruk av abstrakte konstruksjoner i vårt programmeringsspråk. En tradisjonell klasse er mer konkret enn en abstrakt klasse, som er mer konkret enn et grensesnitt.

en Abstrakt klasse er en spesiell type som ikke kan initialiseres. Den inneholder bare definisjoner og delvise implementeringer. En abstrakt baseklasse har vanligvis flere barneklasser. Disse klasseklassene arver den vanlige delfunksjonaliteten fra den abstrakte foreldre, de legger til sin egen utvidede oppførsel, og de må implementere alle metodene som er definert i abstrakt forelder, men ikke implementert i det.

en Interface er en spesiell type som bare tillater definisjon av metoder og variabler. Det er den mest abstrakte konstruksjonen i objektorientert programmering. Enhver implementering må alltid implementere alle metodene i parentes-grensesnittet. En konkret klasse kan implementere flere grensesnitt.

Bortsett fra de C-objektobjektive språkene, tillater de andre som Java eller PHP ikke flere arv. Så en konkret klasse kan utvide en enkelt abstrakt klasse, men den kan implementere flere grensesnitt, selv om det er nødvendig om nødvendig. Eller sett fra et annet perspektiv, kan en enkelt abstrakt klasse ha mange implementeringer, mens mange grensesnitt kan ha mange implementeringer.

For en mer fullstendig forklaring av DIP, vennligst les veiledningen dedikert til dette SOLID-prinsippet.

Inverteringsavhengighet ved hjelp av et grensesnitt

PHP støtter grensesnitt. Starter fra Vise klasse som vår modell, kunne vi definere et grensesnitt med de offentlige metodene alle klasser som er ansvarlige for å vise data må implementere.

Ser på Vises liste over metoder, det er 12 offentlige metoder, inkludert konstruktøren. Dette er ganske stort grensesnitt, du bør holde dette nummeret så lavt som mulig, utsette grensesnitt etter hvert som klientene trenger dem. Interface Segregation Principle har noen gode ideer om dette. Kanskje vi vil forsøke å håndtere dette problemet i en fremtidig opplæring.

Det vi vil nå nå er en arkitektur som den nedenfor.

Denne måten, i stedet for Spill avhengig av mer konkret Vise, de er begge avhengige av det svært abstrakte grensesnittet. Spill bruker grensesnittet, mens Vise implementerer den.

Oppgi grensesnitt

Phil Karlton sa, "Det er bare to harde ting i datalogi: cache ugyldighet og navngi ting."

Mens vi ikke bryr oss om caches, må vi nevne våre klasser, variabler og metoder. Navngi grensesnitt kan være en ganske utfordring.

I gamle dager av den ungarske notasjonen ville vi ha gjort det på denne måten.

For dette diagrammet brukte vi de faktiske klassen / filnavnene og den faktiske kapitaliseringen. Grensesnittet heter "IDisplay" med hovedstaden "I" foran "Display". Det var faktisk programmeringsspråk som krever en slik navngivning for grensesnitt. Jeg er sikker på at det er noen lesere som fortsatt bruker dem og smiler akkurat nå.

Problemet med denne navngivningssystemet er feilplassert bekymring. Grensesnitt tilhører sine kunder. Grensesnittet vårt tilhører Spill. Og dermed Spill må ikke vite at det bruker et grensesnitt eller et ekte objekt. Spill må ikke være bekymret for implementeringen den faktisk får. Fra Spills synspunkt, det bruker bare en "skjerm", det er alt.

Dette løser Spill til Vise navngi problem. Bruke Impl-suffikset for implementeringen er noe bedre. Det hjelper å eliminere bekymringen fra Spill.

Det er også mye mer effektivt for oss. Tenker på Spill som det ser ut akkurat nå. Den bruker en Vise objekt og vet hvordan du bruker den. Hvis vi heter grensesnittet "Skjerm", vil vi redusere antall endringer som trengs Spill.

Men fortsatt er denne navngivelsen bare marginalt bedre enn den forrige. Det tillater bare en implementering for Vise og navnet på implementeringen vil ikke fortelle oss hvilken type skjerm vi snakker om.

Nå er det betydelig bedre. Implementeringen vår ble kalt "CLIDisplay", da den kommer ut til CLI. Hvis vi vil ha en HTML-utgang eller en Windows-desktop-brukergrensesnitt, kan vi enkelt legge til alt det som er i vår arkitektur.

Vis meg koden

Da vi har to typer tester, den langsomme gylne mesteren og de raske enhetstestene, vil vi stole på enhetstester så mye som mulig, og på gylden mester så lite som mulig. Så la oss merke våre gylne mestertester som hoppet over og prøv å stole på enhetstestene våre. De passerer akkurat nå, og vi ønsker å gjøre en endring som vil holde dem passerer. Men hvordan kan vi gjøre noe slikt, uten å gjøre alle de foreslåtte endringene?

Er det en måte å teste på som vil gjøre det mulig for oss å ta et mindre skritt?

Mocking sparer dagen

Det er en slik måte. I testing er det et konsept kalt "Mocking".

Wikipedia definerer Mocking som sådan, "I objektorientert programmering er mockeobjekter simulerte gjenstander som etterligner oppførelsen av ekte objekter på kontrollerte måter."

Et slikt objekt ville være til stor hjelp for oss. Faktisk trenger vi ikke engang noe så komplisert som å simulere all oppførsel. Alt vi trenger er et falskt, dumt objekt som vi kan sende til Spill i stedet for den virkelige visningslogikken.

Opprette grensesnittet

La oss lage et grensesnitt som heter Vise med alle offentlige metoder i dagens konkrete klasse.

Som du kan observere, den gamle Display.php ble omdøpt til DisplayOld.php. Dette er bare et midlertidig trinn, som gjør det mulig for oss å ta det ut av veien og konsentrere oss om grensesnittet.

grensesnitt display  

Det er alt der er å skape et grensesnitt. Du kan se at den er definert som "grensesnitt" og ikke som en "klasse". La oss legge til metodene.

grensesnitt Display function statusAfterRoll ($ rolledNumber, $ currentPlayer); funksjon playerSentToPenaltyBox ($ currentPlayer); funksjons playerStaysInPenaltyBox ($ currentPlayer); funksjonsstatusAfterNonPenalizedPlayerMove ($ currentPlayer, $ currentPlace, $ currentCategory); funksjon statusAfterPlayerGettingOutOfPenaltyBox ($ currentPlayer, $ currentPlace, $ currentCategory); funksjonsspillerAdded ($ playerName, $ numberOfPlayers); funksjon askQuestion ($ currentCategory); funksjon correctAnswer (); funksjonen korrektAnswerWithTypo (); funksjonen feilaktigAnswer (); funksjon playerCoins ($ currentPlayer, $ playerCoins);  

Ja. Et grensesnitt er bare en rekke funksjonsdeklarasjoner. Tenk det som en C-headerfil. Ingen implementeringer, bare erklæringer. Det kan ikke ha en gjennomføring i det hele tatt. Hvis du prøver å implementere noen av metodene, vil det føre til en feil.

Men disse svært abstrakte definisjonene gir oss noe fantastisk. Våre Spill klassen avhenger nå av dem, i stedet for en konkret gjennomføring. Men hvis vi prøver å kjøre testene våre, vil de mislykkes.

Fatal feil: Kan ikke instantiere grensesnitt Skjerm

Det er fordi Spill prøver å lage en ny skjerm på egen hånd på linje 25, i konstruktøren.

Vi vet at vi ikke kan gjøre det. Et grensesnitt eller en abstrakt klasse kan ikke opprettes. Vi trenger et reelt objekt.

Dependency Injection

Vi trenger et dummyobjekt som skal brukes i våre tester. En enkel klasse, implementering av alle metodene til Vise grensesnitt, men gjør ingenting. La oss skrive det direkte i vår enhetstest. Hvis programmeringsspråket ditt ikke tillater flere klasser i samme fil, kan du gjerne opprette en ny fil for din dummy-klasse.

klasse DummyDisplay implementerer Display function statusAfterRoll ($ rolledNumber, $ currentPlayer) // TODO: Implementer statusAfterRoll () -metoden.  funksjons playerSentToPenaltyBox ($ currentPlayer) // TODO: Implementer playerSentToPenaltyBox () -metoden.  funksjons playerStaysInPenaltyBox ($ currentPlayer) // TODO: Implementer playerStaysInPenaltyBox () -metoden.  funksjonsstatusAfterNonPenalizedPlayerMove ($ currentPlayer, $ currentPlace, $ currentCategory) // TODO: Implementer statusAfterNonPenalizedPlayerMove () -metoden.  funksjon statusAfterPlayerGettingOutOfPenaltyBox ($ currentPlayer, $ currentPlace, $ currentCategory) // TODO: Implementer statusAfterPlayerGettingOutOfPenaltyBox () -metoden.  funksjonsspillerAdded ($ playerName, $ numberOfPlayers) // TODO: Implement playerAdded () -metoden.  funksjon askQuestion ($ currentCategory) // TODO: Implement askQuestion () metode.  funksjon correctAnswer () // TODO: Implementer correctAnswer () -metoden.  funksjon correctAnswerWithTypo () // TODO: Implementer correctAnswerWithTypo () -metoden.  Funksjonen er feilaktigAnswer () // TODO: Implementer feilaktigAnswer () -metoden.  function playerCoins ($ currentPlayer, $ playerCoins) // TODO: Implement playerCoins () -metoden. 

Så snart du sier at klassen din implementerer et grensesnitt, vil IDE tillate deg å fylle ut de manglende metodene automatisk. Dette gjør det mulig å lage slike objekter veldig raskt, på få sekunder.

La oss nå bruke det Spill ved å initialisere den i sin konstruktør.

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

Dette gjør testen forbi, men introduserer et stort problem. Spill må vite om testen. Vi vil virkelig ikke ha dette. En test er bare et annet inngangspunkt. De DummyDisplay er bare et annet brukergrensesnitt. Vår forretningslogikk, den Spill klassen, bør ikke avhenge av brukergrensesnittet. Så la oss få det avhengig bare av grensesnittet.

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

Men for å teste Spill, Vi må sende inn dummy-skjermen fra testene våre.

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

Det er det. Vi trengte å endre en enkelt linje i enhetstestene våre. I oppsettet skal vi som en parameter sende inn en ny forekomst av DummyDisplay. Det er en avhengighetsinjeksjon. Bruk av grensesnitt og avhengighetsinjeksjon hjelper spesielt hvis du jobber i et lag. Vi på Syneto observert at du spesifiserer en grensesnitttype for en klasse og injiserer den, vil hjelpe oss med å kommunisere mye bedre hensikten med klientkoden. Alle som ser på klienten, vet hvilken type objekt som brukes i parameterne. Og en kul bonus er at din IDE vil autofullfør metodene for disse parameterne fordi den kan bestemme sine typer.

En virkelig implementering for Golden Master

Den gyldne mestertesten, kjører vår kode som i den virkelige verden. For å få det til å passere, må vi forvandle vår gamle skjermklasse til en virkelig implementering av grensesnittet og sende den inn i vår forretningslogikk. Her er en måte å gjøre det på.

klasse CLIDisplay implementerer Display // ... //

Gi nytt navn til CLIDisplay og få det til å gjennomføre Vise.

funksjonsdrift () $ display = nytt CLIDisplay (); $ aGame = nytt spill ($ display); $ AGame-> legge til ( "Chet"); $ AGame-> legge til ( "Pat"); $ AGame-> legge til ( "Sue"); gjør $ terning = rand (0, 5) + 1; $ AGame-> roll ($ terninger);  mens (! gjordeSomebodyWin ($ aGame, isCurrentAnswerCorrect ())); 

I RunnerFunctions.php, i løpe() funksjon, opprett en ny skjerm for CLI og send den til Spill når den er opprettet.

Uncomment og kjøre dine gylne mestertester. De vil passere.

Siste tanker

Denne løsningen fører effektivt til en arkitektur som i diagrammet nedenfor.

Så nå skaper vår spillløper, som er inngangspunktet for vår søknad, en konkret CLIDisplay og dermed avhenger av det. CLIDisplay Avhenger bare av grensesnittet som sitter på grensen mellom presentasjon og forretningslogikk. Vår løperen avhenger også av forretningslogikken. Slik ser vår søknad ut når vi projiseres på den rene arkitekturen som vi startet denne artikkelen med.

Takk for at du leser, og ikke gå glipp av neste opplæring når vi skal snakke om mocking og klassens interaksjon i flere detaljer.