SOLID Del 4 - The Dependency Inversion Principle

Den enkelte ansvar (SRP), Open / Closed (OCP), Liskov Substitution, Interface Segregation, og Dependency Inversion. Fem smidige prinsipper som skal veilede deg hver gang du skriver kode.

Det ville være urettferdig å fortelle deg at et av SOLID-prinsippene er viktigere enn et annet. Imidlertid har sannsynligvis ingen av de andre en så umiddelbar og dyp effekt på koden din enn Dependency Inversion Principle, eller DIP kort sagt. Hvis du finner de andre prinsippene som er vanskelige å forstå eller anvende, start med denne og bruk resten på kode som allerede respekterer DIP.

Definisjon

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 prinsippet ble definert av Robert C. Martin i sin bok Agile Software Development, Principles, Patterns, and Practices og senere publisert i C # versjonen av boka Agile Principles, Patterns, and Practices i C #, og det er den siste av de fem SOLID agile prinsipper.

DIP i den virkelige verden

Før vi begynner koding, vil jeg gjerne fortell deg en historie. På Syneto var vi ikke alltid så forsiktig med vår kode. For noen år siden visste vi mindre og selv om vi prøvde å gjøre vårt beste, var ikke alle våre prosjekter så hyggelige. Vi gikk gjennom helvete og tilbake igjen, og vi lærte mange ting ved prøving og feiling.

SOLID-prinsippene og rene arkitekturprinsipper for onkel Bob (Robert C. Martin) ble en spilleskifter for oss og forvandlet vår måte å kodere på måter som er vanskelig å beskrive. Jeg vil prøve å eksemplifisere, i et nøtteskall, noen få viktige arkitektoniske beslutninger pålagt av DIP som hadde stor innvirkning på våre prosjekter.

De fleste webprosjekter inneholder tre hovedteknologier: HTML, PHP og SQL. Den spesielle versjonen av disse programmene vi snakker om, eller hvilken type SQL-implementeringer du bruker, er irrelevant. Saken er at informasjonen fra et HTML-skjema må ende opp på en eller annen måte i databasen. Limet mellom de to kan leveres med PHP.

Det som er viktig å ta bort fra dette, er det hvor fint de tre teknologiene representerer tre forskjellige arkitektoniske lag: brukergrensesnitt, forretningslogikk og utholdenhet. Vi vil snakke om implikasjonene av disse lagene om et minutt. For nå, la oss fokusere på noen merkelige, men ofte oppdaget løsninger for å få teknologiene til å fungere sammen.

Mange ganger har jeg sett prosjekter som brukte SQL-kode i en PHP-tag i en HTML-fil, eller PHP-kode ekko sider og sider med HTML og tolke direkte $ _GET eller $ _POST globale variabler. Men hvorfor er dette dårlig?


Bildene ovenfor representerer en rå versjon av det vi beskrev i forrige avsnitt. Pilene representerer ulike avhengigheter, og som vi kan konkludere, er alt i alt alt avhengig av alt. Hvis vi trenger å endre en database tabell, kan vi ende opp med å redigere en HTML-fil. Eller hvis vi endrer et felt i HTML, kan det hende vi endrer navnet på en kolonne i en SQL-setning. Eller hvis vi ser på det andre skjemaet, kan vi veldig godt trenge å endre PHP hvis HTML endres, eller i svært dårlig tilfeller når vi genererer alt HTML-innhold fra en PHP-fil, må vi sikkert endre en PHP-fil til endre HTML-innhold. Så det er ingen tvil om at avhengighetene er zigzagging mellom klassene og modulene. Men det slutter ikke her. Du kan lagre prosedyrer; PHP-kode i SQL-tabeller.


I skjemaet ovenfor returnerer spørringer til SQL-databasen PHP-kode generert med data fra tabellene. Disse PHP-funksjonene eller -klassene gjør andre SQL-spørringer som returnerer forskjellig PHP-kode, og syklusen fortsetter til endelig all informasjon er oppnådd og returnert ... sannsynligvis til brukergrensesnittet.

Jeg vet at dette kan høres opprørende mot mange av dere, men hvis du ikke har jobbet med et prosjekt oppfunnet og implementert på denne måten, vil du sikkert i din fremtidige karriere. De fleste eksisterende prosjekter, uavhengig av de programmerte språkene, ble skrevet med gamle prinsipper i tankene, av programmerere som ikke bryr seg eller vet nok til å gjøre det bedre. Hvis du leser disse veiledningene, er du mest sannsynlig et nivå høyere enn det. Du er klar, eller gjør deg klar til å respektere ditt yrke, å omfavne håndverket, og å gjøre det bedre.

Det andre alternativet er å gjenta feilene dine forgjengere har gjort og leve med konsekvensene. På Syneto, etter at en av våre prosjekter nådde en nesten uoppnåelig tilstand på grunn av sin gamle og kryssavhengige arkitektur, og vi måtte basere oss for alltid, bestemte vi oss for å aldri gå nedover den veien igjen. Siden da har vi tenkt å ha en ren arkitektur som korrekt respekterer SOLID-prinsippene, og viktigst avhengig av prinsippet om avhengighetsinversjon.


Det som er så utrolig med denne arkitekturen er hvordan avhengighetene peker:

  • Brukergrensesnittet (i de fleste tilfeller et web-MVC-rammeverk) eller hvilken annen leveringsmekanisme det er for prosjektet ditt vil avhenge av forretningslogikken. Forretningslogikk er ganske abstrakt. Et brukergrensesnitt er veldig konkret. UI er bare en detalj for prosjektet, og det er også veldig volatilt. Ingenting bør avhenge av brukergrensesnittet, ingenting bør avhenge av MVC-rammen.
  • Den andre interessante observasjonen vi kan lage er at utholdenheten, databasen, MySQL eller PostgreSQL, avhenger av forretningslogikken. Din forretningslogikk er databasen agnostisk. Dette tillater utveksling av utholdenhet som du ønsker. Hvis du i morgen vil endre MySQL med PostgreSQL eller bare enkle tekstfiler, kan du gjøre det. Du må selvfølgelig implementere et bestemt utholdenhetslag for den nye utholdenhetsmetoden, men du trenger ikke å endre en enkelt linje med kode i forretningslogikken. Det er en mer detaljert forklaring på persistensemnet i Evolving Toward a Persistence Layer opplæringen.
  • Til slutt, til høyre for forretningslogikken, utenfor det, har vi alle klassene som skaper forretningslogikklasser. Dette er fabrikker og klasser laget av inngangspunktet til vår søknad. Mange mennesker har en tendens til å tro at disse tilhører forretningslogikken, men mens de lager forretningsmessige objekter, er deres eneste grunn til å gjøre dette. De er klasser bare for å hjelpe oss med å lage andre klasser. Forretningsobjektene og logikken de gir er uavhengige av disse fabrikkene. Vi kunne bruke forskjellige mønstre, for eksempel Simple Factory, Abstract Factory, Builder eller ren objektopprettelse for å gi forretningslogikken. Det spiller ingen rolle. Når virksomhetsobjektene er opprettet, kan de gjøre jobben sin.

Vis meg koden

Bruk av Dependency Inversion Principle (DIP) på arkitektonisk nivå er ganske enkelt hvis du respekterer de klassiske, smidige designmønstrene. Å trene og eksemplifisere det i forretningslogikken er ganske enkelt også og kan til og med være morsomt. Vi vil forestille oss et e-bokleserprogram.

klasse Test utvider PHPUnit_Framework_TestCase funksjon testItCanReadAPDFBook () $ b = ny PDFBook (); $ r = ny PDFReader ($ b); $ this-> assertRegExp ('/ pdf book /', $ r-> read ());  klasse PDFReader privat $ bok; funksjon __construct (PDFBook $ bok) $ this-> book = $ book;  funksjonslese () return $ this-> book-> read ();  klasse PDFBook funksjon lese () return "leser en pdf bok."; 

Vi begynner å utvikle vår e-leser som PDF-leser. Så langt så bra. Vi har en PDFReader klasse ved hjelp av a PDFBook. De lese() fungere på leserens delegater til bokens lese() metode. Vi bekrefter dette ved å foreta en regex-kontroll etter en nøkkeldel av strengen returnert av PDFBook's leser() metode.

Vær oppmerksom på at dette bare er et eksempel. Vi vil ikke implementere leselogikken for PDF-filer eller andre filformater. Derfor vil våre tester bare bare sjekke etter noen grunnleggende strenger. Hvis vi skulle skrive den virkelige applikasjonen, ville den eneste forskjellen være hvordan vi tester de forskjellige filformatene. Avhengighetsstrukturen vil være veldig lik vår eksempel.


Å ha en PDF-leser som bruker en PDF-bok, kan være en lydløsning for et begrenset program. Hvis vårt omfang var å skrive en PDF-leser og ingenting mer, ville det faktisk være en akseptabel løsning. Men vi vil skrive en generisk e-bokleser, som støtter flere formater, blant annet vår første implementerte versjon PDF. La oss omdøpe vår leserklasse.

klasse Test utvider PHPUnit_Framework_TestCase funksjon testItCanReadAPDFBook () $ b = ny PDFBook (); $ r = ny EBookReader ($ b); $ this-> assertRegExp ('/ pdf book /', $ r-> read ());  klasse EBookReader privat $ bok; funksjon __construct (PDFBook $ bok) $ this-> book = $ book;  funksjonslese () return $ this-> book-> read ();  klasse PDFBook funksjon lese () return "leser en pdf bok."; 

Omdøping hadde ingen funksjonelle tellereffekter. Testen går fortsatt forbi.

Testingen startet klokken 13:04 ...
PHPUnit 3.7.28 av Sebastian Bergmann.
Tid: 13 ms, Minne: 2,50Mb
OK (1 test, 1 påstand)
Prosess ferdig med utgangskode 0

Men det har en seriøs design effekt.


Leseren ble mye mer abstrakt. Mye mer generelt. Vi har en generisk E-bok leser som bruker en veldig spesifikk boktype, PDFBook. En abstraksjon avhenger av en detalj. At boken er av typen PDF, bør bare være en detalj, og ingen bør avhenge av den.

klasse Test utvider PHPUnit_Framework_TestCase funksjon testItCanReadAPDFBook () $ b = ny PDFBook (); $ r = ny EBookReader ($ b); $ this-> assertRegExp ('/ pdf book /', $ r-> read ());  grensesnittet EBook function read ();  klasse EBookReader privat $ bok; funksjon __construct (EBook $ bok) $ this-> book = $ book;  funksjonslese () return $ this-> book-> read ();  klasse PDFBook implementerer EBook funksjonslest () return leser en pdf-bok. "; 

Den vanligste og mest brukte løsningen for å reversere avhengigheten er å introdusere en mer abstrakt modul i vårt design. "Det mest abstrakte elementet i OOP er et grensesnitt. Således kan en hvilken som helst annen klasse avhenge av et grensesnitt og fortsatt respektere DIP".

Vi opprettet et grensesnitt for leseren vår. Grensesnittet kalles EBook og representerer behovene til E-bok leser. Dette er et direkte resultat av å respektere Interface Segregation Principle (ISP) som fremmer ideen om at grensesnitt skal gjenspeile kundens behov. Grensesnitt tilhører klientene, og dermed blir de navngitt for hvilke typer og gjenstander kundene trenger, og de vil inneholde metoder kundene ønsker å bruke. Det er bare naturlig for en E-bok leser å bruke ebøker og ha en lese() metode.


I stedet for en enkelt avhengighet har vi to avhengigheter nå.

  • De første avhengighetspunktene fra E-bok leser mot EBook grensesnitt og det er av type bruk. E-bok leser bruker ebøker.
  • Den andre avhengigheten er forskjellig. Det peker fra PDFBook mot det samme EBook grensesnitt, men det er av type implementering. EN PDFBook er bare en bestemt form for EBook, og implementerer dermed det grensesnittet for å tilfredsstille kundens behov.

Uten overraskende lar denne løsningen oss også plugge inn forskjellige typer e-bøker i leseren vår. Den eneste betingelsen for alle disse bøkene er å tilfredsstille EBook grensesnitt og implementere det.

klasse Test utvider PHPUnit_Framework_TestCase funksjon testItCanReadAPDFBook () $ b = ny PDFBook (); $ r = ny EBookReader ($ b); $ this-> assertRegExp ('/ pdf book /', $ r-> read ());  funksjon testItCanReadAMobiBook () $ b = ny MobiBook (); $ r = ny EBookReader ($ b); $ this-> assertRegExp ('/ mobi book /', $ r-> read ());  grensesnittet EBook function read ();  klasse EBookReader privat $ bok; funksjon __construct (EBook $ bok) $ this-> book = $ book;  funksjonslese () return $ this-> book-> read ();  klasse PDFBook implementerer EBook funksjonslest () return leser en pdf-bok. ";  klasse MobiBook implementerer EBook function read () return leser en mobi bok. "; 

Som igjen fører oss til Åpen / lukket prinsipp, og sirkelen er stengt.

Dependency Inversion Principle er en som leder eller hjelper oss å respektere alle de andre prinsippene. Respektere DIP vil:

  • Nesten tvinge deg til å respektere OCP.
  • Tillat deg å skille ansvar.
  • Gjør deg riktig bruk av undertyper.
  • Tilbyr deg muligheten til å segregere grensesnittene dine.

Siste tanker

Det er det. Vi er ferdige. Alle opplæringsprogrammer om SOLID-prinsippene er komplette. For meg personlig, å oppdage disse prinsippene og gjennomføre prosjekter med dem i tankene var en stor forandring. Jeg har helt forandret måten jeg tenker på design og arkitektur, og jeg kan si siden da er alle prosjektene jeg jobber med eksponentielt lettere å administrere og forstå.

Jeg anser SOLID-prinsippene som et av de mest essensielle begrepene til objektorientert design. Disse konseptene som må veilede oss i å gjøre vår kode bedre og vårt liv som programmerere mye lettere. God utformet kode er lettere for programmerere å forstå. Datamaskiner er smarte, de kan forstå kode uansett kompleksitet. Menneskene har derimot et begrenset antall ting de kan beholde i sitt aktive, fokuserte sinn. Nærmere bestemt er antallet slike ting The Magical Number Seven, Plus eller Minus Two.

Vi bør streve for å få vår kode strukturert rundt disse tallene, og det finnes flere teknikker som hjelper oss med å gjøre det. Funksjoner med maksimalt fire linjer i lengden (fem med definisjonslinjen inkludert) slik at de alle kan passe på en gang i vårt sinn. Indrykk som ikke passerer fem nivåer dypt. Klasser med ikke mer enn ni metoder. Design mønstre som vanligvis bruker en rekke fem til ni klasser. Vårt høyt nivå design i skjemaene ovenfor bruker fire til fem konsepter. Det er fem SOLID-prinsipper, hver som krever fem til ni del-konsepter / moduler / klasser som eksemplifiseres. Den ideelle størrelsen på et programmeringslag er mellom fem og ni. Det ideelle antall lag i et selskap er mellom fem og ni.

Som du kan se, er det magiske tallet sju, pluss eller minus to rundt oss, så hvorfor skulle koden din være annerledes?