SOLID Deel 4 - Het principe van de afhankelijkheid van inversie

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

Het zou onrechtvaardig zijn om u te vertellen dat elk van de SOLID-principes belangrijker is dan een ander. Waarschijnlijk heeft geen van de anderen een dergelijk onmiddellijk en diepgaand effect op uw code dan het principe van de afhankelijkheid van Inversie of kortweg DIP. Als je de andere principes moeilijk te begrijpen of toepassen vindt, begin dan met deze en pas de rest toe op code die al DIP respecteert.

Definitie

A. Modulen op hoog niveau mogen niet afhankelijk zijn van modules op een laag niveau. Beide moeten afhangen van abstracties.
B. Abstracties mogen niet afhankelijk zijn van details. Details moeten afhangen van abstracties.

Dit principe werd gedefinieerd door Robert C. Martin in zijn boek Agile Software Development, Principles, Patterns and Practices en later opnieuw gepubliceerd in de C # -versie van het boek Agile Principles, Patterns and Practices in C #, en het is de laatste van de vijf SOLID-agile-principes.

DIP in de echte wereld

Voordat we beginnen met coderen, wil ik je een verhaal vertellen. Bij Syneto waren we niet altijd zo voorzichtig met onze code. Een paar jaar geleden wisten we minder en hoewel we ons best deden, waren niet al onze projecten zo leuk. We gingen door de hel en weer terug en we leerden veel dingen met vallen en opstaan.

De SOLID-principes en de propere architectuurprincipes van oom Bob (Robert C. Martin) werden een spelwisselaar voor ons en transformeerden onze manier van coderen op manieren die moeilijk te beschrijven zijn. Ik zal in een notendop een aantal belangrijke architecturale beslissingen proberen op te stellen die zijn opgelegd door DIP en die een grote impact hebben gehad op onze projecten..

De meeste webprojecten bevatten drie hoofdtechnologieën: HTML, PHP en SQL. De specifieke versie van deze applicaties waar we het over hebben of welk type SQL-implementaties u gebruikt, is niet relevant. Het punt is dat informatie uit een HTML-formulier op de een of andere manier in de database terechtkomt. De lijm tussen de twee kan worden voorzien van PHP.

Wat essentieel is om hier afstand van te nemen, is dat hoe mooi de drie technologieën drie verschillende architecturale lagen vertegenwoordigen: gebruikersinterface, bedrijfslogica en persistentie. We zullen in één minuut de implicaties van deze lagen bespreken. Laten we ons voorlopig concentreren op enkele vreemde maar veel voorkomende oplossingen om de technologieën samen te laten werken.

Vaak heb ik projecten gezien die SQL-code in een PHP-tag in een HTML-bestand gebruikten, of PHP-code die pagina's en pagina's van echo's weergaf en rechtstreeks de $ _GET of $ _POST globale variabelen. Maar waarom is dit slecht?


De bovenstaande afbeeldingen vertegenwoordigen een onbewerkte versie van wat we in de vorige alinea hebben beschreven. De pijlen vertegenwoordigen verschillende afhankelijkheden, en zoals we kunnen concluderen, is in feite alles van alles afhankelijk. Als we een databasetabel moeten wijzigen, kunnen we uiteindelijk een HTML-bestand bewerken. Of als we een veld in HTML wijzigen, veranderen we mogelijk de naam van een kolom in een SQL-instructie. Of, als we naar het tweede schema kijken, moeten we misschien onze PHP aanpassen als de HTML verandert, of in zeer slechte gevallen, wanneer we alle HTML-inhoud vanuit een PHP-bestand genereren, zullen we zeker een PHP-bestand moeten wijzigen HTML-inhoud aanpassen. Het lijdt dus geen twijfel dat de afhankelijkheden zigzaggen tussen de klassen en modules. Maar hier eindigt het niet. U kunt procedures opslaan; PHP-code in SQL-tabellen.


In het bovenstaande schema retourneren query's naar de SQL-database PHP-code gegenereerd met gegevens uit de tabellen. Deze PHP-functies of klassen doen andere SQL-query's die verschillende PHP-code retourneren en de cyclus gaat door totdat uiteindelijk alle informatie is verkregen en geretourneerd ... waarschijnlijk naar de UI.

Ik weet dat dit voor veel van jullie misschien schandalig klinkt, maar als je nog niet hebt gewerkt met een project dat op deze manier is uitgevonden en geïmplementeerd, zul je dat zeker doen in je toekomstige carrière. De meeste bestaande projecten, ongeacht de gebruikte programmeertalen, zijn geschreven met oude principes in het achterhoofd, door programmeurs die het niet konden schelen of genoeg weten om het beter te doen. Als u deze zelfstudies leest, bent u waarschijnlijk een hoger niveau dan dat. Je bent klaar, of je klaarmaakt om je beroep te respecteren, om je ambacht te omhelzen en om het beter te doen.

De andere optie is om de fouten van je voorgangers te herhalen en te leven met de consequenties. Bij Syneto, nadat een van onze projecten een bijna onaantastbare staat bereikte vanwege zijn oude en kruisafhankelijke architectuur en we het eigenlijk voorgoed moesten verlaten, besloten we om nooit meer die weg terug te gaan. Sindsdien hebben we gestreefd naar een schone architectuur die de SOLID-principes correct respecteert, en het allerbelangrijkste is het principe van de afhankelijkheid van Inversie..


Wat zo verbazingwekkend is aan deze architectuur, is hoe de afhankelijkheden wijzen:

  • De gebruikersinterface (in de meeste gevallen een web MVC-raamwerk) of welk ander leveringsmechanisme er ook is voor uw project, is afhankelijk van de bedrijfslogica. Bedrijfslogica is vrij abstract. Een gebruikersinterface is heel concreet. De gebruikersinterface is slechts een detail voor het project en het is ook erg volatiel. Niets hangt af van de gebruikersinterface, niets zou afhangen van uw MVC-raamwerk.
  • De andere interessante observatie die we kunnen maken, is dat de persistentie, de database, uw MySQL of PostgreSQL afhankelijk is van de bedrijfslogica. Uw bedrijfslogica is database-agnostisch. Hiermee kunt u persistentie uitwisselen zoals u dat wilt. Als je morgen MySQL wilt veranderen met PostgreSQL of gewoon tekstbestanden, kun je dat doen. U zult natuurlijk een specifieke persistentielaag moeten implementeren voor de nieuwe persistentiemethode, maar u hoeft geen enkele regel code in uw bedrijfslogica te wijzigen. Er is een meer gedetailleerde uitleg over het persistentiethema in de evolutieve zelfstudie-tutorial.
  • Ten slotte, aan de rechterkant van de bedrijfslogica, daarbuiten, hebben we alle klassen die bedrijfslogicaklassen maken. Dit zijn fabrieken en klassen die zijn gemaakt door het beginpunt van onze toepassing. Veel mensen denken dat deze tot de bedrijfslogica behoren, maar terwijl ze bedrijfsobjecten maken, is hun enige reden om dit te doen. Het zijn lessen om ons te helpen bij het maken van andere klassen. De bedrijfsobjecten en de logica die ze bieden zijn onafhankelijk van deze fabrieken. We kunnen verschillende patronen gebruiken, zoals het maken van Simple Factory, Abstract Factory, Builder of gewoon object om de bedrijfslogica te bieden. Het maakt niet uit. Zodra de bedrijfsobjecten zijn gemaakt, kunnen ze hun werk doen.

Toon mij de code

Het toepassen van het Dependency Inversion Principle (DIP) op architectonisch niveau is vrij eenvoudig als je de klassieke behendige ontwerppatronen respecteert. Het oefenen en illustreren binnen de bedrijfslogica is vrij eenvoudig en kan zelfs leuk zijn. We stellen ons een e-book reader-applicatie voor.

klasse Test breidt uit PHPUnit_Framework_TestCase function testItCanReadAPDFBook () $ b = nieuwe PDFBook (); $ r = nieuwe PDFReader ($ b); $ this-> assertRegExp ('/ pdf book /', $ r-> read ());  class PDFReader private $ book; functie __construct (PDFBook $ boek) $ this-> book = $ book;  function read () return $ this-> book-> read ();  class PDFBook function read () return "het lezen van een pdf-boek."; 

We beginnen onze e-reader te ontwikkelen als een PDF-lezer. Tot zover goed. We hebben een PDF lezer klasse met behulp van a PDFBook. De lezen() functie op de lezer afgevaardigden naar het boek lezen() methode. We verifiëren dit door een regex-controle uit te voeren na een belangrijk deel van de string geretourneerd door PDFBook's lezer() methode.

Houd er rekening mee dat dit slechts een voorbeeld is. We zullen de leeslogica van PDF-bestanden of andere bestandsindelingen niet implementeren. Dat is de reden waarom onze tests eenvoudigweg controleren op enkele basisstrings. Als we de echte applicatie zouden schrijven, zou het enige verschil zijn hoe we de verschillende bestandsindelingen testen. De afhankelijkheidsstructuur lijkt erg op ons voorbeeld.


Het hebben van een PDF-lezer met een PDF-boek kan een goede oplossing zijn voor een beperkte applicatie. Als het de bedoeling was om een ​​PDF-reader te schrijven en niets meer, zou het eigenlijk een acceptabele oplossing zijn. Maar we willen een generieke e-book reader schrijven, die verschillende formaten ondersteunt, waaronder onze eerste geïmplementeerde versie-PDF. Laten we onze lezersklasse hernoemen.

klasse Test breidt uit PHPUnit_Framework_TestCase function testItCanReadAPDFBook () $ b = nieuwe PDFBook (); $ r = nieuwe EBookReader ($ b); $ this-> assertRegExp ('/ pdf book /', $ r-> read ());  class EBookReader private $ book; functie __construct (PDFBook $ boek) $ this-> book = $ book;  function read () return $ this-> book-> read ();  class PDFBook function read () return "het lezen van een pdf-boek."; 

Hernoemen had geen functionele tegeneffecten. De tests zijn nog steeds voorbij.

Testen begon om 1:04 PM ...
PHPUnit 3.7.28 door Sebastian Bergmann.
Tijd: 13 ms, geheugen: 2,50 MB
OK (1 test, 1 bewering)
Het proces is voltooid met exitcode 0

Maar het heeft een serieus ontwerpeffect.


Onze lezer werd veel abstracter. Veel algemener. We hebben een generiek Ebook lezer die een heel specifiek boektype gebruikt, PDFBook. Een abstractie is afhankelijk van een detail. Het feit dat ons boek van het type PDF is, zou slechts een detail moeten zijn en niemand zou hiervan afhankelijk moeten zijn.

klasse Test breidt uit PHPUnit_Framework_TestCase function testItCanReadAPDFBook () $ b = nieuwe PDFBook (); $ r = nieuwe EBookReader ($ b); $ this-> assertRegExp ('/ pdf book /', $ r-> read ());  interface EBook function read ();  class EBookReader private $ book; function __construct (EBook $ book) $ this-> book = $ book;  function read () return $ this-> book-> read ();  class PDFBook implementeert EBook function read () return "leest een pdf-boek."; 

De meest gebruikelijke en meest gebruikte oplossing om de afhankelijkheid om te keren, is om een ​​meer abstracte module in ons ontwerp te introduceren. "Het meest abstracte element in OOP is een interface, dus elke andere klasse kan afhankelijk zijn van een interface en toch DIP respecteren".

We hebben een interface gemaakt voor onze lezer. De interface wordt gebeld EBook en vertegenwoordigt de behoeften van de Ebook lezer. Dit is een direct gevolg van het respecteren van het Interface Segregation Principle (ISP), dat het idee bevordert dat interfaces de behoeften van de klanten moeten weerspiegelen. Interfaces behoren tot de clients en daarom zijn ze benoemd om de typen en objecten weer te geven die de clients nodig hebben en ze bevatten methoden die de clients willen gebruiken. Het is alleen natuurlijk voor een Ebook lezer gebruiken eBooks en heb een lezen() methode.


In plaats van één afhankelijkheid hebben we nu twee afhankelijkheden.

  • De eerste afhankelijkheid wijst van Ebook lezer naar de EBook interface en het is van het type gebruik. Ebook lezer toepassingen eBooks.
  • De tweede afhankelijkheid is anders. Het wijst van PDFBook naar hetzelfde toe EBook interface maar het is van het type implementatie. EEN PDFBook is gewoon een bepaalde vorm van EBook, en implementeert die interface dus om te voldoen aan de behoeften van de klant.

Het is niet verwonderlijk dat deze oplossing ons ook toestaat om verschillende soorten e-boeken aan te sluiten op onze reader. De enige voorwaarde voor al deze boeken is om te voldoen aan de EBook interface en implementeren.

klasse Test breidt uit PHPUnit_Framework_TestCase function testItCanReadAPDFBook () $ b = nieuwe PDFBook (); $ r = nieuwe EBookReader ($ b); $ this-> assertRegExp ('/ pdf book /', $ r-> read ());  function testItCanReadAMobiBook () $ b = nieuwe MobiBook (); $ r = nieuwe EBookReader ($ b); $ this-> assertRegExp ('/ mobi book /', $ r-> read ());  interface EBook function read ();  class EBookReader private $ book; function __construct (EBook $ book) $ this-> book = $ book;  function read () return $ this-> book-> read ();  class PDFBook implementeert EBook function read () return "leest een pdf-boek.";  klasse MobiBook implementeert EBook function read () return "leest een mobi-boek."; 

Wat ons op zijn beurt naar het Open / Gesloten principe leidt, en de cirkel is gesloten.

Het principe van de afhankelijkheid van Inversie is er een die ons leidt of helpt om alle andere principes te respecteren. Het respecteren van DIP zal:

  • Je bijna dwingen OCP te respecteren.
  • Sta je toe om verantwoordelijkheden te scheiden.
  • Maak je correct gebruik van subtypen.
  • Bied je de mogelijkheid om je interfaces te scheiden.

Laatste gedachten

Dat is het. We zijn klaar. Alle tutorials over de SOLID-principes zijn voltooid. Voor mij persoonlijk was het ontdekken van deze principes en het uitvoeren van projecten met hen in gedachten een enorme verandering. Ik heb de manier waarop ik over design en architectuur denk helemaal veranderd en ik kan zeggen dat sindsdien alle projecten waar ik aan werk exponentieel eenvoudiger te beheren en te begrijpen zijn.

Ik beschouw de SOLID-principes als een van de meest essentiële concepten van objectgericht ontwerpen. Deze concepten die ons moeten helpen onze code beter te maken en ons leven als programmeurs veel gemakkelijker te maken. Goed ontworpen code is voor programmeurs gemakkelijker te begrijpen. Computers zijn slim, ze kunnen de code begrijpen, ongeacht de complexiteit ervan. Mensen hebben daarentegen een beperkt aantal dingen die ze kunnen bewaren in hun actieve, gerichte geest. Meer specifiek, het aantal van zulke dingen is The Magical Number Seven, Plus of Minus Two.

We moeten ernaar streven om onze code rond deze cijfers te laten structureren en er zijn verschillende technieken die ons daarbij helpen. Functies met een maximum van vier regels in lengte (vijf met de definitieregel inbegrepen), zodat ze allemaal tegelijk in onze geest kunnen passen. Inkepingen die niet vijf niveaus diep passeren. Klassen met niet meer dan negen methoden. Ontwerp patronen die meestal een aantal van vijf tot negen klassen gebruiken. Ons ontwerp op hoog niveau in de bovenstaande schema's gebruikt vier tot vijf concepten. Er zijn vijf SOLID-principes, die elk vijf tot negen subconcepten / modules / klassen moeten bevatten. De ideale grootte van een programmeerteam is tussen vijf en negen. Het ideale aantal teams in een bedrijf ligt tussen de vijf en negen.

Zoals je ziet, is het magische getal zeven, plus of min twee overal om ons heen, dus waarom zou jouw code anders zijn??