SOLID Del 2 - Åpent / lukket prinsipp

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.

Definisjon

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.

OCP i SOLID-konteksten

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..

Det åpenbare eksemplet på OCP-overtredelse

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.

Vis meg koden

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?

Endringskrav

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. Fillengden 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.

Løsning 1: Dra nytte av PHPs dynamiske natur

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.

Løsning 2: Bruk strategisk designmønster

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.

En merknad om grensesnittnavn

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?

Løsning 3: Bruk mønstermetodeutformingsmønsteret

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.

En høyere nivåvisning

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.

Siste tanker

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.