SOLID Deel 3 - Principes van Liskov-substitutie en scheidingsvlakscheiding

De enkele verantwoordelijkheid (SRP), Open / Closed (OCP), Liskov-substitutie, interfacesegregatie, en afhankelijkheid inversie. Vijf behendige principes die je elke keer moeten begeleiden als je code schrijft.

Omdat zowel het Liskov Substitution Principle (LSP) als het Interface Segregation Principle (ISP) redelijk gemakkelijk te definiëren en te illustreren zijn, zullen we in deze les over beide gaan praten.

Liskov Substitution Principle (LSP)

Kinderlessen mogen nooit de typedefinities van de ouderklasse doorbreken.

Het concept van dit principe werd geïntroduceerd door Barbara Liskov in een conferentie-keynote uit 1987 en later gepubliceerd in een paper samen met Jannette Wing in 1994. Hun oorspronkelijke definitie is als volgt:

Laat q (x) een eigenschap zijn die aantoonbaar is over objecten x van type T. Dan moet q (y) bewijsbaar zijn voor objecten y van het type S, waarbij S een subtype van T is.

Later, met de publicatie van de SOLID-principes door Robert C. Martin in zijn boek Agile Software Development, Principles, Patterns and Practices en vervolgens opnieuw gepubliceerd in de C # -versie van het boek Agile Principles, Patterns and Practices in C #, de definitie werd bekend als het Liskov-vervangingsprincipe.

Dit leidt ons naar de definitie gegeven door Robert C. Martin:

Subtypes moeten substitueerbaar zijn voor hun basistypen.

Zo simpel is het dat een subklasse de methoden van de ouderklasse moet overschrijven op een manier die de functionaliteit niet uit het oogpunt van de klant schaadt. Hier is een eenvoudig voorbeeld om het concept te demonstreren.

klasse Vehicle function startEngine () // standaardfunctie voor het starten van de motor accelereren met functie () // standaardfunctie voor acceleratie

Gegeven een klas Voertuig - het kan abstract zijn - en twee implementaties:

class Car extends Vehicle function startEngine () $ this-> engageIgnition (); ouder :: startmotor ();  private functie engageIgnition () // Ignition procedure class ElectricBus breidt Vehicle function accelerate () $ this-> increaseVoltage (); $ This-> connectIndividualEngines ();  private functie increaseVoltage () // Electric logic private function connectIndividualEngines () // Verbindingslogica

Een clientklasse moet een van beide kunnen gebruiken als deze kan gebruiken Voertuig.

class Driver functie go (Voertuig $ v) $ v-> startEngine (); $ V-> versnellen (); 

Wat ons leidt naar een eenvoudige implementatie van het Template Method Design Pattern zoals we het gebruikten in de OCP-tutorial.


Gebaseerd op onze eerdere ervaring met het Open / Gesloten Principe, kunnen we concluderen dat Liskov's Substitutie Principe in sterke relatie staat met OCP. "Een schending van LSP is een latente schending van OCP" (Robert C. Martin) en het sjabloonontwerppatroon is een klassiek voorbeeld van het respecteren en implementeren van LSP, dat op zijn beurt een van de oplossingen is om OCP ook te respecteren..

Het klassieke voorbeeld van LSP-overtreding

Om dit volledig te illustreren, gaan we met een klassiek voorbeeld omdat het zeer significant en gemakkelijk te begrijpen is.

class Rectangle private $ topLeft; privé $ breedte; privé $ hoogte; openbare functie setHighight ($ height) $ this-> height = $ height;  openbare functie getHeight () return $ this-> height;  public function setWidth ($ width) $ this-> width = $ width;  openbare functie getWidth () return $ this-> width; 

We beginnen met een geometrische basisvorm, een Rechthoek. Het is gewoon een eenvoudig data-object met setters en getters voor breedte en hoogte. Stel je voor dat onze applicatie werkt en dat deze al op verschillende clients is geïmplementeerd. Nu hebben ze een nieuwe functie nodig. Ze moeten vierkanten kunnen manipuleren.

In het echte leven, in de geometrie, is een vierkant een bepaalde vorm van een rechthoek. Dus we zouden kunnen proberen om een Plein klasse die zich uitstrekt Rechthoek klasse. Er wordt vaak gezegd dat een kind klasse is een bovenliggende klasse, en deze expressie voldoet ook aan LSP, althans op het eerste gezicht.


Maar is een Plein echt een Rechthoek in programmeren?

class Square verlengt Rectangle public function setHeight ($ value) $ this-> width = $ value; $ this-> height = $ waarde;  public function setWidth ($ value) $ this-> width = $ value; $ this-> height = $ waarde; 

Een vierkant is een rechthoek met dezelfde breedte en hoogte, en we zouden een vreemde uitvoering kunnen doen zoals in het bovenstaande voorbeeld. We kunnen beide setters overschrijven om zowel de hoogte als de breedte in te stellen. Maar hoe zou dat van invloed zijn op de klantcode?

class Client function areaVerifier (Rectangle $ r) $ r-> setWidth (5); $ R-> setHeight (4); if ($ r-> area ()! = 20) gooi nieuwe uitzondering ('Bad area!');  return true; 

Het is denkbaar om een ​​clientklasse te hebben die het gebied van de rechthoek verifieert en een uitzondering genereert als deze fout is.

functiegebied () return $ this-> width * $ this-> height; 

Natuurlijk hebben we de bovenstaande methode toegevoegd aan onze Rechthoek klasse om het gebied te bieden.

klasse LspTest breidt PHPUnit_Framework_TestCase uit function testRectangleArea () $ r = new Rectangle (); $ c = nieuwe Client (); $ This-> assertTrue ($ c-> areaVerifier ($ r)); 

En we hebben een eenvoudige test gemaakt door een leeg rechthoekobject naar de gebiedscontroleur te sturen en de testpassen door te geven. Als onze Plein klasse is correct gedefinieerd, verzenden naar de klant areaVerifier () mag de functionaliteit niet schaden. Immers, een Plein is een Rechthoek in alle wiskundige zin. Maar is onze klas?

function testSquareArea () $ r = new Square (); $ c = nieuwe Client (); $ This-> assertTrue ($ c-> areaVerifier ($ r)); 

Het testen is heel eenvoudig en het breekt enorm. Er wordt een uitzondering op ons gegenereerd wanneer we de bovenstaande test uitvoeren.

PHPUnit 3.7.28 door Sebastian Bergmann. Uitzondering: Slechte omgeving! # 0 / paht /: / ... / ... /LspTest.php(18): Client-> areaVerifier (Object (Square)) # 1 [interne functie]: LspTest-> testSquareArea ()

Zo onze Plein klasse is geen Rechthoek ten slotte. Het breekt de wetten van de geometrie. Het faalt en het is in strijd met het Liskov-vervangingsprincipe.

Ik ben vooral dol op dit voorbeeld omdat het niet alleen LSP schendt, maar ook dat objectgeoriënteerd programmeren niet gaat over het in kaart brengen van het echte leven met objecten. Elk object in ons programma moet een abstractie zijn van een concept. Als we één-op-één echte objecten toewijzen aan geprogrammeerde objecten, zullen we bijna altijd falen.

Het interface segregatieprincipe

Het Single Responsibility Principle gaat over actoren en architectuur op hoog niveau. Het open / gesloten principe gaat over klasseontwerp en functie-uitbreidingen. Het Liskov-vervangingsprincipe gaat over subtypen en overerving. Het Interface Segregation Principle (ISP) gaat over bedrijfslogica voor de communicatie van klanten.

In alle modulaire applicaties moet er een soort interface zijn waarop de klant kan vertrouwen. Dit kunnen werkelijke door Interface getypeerde entiteiten zijn of andere klassieke objecten die ontwerppatronen zoals gevels implementeren. Het maakt niet uit welke oplossing wordt gebruikt. Het heeft altijd dezelfde scope: communiceren met de clientcode over het gebruik van de module. Deze interfaces kunnen zich bevinden tussen verschillende modules in dezelfde toepassing of hetzelfde project, of tussen één project als een externe bibliotheek die een ander project ondersteunt. Nogmaals, het maakt niet uit. Communicatie is communicatie en klanten zijn klanten, ongeacht de feitelijke personen die de code schrijven.

Dus, hoe moeten we deze interfaces definiëren? We kunnen nadenken over onze module en alle functies blootleggen die we willen aanbieden.


Dit lijkt een goed begin, een geweldige manier om te definiëren wat we willen implementeren in onze module. Of is het? Een start als deze zal leiden tot een van de twee mogelijke implementaties:

  • Een enorme Auto of Bus klasse die alle methoden implementeert op de Voertuig interface. Alleen de enorme afmetingen van dergelijke klassen zouden ons moeten vertellen om ze ten koste van alles te vermijden.
  • Of, veel kleine klassen zoals LightsControl, Snelheidscontrole, of RadioCD die allemaal de hele interface implementeren maar eigenlijk alleen iets bieden dat nuttig is voor de onderdelen die ze implementeren.

Het is duidelijk dat geen van beide oplossingen aanvaardbaar is om onze bedrijfslogica te implementeren.


We kunnen een andere aanpak kiezen. Breek de interface in stukken, speciaal voor elke implementatie. Dit zou helpen om kleine klassen te gebruiken die om hun eigen interface geven. De objecten die de interfaces implementeren, worden gebruikt door de verschillende typen voertuigen, zoals de auto in de bovenstaande afbeelding. De auto zal de implementaties gebruiken maar zal afhankelijk zijn van de interfaces. Dus een schema als hieronder kan nog expressiever zijn.


Maar dit verandert fundamenteel onze perceptie van de architectuur. De Auto wordt de client in plaats van de implementatie. We willen onze klanten nog steeds manieren bieden om onze hele module te gebruiken, dat is een soort voertuig.


Stel dat we het implementatieprobleem hebben opgelost en we een stabiele bedrijfslogica hebben. Het eenvoudigste is om één enkele interface te bieden met alle implementaties en de clients in ons geval te laten werken Bushalte, Snelweg, Bestuurder en zo verder, om wat dan ook te willen gebruiken van de implementatie van de interface. In feite verschuift dit de verantwoordelijkheid voor gedragsselectie naar de klanten. Je vindt dit soort oplossing in veel oudere applicaties.

Het interface-segregatieprincipe (ISP) stelt dat geen enkele cliënt moet worden gedwongen afhankelijk te zijn van methoden die hij niet gebruikt.

Deze oplossing heeft echter zijn problemen. Nu zijn alle klanten afhankelijk van alle methoden. Waarom zou een Bushalte afhankelijk van de verlichtingstoestand van de bus of van de radiozenders die door de bestuurder zijn geselecteerd? Dat zou niet moeten. Maar wat als het gebeurt? Maakt het uit? Welnu, als we nadenken over het beginsel van één verantwoordelijkheid, is dit een zusterconcept. Als Bushalte afhankelijk van vele individuele implementaties, zelfs niet gebruikt door het, het kan wijzigingen vereisen als een van de individuele kleine implementaties veranderen. Dit geldt met name voor gecompileerde talen, maar we kunnen nog steeds het effect zien van de Lichtbesturing verander impact Bushalte. Deze dingen zouden nooit mogen gebeuren.

Interfaces behoren tot hun klanten en niet tot de implementaties. Daarom moeten we ze altijd zo ontwerpen dat onze klanten het best worden aangepast. Soms kunnen we, soms weten we onze klanten niet precies. Maar als we kunnen, moeten we onze interfaces in veel kleinere onderbreken, zodat ze beter voldoen aan de exacte behoeften van onze klanten.


Dit zal natuurlijk tot enige mate van duplicatie leiden. Maar onthoud! Interfaces zijn gewoon functienaamdefinities. Er is geen implementatie van enige vorm van logica in hen. Dus de duplicaties zijn klein en beheersbaar.

Dan hebben we het grote voordeel dat klanten alleen afhankelijk zijn van wat ze echt nodig hebben en gebruiken. In sommige gevallen kunnen clients verschillende interfaces gebruiken en nodig hebben, dat is OK, zolang ze alle methoden gebruiken van alle interfaces waarvan ze afhankelijk zijn.

Een andere leuke truc is dat in onze bedrijfslogica een enkele klasse indien nodig meerdere interfaces kan implementeren. We kunnen dus één enkele implementatie bieden voor alle gangbare methoden tussen de interfaces. De gescheiden interfaces zullen ons ook dwingen om meer rekening te houden met onze code vanuit het oogpunt van de klant, wat op zijn beurt zal leiden tot losse koppeling en eenvoudig testen. Dus hebben we onze code niet alleen beter gemaakt voor onze klanten, we hebben het onszelf ook gemakkelijker gemaakt om te begrijpen, te testen en uit te voeren.

Laatste gedachten

LSP heeft ons geleerd waarom de realiteit niet kan worden weergegeven als een een-op-een relatie met geprogrammeerde objecten en hoe subtypen hun ouders moeten respecteren. We plaatsen het ook in het licht van de andere principes die we al kenden.

ISP leert ons onze klanten meer te respecteren dan we nodig achtten. Het respecteren van hun behoeften zal onze code verbeteren en ons leven als programmeurs eenvoudiger maken.

Bedankt voor je tijd.