SOLID Partea 4 - Principiul inversării dependenței

Responsabilitatea unică (SRP), Deschis / Închis (OCP), Înlocuirea lui Liskov, Segregarea interfeței și Inversiunea dependenței. Cinci principii agile care ar trebui să vă ghideze de fiecare dată când scrieți cod.

Ar fi nedrept să vă spun că oricare dintre principiile SOLID este mai important decât altul. Cu toate acestea, probabil nici unul dintre celelalte nu are un efect atât de imediat și profund asupra codului dvs. decât Principiul inversării dependenței, sau DIP pe scurt. Dacă găsiți celelalte principii greu de înțeles sau aplicați, începeți cu aceasta și aplicați restul codului care respectă deja DIP.

Definiție

Modulele de nivel înalt nu ar trebui să depindă de modulele de nivel inferior. Ambele ar trebui să depindă de abstractizări.
B. Abstracțiile nu trebuie să depindă de detalii. Detaliile ar trebui să depindă de abstracții.

Acest principiu a fost definit de Robert C. Martin în cartea sa Agile Software Development, Principles, Patterns and Practices și ulterior republicată în versiunea C # a cărții Agile Principles, Patterns and Practices în C #, și este ultima dintre cele cinci Principii agile solide.

DIP în lumea reală

Înainte de a începe codificarea, aș vrea să vă spun o poveste. La Syneto, nu am fost întotdeauna atât de atent cu codul nostru. Acum câțiva ani am știut mai puțin și chiar dacă am încercat să facem tot ce ne stă în putință, nu toate proiectele noastre au fost atât de drăguțe. Am trecut prin iad și înapoi și am învățat multe lucruri prin încercări și erori.

Principiile SOLID și principiile de arhitectură curată ale unchiului Bob (Robert C. Martin) au devenit un schimbător de joc pentru noi și au transformat modul nostru de codificare în moduri greu de descris. Voi încerca să exemplific, pe scurt, câteva decizii cheie de arhitectură impuse de DIP care au avut un mare impact asupra proiectelor noastre.

Majoritatea proiectelor web conțin trei tehnologii principale: HTML, PHP și SQL. Versiunea specială a acestor aplicații despre care vorbim sau ce tip de implementări SQL utilizați este irelevantă. Chestia este că informația dintr-un formular HTML trebuie să se termine, într-un fel sau altul, în baza de date. Adezivul dintre cele două poate fi furnizat cu PHP.

Ceea ce este esențial pentru a scăpa de acest lucru este că modul în care cele trei tehnologii reprezintă trei straturi arhitecturale diferite: interfața cu utilizatorul, logica de afaceri și persistența. Vom vorbi despre implicațiile acestor straturi într-un minut. Pentru moment, să ne concentrăm asupra unor soluții ciudate, dar întâlnite frecvent, pentru ca tehnologiile să funcționeze împreună.

De multe ori am văzut proiecte care utilizează codul SQL într-o etichetă PHP în interiorul unui fișier HTML, sau cod PHP care echivă paginile și paginile HTML și interpretează direct $ _GET sau $ _POST variabile globale. Dar de ce e rău?


Imaginile de mai sus reprezintă o versiune primară a ceea ce am descris în paragraful anterior. Săgețile reprezintă diferite dependențe și, după cum putem concluziona, practic totul depinde de tot. Dacă trebuie să schimbăm o tabelă de baze de date, este posibil să terminăm editarea unui fișier HTML. Sau dacă schimbăm un câmp în HTML, este posibil să schimbăm numele unei coloane într-o instrucțiune SQL. Sau dacă ne uităm la cea de-a doua schemă, este posibil să avem nevoie să modificăm PHP în cazul în care modificările HTML, sau în cazuri foarte proaste, când vom genera tot conținutul HTML din interiorul unui fișier PHP, cu siguranță va trebui să schimbăm un fișier PHP modificați conținutul HTML. Deci, nu există nici o îndoială, dependențele sunt zigzagging între clase și module. Dar nu se termină aici. Puteți stoca procedurile; Cod PHP în tabele SQL.


În schema de mai sus, interogările la baza de date SQL returnează codul PHP generat cu datele din tabele. Aceste funcții sau clase PHP fac alte interogări SQL care returnează cod PHP diferit, iar ciclul continuă până când toate informațiile sunt obținute și returnate ... probabil către interfața de utilizare.

Știu că s-ar părea scandalos pentru mulți dintre voi, dar dacă nu ați lucrat încă cu un proiect inventat și implementat în acest mod, cu siguranța veți avea în cariera viitoare. Cele mai multe proiecte existente, indiferent de limbile de programare folosite, au fost scrise cu principii vechi, de către programatori care nu au avut grijă și nu știu suficient pentru a face mai bine. Dacă citiți aceste tutoriale, sunteți cel mai probabil un nivel mai mare decât acesta. Sunteți pregătiți să vă respectați meseria, să vă îmbrățișați meseria și să faceți mai bine.

Cealaltă opțiune este să repetați greșelile făcute de predecesorii voștri și să trăiți cu consecințele. La Syneto, după ce unul dintre proiectele noastre a ajuns într-o stare aproape imposibil de întreținut datorită arhitecturii vechi și transversale dependente și trebuia să o abandonăm pentru totdeauna, am decis să nu mai revenim din nou pe acel drum. De atunci, ne-am străduit să avem o arhitectură curată, care să respecte în mod corect principiile SOLID și, cel mai important, principiul inversiunii dependenței.


Ceea ce este atât de uimitor în privința acestei arhitecturi este modul în care se îndreaptă dependențele:

  • Interfața cu utilizatorul (în majoritatea cazurilor un cadru web MVC) sau orice alt mecanism de livrare există pentru proiectul dvs. va depinde de logica de afaceri. Logica de afaceri este destul de abstractă. O interfață cu utilizatorul este foarte concretă. UI este doar un detaliu al proiectului și este, de asemenea, foarte volatilă. Nimic nu trebuie să depindă de UI, nimic nu ar trebui să depindă de cadrul MVC.
  • O altă observație interesantă pe care o putem face este că persistența, baza de date, MySQL sau PostgreSQL depinde de logica de afaceri. Logica dvs. de afaceri este baza de date agnostică. Acest lucru permite schimbul de persistență după cum doriți. Dacă mâine vreți să schimbați MySQL cu PostgreSQL sau doar fișiere text simplu, puteți face acest lucru. Desigur, va trebui să implementați un strat specific de persistență pentru noua metodă de persistență, dar nu va trebui să modificați o singură linie de cod în logica dvs. de afaceri. Există o explicație mai detaliată cu privire la subiectul persistenței din tutorialul Evolving Towards a Persistence Layer.
  • În cele din urmă, în dreapta logicii de afaceri, în afara acesteia, avem toate clasele care creează clase de logică de afaceri. Acestea sunt fabrici și clase create de punctul de intrare la cererea noastră. Mulți oameni tind să creadă că acestea aparțin logicii de afaceri, dar în timp ce ei creează obiecte de afaceri, singurul lor motiv este să facă acest lucru. Sunt clase doar pentru a ne ajuta să creăm alte clase. Obiectele de afaceri și logica pe care o furnizează sunt independente de aceste fabrici. Am putea folosi modele diferite, cum ar fi Simple Factory, Abstract Factory, Builder sau crearea obiectului simplu pentru a oferi logica de afaceri. Nu contează. Odată ce obiectele de afaceri sunt create, ei își pot face treaba.

Arată-mi codul

Aplicarea principiului inversiunii dependenței (DIP) la nivel arhitectural este destul de ușoară dacă respectați modelele clasice de design agile. Exercitarea și exemplificarea în interiorul logicii de afaceri este destul de ușoară și poate fi chiar distractivă. Ne vom imagina o aplicație cititor de cărți electronice.

testul de clasă extinde PHPUnit_Framework_TestCase function testItCanReadAPDFBook () $ b = newBookBook (); $ r = nou PDFReader ($ b); $ this-> assertRegExp ('/ pdf carte /', $ r-> citi ());  clasa PDFReader private $ book; funcția __construct (PDFbookbook book) $ this-> book = $ book;  function read () return $ this-> book-> read ();  clasa PDFBook function read () return "citind o carte pdf."; 

Începem să dezvoltăm cititorul nostru electronic ca cititor PDF. Până acum, bine. Noi avem un PDFReader clasa folosind a PDFBook. citit() funcție pe cititor delegat la cartea lui citit() metodă. Verificăm acest lucru efectuând o verificare regex după o parte cheie a șirului returnat PDFBook„s cititor() metodă.

Rețineți că acesta este doar un exemplu. Nu vom implementa logica de citire a fișierelor PDF sau a altor formate de fișiere. De aceea, testele noastre vor verifica pur și simplu câteva șiruri de bază. Dacă ar fi să scriem aplicația reală, singura diferență ar fi aceea de a testa diferitele formate de fișiere. Structura dependenței ar fi foarte asemănătoare cu exemplul nostru.


Utilizarea unui cititor PDF utilizând o carte PDF poate fi o soluție solidă pentru o aplicație limitată. Dacă scopul nostru ar fi să scriem un cititor PDF și nimic mai mult, ar fi de fapt o soluție acceptabilă. Dar vrem să scriem un cititor de e-book generic, care să susțină mai multe formate, printre care și cea mai nouă versiune implementată PDF. Să redenumim clasa cititorilor noștri.

testul de clasă extinde PHPUnit_Framework_TestCase function testItCanReadAPDFBook () $ b = newBookBook (); $ r = noul EBookReader ($ b); $ this-> assertRegExp ('/ pdf carte /', $ r-> citi ());  clasa EBookReader private $ book; funcția __construct (PDFbookbook book) $ this-> book = $ book;  function read () return $ this-> book-> read ();  clasa PDFBook function read () return "citind o carte pdf."; 

Redenumirea nu a avut efecte contrare funcționale. Testele încă mai trec.

Testarea a început la 1:04 PM ...
PHPUnit 3.7.28 de Sebastian Bergmann.
Timp: 13 ms, Memorie: 2.50Mb
OK (1 test, 1 afirmație)
Procesul a terminat cu codul de iesire 0

Dar are un efect serios de design.


Cititorul nostru a devenit mult mai abstract. Mult mai general. Avem o generică EBookReader care utilizează un tip de carte foarte specific, PDFBook. O abstracție depinde de un detaliu. Faptul că cartea noastră este de tip PDF ar trebui să fie doar un detaliu și nimeni nu ar trebui să depindă de ea.

testul de clasă extinde PHPUnit_Framework_TestCase function testItCanReadAPDFBook () $ b = newBookBook (); $ r = noul EBookReader ($ b); $ this-> assertRegExp ('/ pdf carte /', $ r-> citi ());  interfață EBook function read ();  clasa EBookReader private $ book; funcția __construct (EBook $ book) $ this-> book = $ book;  function read () return $ this-> book-> read ();  clasa PDFBook implementează EBook function read () return "citind o carte pdf."; 

Soluția cea mai comună și cea mai frecvent utilizată pentru a inversa dependența este de a introduce un modul mai abstract în designul nostru. "Cel mai abstract element din OOP este o interfață. Astfel, orice altă clasă poate depinde de o interfață și încă respectă DIP".

Am creat o interfață pentru cititorul nostru. Se numește interfața EBook și reprezintă nevoile EBookReader. Acesta este un rezultat direct al respectării Principiului de Segregare a Interfeței (ISP) care promovează ideea că interfețele trebuie să reflecte nevoile clienților. Interfețele aparțin clienților și astfel sunt numiți pentru a reflecta tipurile și obiectele de care au nevoie clienții și vor conține metode pe care clienții doresc să le utilizeze. Este firesc doar pentru o EBookReader a folosi ebooks și au un citit() metodă.


În loc de o singură dependență, acum avem două dependențe.

  • Primele puncte de dependență de la EBookReader spre EBook interfață și este de tip de utilizare. EBookReader utilizări ebooks.
  • A doua dependență este diferită. Ea arată de la PDFBook spre aceleași EBook dar este de tip implementare. A PDFBook este doar o formă particulară de EBook, și astfel implementează acea interfață pentru a satisface nevoile clientului.

În mod surprinzător, această soluție ne permite, de asemenea, să conectăm în cititorii noștri diferite tipuri de cărți electronice. Singura condiție pentru toate aceste cărți este de a satisface EBook și implementați-o.

testul de clasă extinde PHPUnit_Framework_TestCase function testItCanReadAPDFBook () $ b = newBookBook (); $ r = noul EBookReader ($ b); $ this-> assertRegExp ('/ pdf carte /', $ r-> citi ());  funcția testItCanReadAMobiBook () $ b = nou MobiBook (); $ r = noul EBookReader ($ b); $ this-> assertRegExp ('/ mobi carte /', $ r-> citi ());  interfață EBook function read ();  clasa EBookReader private $ book; funcția __construct (EBook $ book) $ this-> book = $ book;  function read () return $ this-> book-> read ();  clasa PDFBook implementează EBook function read () return "citind o carte pdf.";  clasa MobiBook implementează EBook function read () retur "citind o carte mobi."; 

Care, la rândul nostru, ne conduce la principiul Open / Closed, iar cercul este închis.

Principiul Inversiunii Dependenței este unul care ne conduce sau ne ajută să respectăm toate celelalte principii. Respectarea DIP va:

  • Aproape vă forțați să respectați OCP.
  • Permiteți-vă să vă separați responsabilitățile.
  • Efectuați corect utilizarea subtipului.
  • Oferiți-vă oportunitatea de a vă separa interfețele.

Gândurile finale

Asta e. Am terminat. Toate tutoriale despre principiile SOLID sunt complete. Pentru mine, descoperirea personală a acestor principii și implementarea proiectelor cu acestea în minte a fost o schimbare uriașă. Am schimbat complet modul în care mă gândesc la design și arhitectură și pot spune că de atunci toate proiectele pe care lucrez sunt mai ușor de gestionat și înțeleg în mod exponențial.

Consider că principiile SOLID reprezintă unul dintre cele mai esențiale concepte ale designului orientat pe obiecte. Aceste concepte care trebuie să ne ghideze în îmbunătățirea codului nostru și în viața noastră ca programatori sunt mult mai ușoare. Codul bine conceput este mai ușor pentru programatori să înțeleagă. Computerele sunt inteligente, pot înțelege codul, indiferent de complexitatea sa. Ființele umane, pe de altă parte, au un număr limitat de lucruri pe care le pot păstra în mintea lor activă, concentrată. Mai exact, numărul de astfel de lucruri este Numărul Magic Șapte, Plus sau Minus Două.

Ar trebui să ne străduim să avem codul nostru structurat în jurul acestor numere și există mai multe tehnici care ne ajută să facem acest lucru. Funcții cu o lungime maximă de patru linii (cinci cu linia de definiție inclusă), astfel încât acestea să se potrivească imediat în mintea noastră. Indentări care nu trec de cinci niveluri profunde. Clase cu cel mult nouă metode. Modele de design care folosesc de obicei un număr de cinci până la nouă clase. Designul nostru la nivel înalt în schemele de mai sus utilizează patru până la cinci concepte. Există cinci principii SOLID, fiecare necesitând exemple de cinci până la nouă sub-concepte / module / clase. Mărimea ideală a unei echipe de programare este între cinci și nouă. Numărul ideal de echipe dintr-o companie este între cinci și nouă.

După cum puteți vedea, numărul magic șapte, plus sau minus doi este în jurul nostru, deci de ce ar trebui să vă diferiți codul?

Cod