SOLID Del 3 - Liskov Substitution & Interface Segregation Principles

Det enda ansvaret (SRP), Open / Closed (OCP), Liskov Substitution, Interface Segregation, och beroendeinversion. Fem smidiga principer som bör styra dig varje gång du skriver kod.

Eftersom både Liskov Substitution Principle (LSP) och Interface Segregation Principle (ISP) är ganska lätta att definiera och exemplifiera, i den här lektionen kommer vi att prata om dem båda.

Liskov Substitution Principle (LSP)

Barnklasser bör aldrig bryta föräldrarklassens definitioner.

Begreppet denna princip infördes av Barbara Liskov i en konferens-konferens 1987 och publicerades senare i ett papper tillsammans med Jannette Wing 1994. Den ursprungliga definitionen är följande:

Låt q (x) vara en egenskap som kan provas om objekt x av typ T. Då ska q (y) vara provbar för objekt y av typ S där S är en subtyp av T.

Senare, med publiceringen av SOLID-principerna av Robert C. Martin i sin bok Agile Software Development, Principles, Patterns, and Practices och sedan publiceras i C # versionen av boken Agile Princips, Patterns and Practices i C #, definierar definitionen blev känt som Liskov Substitution Principle.

Detta leder oss till definitionen av Robert C. Martin:

Undertyper måste vara ersättningsbara för sina bastyper.

Så enkelt som det borde en underklass överväga moderklassens metoder på ett sätt som inte bryter funktionalitet ur kundens synvinkel. Här är ett enkelt exempel för att visa konceptet.

klassfordon funktion startEngine () // Standard motor startfunktion funktion accelerera () // Standard accelerationsfunktionalitet

Givet en klass Fordon - det kan vara abstrakt - och två implementeringar:

klass Bilen utökar fordon funktion startEngine () $ this-> engageIgnition (); förälder :: startEngine ();  privat funktion engageIgnition () // Tändningsprocedur klass ElectricBus utökar fordon funktion accelerera () $ this-> increaseVoltage (); $ This-> connectIndividualEngines ();  privat funktion increaseVoltage () // Elektrisk logik privat funktion connectIndividualEngines () // Connection logic

En klientklass ska kunna använda någon av dem, om den kan använda Fordon.

klassförare funktion gå (fordon $ v) $ v-> startEngine (); $ V-> accelerera (); 

Vilket leder oss till en enkel implementering av Template Method Design Pattern som vi använde det i OCP-handledningen.


Baserat på vår tidigare erfarenhet av Open / Closed Principle kan vi dra slutsatsen att Liskovs Substitution Principle är i stark relation med OCP. Faktum är att "ett brott mot LSP är en latent kränkning av OCP" (Robert C. Martin), och Template Method Design Pattern är ett klassiskt exempel på att respektera och implementera LSP, vilket i sin tur är en av lösningarna att respektera OCP också.

Det klassiska exemplet av LSP-överträdelse

För att illustrera detta helt kommer vi att gå med ett klassiskt exempel eftersom det är mycket viktigt och lättförståeligt.

klass rektangel privat $ topLeft; privat $ bredd; privat $ höjd; allmän funktion setHeight ($ höjd) $ this-> height = $ height;  offentlig funktion getHeight () returnera $ this-> height;  public function setWidth ($ width) $ this-> width = $ width;  offentlig funktion getWidth () return $ this-> width; 

Vi börjar med en grundläggande geometrisk form, a Rektangel. Det är bara ett enkelt dataobjekt med setters och getters för bredd och höjd. Föreställ dig att vår ansökan fungerar och den är redan distribuerad till flera kunder. Nu behöver de en ny funktion. De måste kunna manipulera rutor.

I det verkliga livet, i geometri, är en kvadrat en särskild form av rektangel. Så vi kunde försöka genomföra en Fyrkant klass som sträcker sig a Rektangel klass. Det sägs ofta att en barnklass är en föräldraklass, och detta uttryck överensstämmer också med LSP, åtminstone vid första ögonkastet.


Men är en Fyrkant verkligen a Rektangel i programmering?

klass kvadrat utvidgar rektangel public function setHeight ($ value) $ this-> width = $ value; $ this-> height = $ value;  public function setWidth ($ value) $ this-> width = $ value; $ this-> height = $ value; 

En fyrkant är en rektangel med samma bredd och höjd, och vi kan göra en konstig implementering som i ovanstående exempel. Vi kunde skriva över båda setterna för att ställa in både höjd och bredd. Men hur skulle det påverka klientkoden?

klass klient funktion areaVerifier (rektangel $ r) $ r-> setWidth (5); $ R-> setHeight (4); om ($ r-> area ()! = 20) kasta ny Undantag ('Dåligt område!');  returnera sant; 

Det är tänkbart att ha en klientklass som verifierar rektangelns område och kastar ett undantag om det är fel.

funktionsområde () return $ this-> width * $ this-> height; 

Naturligtvis lade vi till ovanstående metod till vår Rektangel klass för att ge området.

klass LspTest utökar PHPUnit_Framework_TestCase funktionstestRectangleArea () $ r = ny rektangel (); $ c = ny klient (); $ This-> assertTrue ($ c-> areaVerifier ($ r)); 

Och vi skapade ett enkelt test genom att skicka ett tomt rektangelobjekt till områdesverifieraren och testet passerar. Om vår Fyrkant klassen är korrekt definierad, skickar den till kundens areaVerifier () borde inte bryta dess funktionalitet. Trots allt, a Fyrkant är en Rektangel i all matematisk bemärkelse. Men är vår klass?

funktion testSquareArea () $ r = nytt kvadrat (); $ c = ny klient (); $ This-> assertTrue ($ c-> areaVerifier ($ r)); 

Testa det är väldigt lätt och det går sönder stor tid. Ett undantag slängs till oss när vi kör testet ovan.

PHPUnit 3.7.28 av Sebastian Bergmann. Undantag: Dåligt område! # 0 / paht /: / ... / ... / LspTest.php(18): Klient-> areaVerifier (Object (Square)) # 1 [intern funktion]: LspTest-> testSquareArea ()

Så, vår Fyrkant klassen är inte en Rektangel trots allt. Det bryter geometrins lagar. Det misslyckas och det strider mot principen om Liskov-substitution.

Jag älskar särskilt det här exemplet eftersom det inte bara kränker LSP, det visar också att objektorienterad programmering inte handlar om att kartlägga det verkliga livet till objekt. Varje objekt i vårt program måste vara en abstraktion över ett koncept. Om vi ​​försöker kartlägga ett till ett riktigt objekt till programmerade objekt, kommer vi nästan alltid att misslyckas.

Gränssnittssegregationsprincipen

Principen om ett ansvarsområde handlar om aktörer och arkitektur på hög nivå. Det öppna / slutna principen handlar om klassdesign och funktionstillägg. Liskov Substitution Principle handlar om subtyping och arv. Interface Segregation Principle (ISP) handlar om affärslogik till kundens kommunikation.

I alla modulära applikationer måste det finnas något slags gränssnitt som kunden kan lita på. Dessa kan vara aktuella gränssnittstypade enheter eller andra klassiska objekt som genomför designmönster som fasader. Det spelar ingen roll vilken lösning som används. Den har alltid samma räckvidd: att kommunicera med klientkoden om hur man använder modulen. Dessa gränssnitt kan ligga mellan olika moduler i samma applikation eller projekt, eller mellan ett projekt som ett tredje part bibliotek som betjänar ett annat projekt. Återigen spelar det ingen roll. Kommunikation är kommunikation och kunder är klienter, oavsett de faktiska personer som skriver koden.

Så hur ska vi definiera dessa gränssnitt? Vi kan tänka på vår modul och avslöja alla funktioner vi vill erbjuda.


Det ser ut som en bra start, ett bra sätt att definiera vad vi vill implementera i vår modul. Eller är det? En start så här kommer att leda till en av två möjliga implementeringar:

  • En enorm Bil eller Buss klass genomföra alla metoder på Fordon gränssnitt. Endast de stora dimensionerna av sådana klasser bör berätta för oss att undvika dem till varje pris.
  • Eller, många små klasser som LightsControl, Hastighets kontroll, eller RadioCD som alla implementerar hela gränssnittet men faktiskt ger något användbart bara för de delar de implementerar.

Det är uppenbart att ingen lösning är acceptabel för att genomföra vår affärslogik.


Vi kunde ta ett annat tillvägagångssätt. Bryt gränssnittet i bitar, specialiserat på varje implementering. Detta skulle hjälpa till att använda små klasser som bryr sig om sitt eget gränssnitt. Objekten som genomför gränssnittet kommer att användas av olika typer av fordon, som bil i bilden ovan. Bilen kommer att använda implementeringarna men beror på gränssnittet. Så ett schema som det nedan kan vara ännu mer uttrycksfullt.


Men detta förändrar i grunden vår uppfattning om arkitekturen. De Bil blir kunden istället för genomförandet. Vi vill fortfarande ge våra kunder möjligheter att använda hela vår modul, det är en typ av fordon.


Anta att vi löst implementeringsproblemet och vi har en stabil affärslogik. Det enklaste att göra är att tillhandahålla ett enda gränssnitt med alla implementeringar och låta kunderna, i vårt fall Busstation, Motorväg, Förare och så vidare, för att använda vad som helst som helst från gränssnittets genomförande. I grund och botten skiftar detta beteendevalet ansvaret till kunderna. Du hittar denna typ av lösning i många äldre applikationer.

Gränssnittets segregeringsprincipen (ISP) säger att ingen klient måste tvingas bero på metoder som den inte använder.

Emellertid har denna lösning sina problem. Nu är alla kunder beroende av alla metoder. Varför ska a Busstation Beroende på läget för bussen eller på de radiokanaler som valts av föraren? Det borde inte. Men vad händer om det gör det? Spelar det någon roll? Tja, om vi tänker på principen om ensam ansvar, är det ett systerkoncept för den här. Om Busstation beror på många individuella implementeringar, inte ens används av den, kan det kräva förändringar om någon av de enskilda små implementationerna ändras. Detta gäller speciellt för kompilerade språk, men vi kan fortfarande se effekten av Ljus kontroll förändra påverkan Busstation. Dessa saker borde aldrig hända.

Gränssnitt tillhör sina kunder och inte till implementeringarna. Således bör vi alltid utforma dem på ett sätt som bäst passar våra kunder. Vissa gånger kan vi, ibland kan vi inte exakt veta våra kunder. Men när vi kan, borde vi bryta våra gränssnitt i många mindre, så att de bättre uppfyller våra kunders exakta behov.


Naturligtvis kommer detta att leda till viss grad av dubbelarbete. Men kom ihåg! Gränssnitt är bara vanliga funktionsnamndefinitioner. Det finns ingen implementering av någon form av logik i dem. Så dubbletterna är små och hanterbara.

Därefter har vi den stora fördelen av kunderna beroende bara och bara på vad de faktiskt behöver och använder. I vissa fall kan kunder använda och behöva flera gränssnitt, det är OK, så länge de använder alla metoder från alla gränssnitt de är beroende av.

Ett annat bra knep är att i vår affärslogik kan en enda klass genomföra flera gränssnitt om det behövs. Så vi kan tillhandahålla en enda implementering för alla vanliga metoder mellan gränssnitten. De segregerade gränssnitten kommer också att tvinga oss att tänka på vår kod mer från kundens synvinkel, vilket i sin tur leder till lös koppling och enkel testning. Så, inte bara har vi gjort vår kod bättre för våra kunder, vi gjorde det också lättare för oss själva att förstå, testa och genomföra.

Slutgiltiga tankar

LSP lärde oss varför verkligheten inte kan representeras som ett ett-till-ett-förhållande med programmerade objekt och hur subtyper ska respektera sina föräldrar. Vi lägger det också i ljuset av de övriga principerna som vi redan visste.

Internetleverantör lär oss att respektera våra kunder mer än vi trodde var nödvändiga. Att respektera deras behov kommer att göra vår kod bättre och våra liv som programmerare lättare.

Tack för din tid.