Enkelt ansvar (SRP), Open / Closed (OCP), Liskovs Substitution, Interface Segregation, og Dependency Inversion. Fem smidige prinsipper som skal veilede deg hver gang du trenger å skrive kode.
Programvareenheter (klasser, moduler, funksjoner, etc.) skal være åpne for utvidelse, men stengt for endring.
Det åpne / lukkede prinsippet, OCP kort sagt, krediteres Bertrand Mayer, en fransk programmerer, som først publiserte den i sin bok n Object Oriented Software Construction i 1988.
Prinsippet steg i popularitet tidlig på 2000-tallet da det ble et av SOLID-prinsippene definert av Robert C. Martin i sin bok Agile Software Development, Principles, Patterns, and Practices og senere publisert i C # versjonen av boken Agile Principles, Patterns , og praksis i C #.
Det vi snakker om her, er å designe våre moduler, klasser og funksjoner på en måte at når en ny funksjonalitet er nødvendig, bør vi ikke endre vår eksisterende kode, men heller skrive ny kode som vil bli brukt av eksisterende kode. Dette høres litt rart ut, spesielt hvis vi jobber på språk som Java, C, C ++ eller C # der det ikke bare gjelder kildekoden selv, men også det binære. Vi ønsker å skape nye funksjoner på måter som ikke krever at vi omfordeler eksisterende binære filer, kjørbare filer eller DLLer.
Når vi går videre med disse veiledningene, kan vi sette hvert nytt prinsipp i sammenheng med de allerede diskuterte. Vi har allerede diskutert Single Responsibility (SRP) som uttalt at en modul bare har én grunn til å endre. Hvis vi tenker på OCP og SRP, kan vi observere at de er komplementære. Kode spesifikt utformet med SRP i tankene vil være nær OCP-prinsipper eller enkelt å gjøre det respektere disse prinsippene. Når vi har kode som har en enkelt grunn til å forandre, vil innføring av en ny funksjon skape en sekundær grunn til den forandringen. Så både SRP og OCP vil bli brutt. På samme måte, hvis vi har kode som bare skal endres når hovedfunksjonen endres og bør forbli uendret når en ny funksjon legges til den, og dermed respekterer OCP, vil det meste respektere SRP også.
Dette betyr ikke at SRP alltid fører til OCP eller omvendt, men i de fleste tilfeller hvis en av dem respekteres, er det ganske enkelt å oppnå den andre..
Fra et rent teknisk synspunkt er Open / Closed-prinsippet veldig enkelt. Et enkelt forhold mellom to klasser, som den som er under, bryter med OCP.
De Bruker
klassen bruker Logikk
klasse direkte. Hvis vi trenger å implementere et sekund Logikk
klassen på en måte som gjør at vi kan bruke både den nåværende og den nye, den eksisterende Logikk
klassen må endres. Bruker
er direkte knyttet til gjennomføringen av Logikk
, det er ingen måte for oss å gi en ny Logikk
uten å påvirke den nåværende. Og når vi snakker om statisk typede språk, er det veldig mulig at Bruker
klassen vil også kreve endringer. Hvis vi snakker om kompilerte språk, absolutt begge Bruker
kjørbar og Logikk
kjørbar eller dynamisk bibliotek vil kreve rekompilering og omplassering til våre kunder, en prosess vi vil unngå når det er mulig.
Basert bare på skjemaet ovenfor kan man utlede at en hvilken som helst klasse som direkte bruker en annen klasse, faktisk ville krenke Open / Closed Principle. Og det er riktig, strengt tatt. Jeg fant det ganske interessant å finne grensene, øyeblikket når du tegner linjen og bestemmer at det er vanskeligere å respektere OCP enn å endre eksisterende kode, eller at arkitektoniske kostnader ikke rettferdiggjør kostnadene ved å endre eksisterende kode.
La oss si at vi ønsker å skrive en klasse som kan gi fremgang som prosent for en fil som lastes ned gjennom vår søknad. Vi vil ha to hovedklasser, a Framgang
og a Fil
, og jeg antar at vi vil bruke dem som i testen nedenfor.
funksjon testItCanGetTheProgressOfAFileAsAPercent () $ file = new File (); $ fil-> lengde = 200; $ fil-> sendt = 100; $ progress = ny fremgang ($ fil); $ this-> assertEquals (50, $ progress-> getAsPercent ());
I denne testen er vi en bruker av Framgang
. Vi ønsker å oppnå en verdi som prosent, uavhengig av den faktiske filstørrelsen. Vi bruker Fil
som kilde til informasjon for vår Framgang
. En fil har en lengde i byte og et felt som heter sendt
som representerer mengden data som sendes til den som gjør nedlastingen. Vi bryr oss ikke om hvordan disse verdiene oppdateres i søknaden. Vi kan anta at det er noe magisk logikk som gjør det for oss, så i en test kan vi sette dem eksplisitt.
klassefil offentlig $ lengde; offentlig $ sendt;
De Fil
klassen er bare en enkel dataobjekt som inneholder de to feltene. Selvfølgelig i det virkelige liv, vil det trolig inneholde annen informasjon og atferd også, som filnavn, sti, relativ sti, nåværende katalog, type, tillatelser og så videre.
klasse fremgang private $ file; funksjon __construct (Fil $ fil) $ this-> file = $ file; funksjon getAsPercent () return $ this-> file-> sendt * 100 / $ this-> file-> lengde;
Framgang
er bare en klasse tar en Fil
i sin konstruktør. For klarhet spesifiserte vi typen av variabelen i konstruktørens parametere. Det er en enkelt nyttig metode på Framgang
, getAsPercent ()
, som vil ta verdiene sendt og lengden fra Fil
og forvandle dem til en prosent. Enkel, og det fungerer.
Testing startet klokka 17:39 ... PHPUnit 3.7.28 av Sebastian Bergmann ... Tid: 15 ms, Minne: 2,50Mb OK (1 test, 1 påstand)
Denne koden ser ut til å være riktig, men den bryter med Open / Closed Principle. Men hvorfor? Og hvordan?
Hvert program som forventes å utvikle seg i tide, vil trenge nye funksjoner. En ny funksjon for applikasjonen vår kan være å tillate streaming av musikk, i stedet for bare å laste ned filer. Fil
lengden er representert i byte, musikkens varighet i sekunder. Vi ønsker å tilby en god fremdriftslinje til våre lyttere, men kan vi gjenbruke den vi allerede har?
Nei vi kan ikke. Vår fremgang er bundet til Fil
. Det forstår bare filer, selv om det også kan brukes på musikkinnhold. Men for å gjøre det må vi endre det, vi må gjøre Framgang
vite om Musikk
og Fil
. Hvis vårt design vil respektere OCP, trenger vi ikke å røre Fil
eller Framgang
. Vi kunne bare bare gjenbruke eksisterende Framgang
og bruk den til Musikk
.
Dynamisk skrivte språk har fordelene ved å gjette typen objekter på kjøretid. Dette tillater oss å fjerne typehinten fra Framgang
'Konstruktør og koden vil fortsatt fungere.
klasse fremgang private $ file; funksjon __construct ($ file) $ this-> file = $ file; funksjon getAsPercent () return $ this-> file-> sendt * 100 / $ this-> file-> lengde;
Nå kan vi kaste noe på Framgang
. Og med noe mener jeg bokstavelig talt noe:
klassemusikk offentlig $ lengde; offentlig $ sendt; offentlig $ artist; offentlig $ album; offentlig $ releaseDate; funksjon getAlbumCoverFile () return 'Images / Covers /'. $ this-> artist. '/'. $ dette-> albumet. 'PNG';
Og a Musikk
Klassen som den som er over, fungerer fint. Vi kan teste det enkelt med en veldig lignende test til Fil
.
funksjon testItCanGetTheProgressOfAMusicStreamAsAPercent () $ music = new Music (); $ musikk-> lengde = 200; $ musikk-> sendt = 100; $ fremgang = ny fremgang ($ musikk); $ this-> assertEquals (50, $ progress-> getAsPercent ());
Så i utgangspunktet kan noe målbart innhold brukes med Framgang
klasse. Kanskje vi burde uttrykke dette i kode ved å endre variabelenes navn også:
klasse fremgang private $ measurableContent; funksjon __construct ($ measurableContent) $ this-> measurableContent = $ measurableContent; funksjon getAsPercent () return $ this-> measurableContent-> sendt * 100 / $ this-> measurableContent-> lengde;
Bra, men vi har et stort problem med denne tilnærmingen. Da vi hadde Fil
Spesifisert som en typehint, var vi positive om hva vår klasse kan håndtere. Det var eksplisitt, og hvis noe annet kom inn, fortalte en fin feil oss det.
Argument 1 bestått til Progress :: __ construct () må være en forekomst av fil, forekomst av musikk gitt.
Men uten typehint, må vi stole på at det som kommer inn, vil ha to offentlige variabler av noen eksakte navn som "lengde
"og"sendt
"Ellers vil vi ha en nektet erobring.
Nektet erverv: en klasse som overstyrer en metode for en grunnklasse på en slik måte at kontrakten til grunnklassen ikke blir æret av den avledede klassen. ~ Kilde Wikipedia.
Dette er en av kode lukter presentert i mye mer detalj i Detecting Code Smells Premium kurset. Kort sagt, vi ønsker ikke å ende opp med å prøve å ringe metoder eller tilgangsfelt på objekter som ikke samsvarer med kontrakten vår. Når vi hadde en typehint, ble kontrakten spesifisert av den. Feltene og metodene til Fil
klasse. Nå som vi ikke har noe, kan vi sende inn alt, enda en streng, og det ville føre til en stygg feil.
funksjon testItFailsWithAParameterThatDoesNotRespectTheImplicitContract () $ progress = new Progress ('some string'); $ this-> assertEquals (50, $ progress-> getAsPercent ());
En test som dette, der vi sender inn en enkel streng, vil produsere en nektet okkupasjon:
Prøver å få eiendom av ikke-objekt.
Mens sluttresultatet er det samme i begge tilfeller, som betyr at koden bryter, har den første laget en fin melding. Denne er imidlertid svært uklar. Det er ingen måte å vite hva variabelen er - en streng i vårt tilfelle - og hvilke egenskaper ble etterspurt og ikke funnet. Det er vanskelig å feilsøke og løse problemet. En programmerer må åpne Framgang
klassen og les den og forstå den. Kontrakten, i dette tilfellet, når vi ikke spesifiserer typetypen eksplisitt, er definert av oppførselen til Framgang
. Det er en implisitt kontrakt, kun kjent for Framgang
. I vårt eksempel er det definert av tilgangen til de to feltene, sendt
og lengde
, i getAsPercent ()
metode. I virkeligheten kan den implisitte kontrakten være svært kompleks og vanskelig å oppdage ved bare å se etter noen få sekunder på klassen.
Denne løsningen anbefales kun hvis ingen av de andre forslagene nedenfor lett kan implementeres, eller hvis de ville føre til alvorlige arkitektoniske forandringer som ikke rettferdiggjør innsatsen.
Dette er den vanligste og sannsynligvis den mest hensiktsmessige løsningen for å respektere OCP. Det er enkelt og effektivt.
Strategimønsteret introduserer bare bruken av et grensesnitt. Et grensesnitt er en spesiell type enhet i Objektorientert Programmering (OOP) som definerer en kontrakt mellom en klient og en serverklasse. Begge klassene vil følge kontrakten for å sikre forventet oppførsel. Det kan være flere, ikke-relaterte, serverklasser som respekterer den samme kontrakten og dermed er i stand til å betjene samme klientklasse.
grensesnitt Målbar funksjon getLength (); funksjon getSent ();
I et grensesnitt kan vi definere bare oppførsel. Det er derfor i stedet for direkte å bruke offentlige variabler, må vi tenke på å bruke getters og settere. Å tilpasse de andre klassene vil ikke være vanskelig på dette punktet. Vår IDE kan gjøre det meste av jobben.
funksjon testItCanGetTheProgressOfAFileAsAPercent () $ file = new File (); $ Fil-> setLength (200); $ Fil-> setSent (100); $ progress = ny fremgang ($ fil); $ this-> assertEquals (50, $ progress-> getAsPercent ());
Som vanlig starter vi med våre tester. Vi må bruke setters til å sette verdiene. Hvis det anses obligatorisk, kan disse setterne også defineres i målbar
grensesnitt. Vær imidlertid forsiktig med hva du setter der. Grensesnittet er å definere kontrakten mellom klientklassen Framgang
og de forskjellige serverklassene som Fil
og Musikk
. Gjør Framgang
må du sette verdiene? Sannsynligvis ikke. Så setterne er svært lite sannsynlig å være behov for å bli definert i grensesnittet. Også, hvis du skulle definere settere der, ville du tvinge alle serverklassene til å implementere settere. For noen av dem kan det være logisk å ha settere, men andre kan oppføre seg helt annerledes. Hva om vi vil bruke vår Framgang
klasse for å vise temperaturen på ovnen vår? De OvenTemperature
klassen kan initialiseres med verdiene i konstruktøren, eller få informasjonen fra en tredje klasse. Hvem vet? Å ha settere på den klassen ville være rart.
klasse Filredskaper Målbar privat $ lengde; privat $ sendt; offentlig $ filnavn; offentlig $ eier funksjon setLength ($ lengde) $ this-> lengde = $ lengde; funksjon getLength () return $ this-> lengde; funksjon setSent ($ sendt) $ this-> send = $ sendt; funksjon getSent () return $ this-> send; funksjon getRelativePath () return dirname ($ this-> filnavn); funksjon getFullPath () return realpath ($ this-> getRelativePath ());
De Fil
Klassen er endret litt for å imøtekomme kravene ovenfor. Det implementerer nå målbar
grensesnitt og har setters og getters for feltene vi er interessert i. Musikk
er veldig like, kan du sjekke innholdet i vedlagte kildekoden. Vi er nesten ferdige.
klasse fremgang private $ measurableContent; funksjon __construct (Målbar $ measurableContent) $ this-> measurableContent = $ measurableContent; funksjon getAsPercent () return $ this-> measurableContent-> getSent () * 100 / $ this-> measurableContent-> getLength ();
Framgang
trengte også en liten oppdatering. Vi kan nå spesifisere en type, ved hjelp av typehinting, i konstruktøren. Den forventede typen er målbar
. Nå har vi en eksplisitt kontrakt. Framgang
kan være sikker på at de tilgjengelige metodene alltid vil være til stede fordi de er definert i målbar
grensesnitt. Fil
og Musikk
kan også være sikker på at de kan gi alt som trengs for Framgang
ved å bare implementere alle metodene på grensesnittet, et krav når en klasse implementerer et grensesnitt.
Dette designmønsteret forklares mer detaljert i Agile Design Patterns kurset.
Folk har en tendens til å nevne grensesnitt med en hovedstad Jeg
foran dem, eller med ordet "Interface
"festet på slutten, som ifile
eller FileInterface
. Dette er en gammel stil notasjon pålagt av noen utdaterte standarder. Vi er så mye forbi ungarske notater eller behovet for å spesifisere typen av en variabel eller objekt i navnet for å forenkle identifisere det. IDEer identifiserer noe i et delt sekund for oss. Dette gjør at vi kan konsentrere oss om hva vi egentlig vil abstrahere.
Grensesnitt tilhører sine kunder. Ja. Når du vil nevne et grensesnitt, må du tenke på klienten og glemme implementeringen. Da vi kalt vårt grensesnitt, var målet, så tenkte vi på Progress. Hvis jeg ville være en fremgang, hva ville jeg trenge for å kunne gi prosentandelen? Svaret er enkelt, noe vi kan måle. Dermed navnet Measurable.
En annen grunn er at implementeringen kan komme fra ulike domener. I vårt tilfelle er det filer og musikk. Men vi kan veldig godt gjenbruke vår Framgang
i en racesimulator. I så fall vil de målte klassene være hastighet, drivstoff, etc. Nice, er det ikke?
Template Metode designmønsteret ligner veldig på strategien, men i stedet for et grensesnitt bruker det en abstrakt klasse. Det anbefales å bruke et malemønster når vi har en klient som er veldig spesifikk for applikasjonen vår, med redusert gjenbrukbarhet og når serverklassen har felles oppførsel.
Dette designmønsteret forklares mer detaljert i Agile Design Patterns kurset.
Så, hvordan påvirker alt dette vår arkitektur på høyt nivå?
Hvis bildet ovenfor representerer den nåværende arkitekturen i vår søknad, bør det legges til en ny modul med fem nye klasser (de blå) på moderat måte (rød klasse).
I de fleste systemer kan du ikke forvente absolutt ingen effekt på eksisterende kode når nye klasser blir introdusert. Imidlertid vil respekt for Open / Closed Principle redusere klassene og modulene som krever konstant forandring, betydelig.
Som med ethvert annet prinsipp, prøv å ikke tenke på alt fra før. Hvis du gjør det, vil du ende opp med et grensesnitt for hver av klassene dine. Et slikt design vil være vanskelig å vedlikeholde og forstå. Vanligvis er den sikreste måten å gå på å tenke på mulighetene, og om du kan avgjøre om det vil være andre typer serverklasser. Mange ganger kan du enkelt forestille deg en ny funksjon, eller du kan finne en på prosjektets tilbakemelding som vil produsere en annen serverklasse. I slike tilfeller legger du til grensesnittet fra begynnelsen. Hvis du ikke kan bestemme, eller hvis du er usikker - mesteparten av tiden - bare utelat det. La neste programmerer, eller kanskje til og med selv, legge til grensesnittet når du trenger en annen implementering.
Hvis du følger disiplinen din og legger til grensesnitt så snart en annen server er nødvendig, vil endringer bli få og enkle. Husk at hvis koden kreves endres en gang, er det en høy mulighet det vil kreve endring igjen. Når den muligheten blir til virkelighet, vil OCP spare deg mye tid og krefter.
Takk for at du leser.