Den eneste ansvaret (SRP), Open / Closed (OCP), Liskov Substitution, Interface Segregation, og avhengighetsinversjon. Fem smidige prinsipper som skal veilede deg hver gang du skriver kode.
Fordi både Liskov Substitution Principle (LSP) og Interface Segregation Principle (ISP) er ganske enkelt å definere og eksemplificere, i denne leksjonen vil vi snakke om begge dem.
Barnklasser bør aldri bryte foreldreklassens typedefinisjoner.
Konseptet med dette prinsippet ble introdusert av Barbara Liskov i en konferanse i 1987 og senere publisert i et papir sammen med Jannette Wing i 1994. Den opprinnelige definisjonen er som følger:
La q (x) være en egenskap som er bevisbar for objekter x av type T. Da skal q (y) være bevisbar for objekter y av type S hvor S er en undertype av T.
Senere med publisering av SOLID-prinsippene av Robert C. Martin i sin bok Agile Software Development, Prinsipper, Mønstre og Practices, og deretter publisert i C # versjonen av boka Agile Prinsipper, Mønstre og Practices i C #, definisjonen ble kjent som Liskov Substitution Principle.
Dette fører oss til definisjonen gitt av Robert C. Martin:
Undertyper må være substituerbare for deres basetyper.
Så enkelt som det, bør en underklasse overstyre klassenes metoder på en måte som ikke bryter funksjonaliteten fra kundens synspunkt. Her er et enkelt eksempel for å demonstrere konseptet.
klasse kjøretøy funksjon startEngine () // Standard motor start funksjonalitet funksjon akselerere () // Standard akselerasjon funksjonalitet
Gitt en klasse Kjøretøy
- det kan være abstrakt - og to implementeringer:
Klassen Bilen utvider kjøretøyet funksjon startEngine () $ this-> engageIgnition (); ordnede :: startEngine (); Private funksjon engageIgnition () // Tenning procedure klasse ElectricBus utvider kjøretøy funksjon akselerere () $ this-> increaseVoltage (); $ Dette-> connectIndividualEngines (); privat funksjon increaseVoltage () // Elektrisk logikk privat funksjon connectIndividualEngines () // Connection logic
En klient klasse skal kunne bruke en av dem, hvis den kan bruke Kjøretøy
.
klasse driver funksjon gå (kjøretøy $ v) $ v-> startEngine (); $ V-> akselerere ();
Som fører oss til en enkel implementering av Template Method Design Pattern som vi brukte det i OCP-opplæringen.
Basert på vår tidligere erfaring med åpen / lukket prinsipp kan vi konkludere med at Liskovs Substitutionsprinsipp er i sterk sammenheng med OCP. Faktisk er "et brudd på LSP et latent brudd på OCP" (Robert C. Martin), og Template Method Design Pattern er et klassisk eksempel på å respektere og implementere LSP, som igjen er en av løsningene for å respektere OCP også.
For å illustrere dette helt, vil vi gå med et klassisk eksempel fordi det er svært signifikant og lettforståelig.
klasse rektangel private $ topLeft; privat $ bredde; privat $ høyde; offentlig funksjon setHeight ($ høyde) $ this-> height = $ height; offentlig funksjon getHeight () return $ this-> height; offentlig funksjon setWidth ($ bredde) $ this-> width = $ width; offentlig funksjon getWidth () return $ this-> width;
Vi starter med en grunnleggende geometrisk form, a Rektangel
. Det er bare et enkelt dataobjekt med setters og getters for bredde
og høyde
. Tenk at applikasjonen vår virker, og den er allerede distribuert til flere kunder. Nå trenger de en ny funksjon. De må kunne manipulere kvadrater.
I virkeligheten, i geometri, er en firkant en bestemt form for rektangel. Så vi kunne prøve å implementere en Torget
klasse som strekker seg a Rektangel
klasse. Det er ofte sagt at en barneklasse er en foreldre klasse, og dette uttrykket samsvarer også med LSP, i hvert fall ved første blikk.
Men er en Torget
virkelig a Rektangel
i programmering?
Class Square utvider rektangel offentlig funksjon setHeight ($ value) $ this-> width = $ value; $ this-> height = $ value; offentlig funksjon setWidth ($ verdi) $ this-> width = $ value; $ this-> height = $ value;
En firkant er et rektangel med like bredde og høyde, og vi kunne gjøre en merkelig implementering som i eksempelet ovenfor. Vi kunne overskrive begge setter til å sette høyde og bredde. Men hvordan ville det påvirke klientkode?
klasseklient funksjonsområdeVerifier (rektangel $ r) $ r-> setWidth (5); $ R-> setHeight (4); hvis ($ r-> område ()! = 20) kast ny unntak ('dårlig område!'); returnere sann;
Det er tenkelig å ha en klientklasse som verifiserer rektangelens område og kaster et unntak dersom det er feil.
funksjonsområde () return $ this-> width * $ this-> height;
Selvfølgelig har vi lagt til den ovennevnte metoden til vår Rektangel
klasse for å gi området.
klasse LspTest utvider PHPUnit_Framework_TestCase funksjonstestRectangleArea () $ r = nytt rektangel (); $ c = ny klient (); $ Dette-> assertTrue ($ C-> areaVerifier ($ r));
Og vi opprettet en enkel test ved å sende et tomt rektangelobjekt til områdeverifikator og testen passerer. Hvis vår Torget
klassen er riktig definert, sender den til klientens areaVerifier ()
bør ikke bryte sin funksjonalitet. Tross alt, a Torget
er en Rektangel
i all matematisk forstand. Men er vår klasse?
funksjon testSquareArea () $ r = nytt Square (); $ c = ny klient (); $ Dette-> assertTrue ($ C-> areaVerifier ($ r));
Testing det er veldig enkelt og det bryter stor tid. Et unntak blir kastet til oss når vi kjører testen ovenfor.
PHPUnit 3.7.28 av Sebastian Bergmann. Unntak: Dårlig område! # 0 / paht /: / ... / ... /LspTest.php(18): Client-> areaVerifier (Object (Square)) # 1 [intern funksjon]: LspTest-> testSquareArea ()
Så, vår Torget
klassen er ikke en Rektangel
tross alt. Det bryter geometriens lover. Det mislykkes, og det bryter med Liskov Substitution Principle.
Jeg elsker spesielt dette eksemplet fordi det ikke bare bryter med LSP, det viser også at objektorientert programmering ikke handler om å kartlegge virkeligheten til objekter. Hvert objekt i vårt program må være en abstraksjon over et konsept. Hvis vi prøver å kartlegge en-til-en ekte objekter til programmerte objekter, vil vi nesten alltid mislykkes.
Prinsippet om enhetsansvar handler om skuespillere og arkitektur på høyt nivå. Det åpne / lukkede prinsippet handler om klassedesign og funksjonsutvidelser. Liskov Substitution Principle handler om subtyping og arv. Interface Segregation Principle (ISP) handler om forretningslogikk til kundens kommunikasjon.
I alle modulære applikasjoner må det være en slags grensesnitt som klienten kan stole på. Disse kan være faktiske Interface-typede enheter eller andre klassiske objekter som implementerer designmønstre som fasader. Det spiller ingen rolle hvilken løsning som brukes. Det har alltid samme omfang: å kommunisere med klientkoden om hvordan du bruker modulen. Disse grensesnittene kan ligge mellom ulike moduler i samme program eller prosjekt, eller mellom ett prosjekt som et tredjepartsbibliotek som serverer et annet prosjekt. Igjen, det spiller ingen rolle. Kommunikasjon er kommunikasjon og klienter er klienter, uavhengig av de faktiske personene som skriver koden.
Så, hvordan skal vi definere disse grensesnittene? Vi kunne tenke på modulen vår og avsløre alle funksjonalitetene vi ønsker at den skal tilby.
Dette ser ut som en god start, en fin måte å definere hva vi vil implementere i modulen vår. Eller er det? En slik start vil føre til en av to mulige implementeringer:
Bil
eller Buss
klasse implementerer alle metodene på Kjøretøy
grensesnitt. Bare de store dimensjonene av slike klasser bør fortelle oss å unngå dem for enhver pris.LightsControl
, Fartskontroll
, eller RadioCD
som alle implementerer hele grensesnittet, men faktisk gir noe nyttig bare for de delene de implementerer.Det er åpenbart at ingen løsning er akseptabel for å implementere vår forretningslogikk.
Vi kunne ta en annen tilnærming. Bryt grensesnittet i stykker, spesialisert på hver implementering. Dette vil bidra til å bruke små klasser som bryr seg om sitt eget grensesnitt. Objektene som implementerer grensesnittene, vil bli brukt av den forskjellige typen kjøretøy, som bil i bildet ovenfor. Bilen vil bruke implementeringene, men vil avhenge av grensesnittene. Så et skjema som det nedenfor kan være enda mer uttrykksfulle.
Men dette fundamentalt endrer vår oppfatning av arkitekturen. De Bil
blir klienten i stedet for implementeringen. Vi vil fortsatt gi våre kunder muligheter til å bruke hele vår modul, som er en type kjøretøy.
Anta at vi løst implementeringsproblemet, og vi har en stabil forretningslogikk. Det enkleste å gjøre er å gi et enkelt grensesnitt med alle implementeringer og la kundene, i vårt tilfelle Busstasjon
, Hovedvei
, Sjåfør
og så videre, for å bruke hva som helst som vil ha fra grensesnittets implementering. I utgangspunktet skifter dette oppførselsseleksjonsansvaret til kundene. Du kan finne denne typen løsning i mange eldre applikasjoner.
Grensesnittet-segregeringsprinsippet (ISP) sier at ingen klient skal være tvunget til å avhenge av metoder som den ikke bruker.
Imidlertid har denne løsningen sine problemer. Nå er alle klientene avhengige av alle metodene. Hvorfor skal en Busstasjon
avhengig av lyset på bussen eller på radiokanaler valgt av sjåføren? Det burde ikke. Men hva om det gjør? Gjør det noe? Vel, hvis vi tenker på "Single Responsibility Principle", er det et søsterkonsept for denne. Hvis Busstasjon
Avhenger av mange individuelle implementeringer, ikke en gang brukt av den, kan det kreve endringer hvis noen av de enkelte små implementeringer endres. Dette gjelder spesielt for kompilerte språk, men vi kan fortsatt se effekten av LightControl
endre innvirkning Busstasjon
. Disse tingene bør aldri skje.
Grensesnitt tilhører sine kunder og ikke til implementeringene. Dermed bør vi alltid utforme dem på en måte som best passer våre kunder. Noen ganger kan vi, noen ganger kan vi ikke akkurat kjenne våre kunder. Men når vi kan, bør vi bryte våre grensesnitt i mange mindre, slik at de bedre tilfredsstiller de eksakte behovene til våre kunder.
Selvfølgelig vil dette føre til en viss grad av duplisering. Men husk! Grensesnitt er bare enkle funksjonsnavndefinisjoner. Det er ingen implementering av noen form for logikk i dem. Så duplikasjonene er små og håndterbare.
Da har vi den store fordelen av klienter avhengig bare og bare på hva de faktisk trenger og bruker. I noen tilfeller kan klienter bruke og trenger flere grensesnitt, det er OK, så lenge de bruker alle metodene fra alle grensesnittene de er avhengige av.
Et annet fint triks er at i vår forretningslogikk kan en enkelt klasse implementere flere grensesnitt om nødvendig. Så vi kan gi en enkelt implementering for alle de vanlige metodene mellom grensesnittene. De segregerte grensesnittene vil også tvinge oss til å tenke på koden mer fra kundens synspunkt, noe som igjen vil føre til løs kobling og enkel testing. Så ikke bare har vi gjort vår kode bedre til våre kunder, vi har også gjort det lettere for oss selv å forstå, teste og implementere.
LSP lærte oss hvorfor virkeligheten ikke kan representeres som en en-til-en-forbindelse med programmerte objekter, og hvordan undertyper skal respektere sine foreldre. Vi legger det også i lys av de andre prinsippene vi allerede visste.
ISP lærer oss å respektere våre kunder mer enn vi trodde nødvendig. Respekt deres behov vil gjøre koden bedre og våre liv som programmerere lettere.
Takk for tiden din.