SOLID Del 4 - Principen om beroendeinversion

Det enda ansvaret (SRP), Open / Closed (OCP), Liskov-substitution, gränssnittssegregering och Dependensinversion. Fem smidiga principer som bör styra dig varje gång du skriver kod.

Det skulle vara orättvist att säga att någon av SOLID-principerna är viktigare än en annan. Förmodligen har ingen av de andra en sådan omedelbar och djupgående effekt på din kod än beroendeprincipen, eller DIP i korthet. Om du hittar de andra principerna svårt att förstå eller tillämpa, börja med den här och använd resten på kod som redan respekterar DIP.

Definition

A. Högnivåmoduler bör inte bero på lågnivåmoduler. Båda borde bero på abstraktioner.
B. Abstraktioner bör inte bero på detaljer. Detaljer bör bero på abstraktioner.

Denna princip definierades av Robert C. Martin i sin bok Agile Software Development, Principles, Patterns, and Practices och senare publicerades i C # versionen av boken Agile Principles, Patterns, and Practices i C #, och det är den sista av de fem SOLID agila principer.

DIP i den verkliga världen

Innan vi börjar kodning skulle jag vilja berätta en historia. På Syneto var vi inte alltid så försiktiga med vår kod. För några år sedan visste vi mindre och trots att vi försökte göra vårt bästa, var inte alla våra projekt så trevliga. Vi gick igenom helvetet och tillbaka igen och vi lärde oss många saker genom försök och fel.

SOLID-principerna och de rena arkitekturprinciperna för Uncle Bob (Robert C. Martin) blev en spelväxlare för oss och förvandlade vårt sätt att koda på sätt som är svåra att beskriva. Jag kommer att försöka exemplifiera, i ett nötskal, några viktiga arkitektoniska beslut som infördes av DIP, som hade stor inverkan på våra projekt.

De flesta webbprojekt innehåller tre huvudteknologier: HTML, PHP och SQL. Den speciella versionen av dessa applikationer vi pratar om eller vilken typ av SQL-implementeringar du använder är irrelevant. Saken är att informationen från en HTML-blankett måste hamna på ett eller annat sätt i databasen. Limet mellan de två kan förses med PHP.

Vad som är väsentligt att ta bort från detta är det hur trevligt de tre teknikerna representerar tre olika arkitektoniska lager: användargränssnitt, affärslogik och uthållighet. Vi kommer att prata om konsekvenserna av dessa lager på en minut. För nu låt oss fokusera på några udda men ofta uppkomna lösningar för att tekniken ska fungera tillsammans.

Många gånger har jag sett projekt som använde SQL-kod i en PHP-tagg inuti en HTML-fil, eller PHP-kod som ekkar sidor och sidor med HTML och tolkar direkt $ _GET eller $ _POST globala variabler. Men varför är det dåligt?


Bilderna ovan representerar en rå version av det vi beskrev i föregående stycke. Pilarna representerar olika beroenden, och som vi kan dra slutsatsen beror allting på allt. Om vi ​​behöver ändra en databas tabell kan vi sluta redigera en HTML-fil. Eller om vi ändrar ett fält i HTML, kan vi sluta ändra namnet på en kolumn i ett SQL-uttalande. Eller om vi tittar på det andra schemat kan vi mycket väl behöva ändra vårt PHP om HTML ändras eller i mycket dåliga fall när vi genererar allt HTML-innehåll från en PHP-fil, så behöver vi säkert ändra en PHP-fil till Ändra HTML-innehåll. Så det finns ingen tvekan, beroendet är zigzagging mellan klasserna och modulerna. Men det slutar inte här. Du kan lagra procedurer; PHP-kod i SQL-tabeller.


I schemat ovan returnerar frågor till SQL-databasen PHP-kod som genereras med data från tabellerna. Dessa PHP-funktioner eller -klasser gör andra SQL-frågor som returnerar olika PHP-kod, och cykeln fortsätter tills äntligen all information erhålls och returneras ... förmodligen till gränssnittet.

Jag vet att det här kan vara skandalöst för många av er, men om du inte har arbetat med ett projekt som uppfunnits och implementerats på detta sätt kommer du säkert i din framtida karriär. De flesta befintliga projekt, oavsett vilka programmeringsspråk som användes, skrevs med gamla principer i åtanke, av programmerare som inte bryr sig eller vet tillräckligt för att göra bättre. Om du läser dessa handledningar är du troligtvis en nivå högre än den. Du är redo eller redo att respektera ditt yrke, att omfamna ditt hantverk och att göra det bättre.

Det andra alternativet är att upprepa de misstag som dina föregångare gjort och leva med konsekvenserna. Vid Syneto, efter att ett av våra projekt nådde ett nästan oförlåtligt tillstånd på grund av sin gamla och korsberoende arkitektur och vi var tvungna att överge det för alltid bestämde vi oss för att aldrig gå tillbaka ner den vägen igen. Sedan dess har vi strävat efter att ha en ren arkitektur som korrekt respekterar SOLID-principerna, och viktigast av principen om beroendeinversion.


Vad som är så fantastiskt om denna arkitektur är hur beroenden pekar:

  • Användargränssnittet (i de flesta fall en web-MVC-ram) eller vilken annan leveransmekanism som finns för ditt projekt beror på affärslogiken. Affärslogiken är ganska abstrakt. Ett användargränssnitt är mycket konkret. UI är bara en detalj för projektet, och det är också mycket volatilt. Ingenting borde vara beroende av användargränssnittet, inget bör bero på din MVC-ram.
  • Den andra intressanta observationen vi kan göra är att persistensen, databasen, MySQL eller PostgreSQL beror på affärslogiken. Din affärslogik är databas agnostic. Detta möjliggör utbyte av uthållighet som du önskar. Om du i morgon vill ändra MySQL med PostgreSQL eller bara vanliga textfiler kan du göra det. Du kommer självklart att behöva genomföra ett specifikt persistenslager för den nya persistensmetoden, men du behöver inte ändra en enda kodrad i din affärslogik. Det finns en mer detaljerad förklaring om persistensämnet i handledningen om evolving mot en persistenslager.
  • Slutligen, till höger om affärslogiken, utanför det, har vi alla klasser som skapar business logic classes. Det här är fabriker och klasser som skapats av ingången till vår ansökan. Många människor tenderar att tro att dessa tillhör affärslogiken, men medan de skapar affärsobjekt är deras enda anledning att göra detta. De är klasser bara för att hjälpa oss att skapa andra klasser. De affärsobjekt och logiken de tillhandahåller är oberoende av dessa fabriker. Vi kunde använda olika mönster, som Simple Factory, Abstract Factory, Builder eller skapandet av objekt för att tillhandahålla affärslogiken. Det spelar ingen roll. När affärsobjekten är skapade kan de göra sitt jobb.

Visa mig koden

Att tillämpa Dependency Inversion Principle (DIP) på arkitektonisk nivå är ganska lätt om du respekterar de klassiska smidiga designmönstren. Att utöva och exemplifiera det inom affärslogiken är ganska enkelt också och kan till och med vara roligt. Vi kommer att föreställa oss en e-bokläsareansökan.

klasstest utökar PHPUnit_Framework_TestCase funktion testItCanReadAPDFBook () $ b = ny PDFBook (); $ r = ny PDFReader ($ b); $ this-> assertRegExp ('/ pdf book /', $ r-> read ());  klass PDFReader privat $ bok; funktion __construct (PDFBook $ bok) $ this-> book = $ book;  funktionsläsning () returnera $ this-> book-> read ();  klass PDFBook funktion läs () returnera "läser en pdf-bok."; 

Vi börjar utveckla vår e-läsare som PDF-läsare. Än så länge är allt bra. Vi har en PDFReader klass med hjälp av a PDFBook. De läsa() funktionen på läsarnas delegater till bokens läsa() metod. Vi bekräftar bara detta genom att göra en regexkontroll efter en nyckeldel av strängen som returneras av PDFBook's läsare() metod.

Tänk på att detta bara är ett exempel. Vi kommer inte att genomföra läslogiken för PDF-filer eller andra filformat. Därför kommer våra tester bara att leta efter några grundläggande strängar. Om vi ​​skulle skriva den verkliga applikationen skulle den enda skillnaden vara hur vi testar de olika filformat. Beroende strukturen skulle mycket likna vårt exempel.


Att ha en PDF-läsare med en PDF-bok kan vara en ljudlösning för en begränsad applikation. Om vårt räckvidd var att skriva en PDF-läsare och ingenting mer, skulle det faktiskt vara en acceptabel lösning. Men vi vill skriva en generisk e-bokläsare, som stöder flera format, bland annat vår första implementerade version PDF. Låt oss byta namn på vår läsarklass.

klasstest utökar PHPUnit_Framework_TestCase funktion testItCanReadAPDFBook () $ b = ny PDFBook (); $ r = ny EBookReader ($ b); $ this-> assertRegExp ('/ pdf book /', $ r-> read ());  klass EBookReader privat $ bok; funktion __construct (PDFBook $ bok) $ this-> book = $ book;  funktionsläsning () returnera $ this-> book-> read ();  klass PDFBook funktion läs () returnera "läser en pdf-bok."; 

Renaming hade inga funktionella motverkaffekter. Testerna passerar fortfarande.

Testningen började klockan 13:04 ...
PHPUnit 3.7.28 av Sebastian Bergmann.
Tid: 13 ms, Minne: 2,50Mb
OK (1 test, 1 påstående)
Processen avslutad med utgående kod 0

Men det har en seriös designeffekt.


Vår läsare blev mycket mer abstrakt. Mycket mer generellt. Vi har en generisk Eboksläsare som använder en mycket specifik bok typ, PDFBook. En abstraktion beror på en detalj. Det faktum att vår bok är av typ PDF bör bara vara en detalj, och ingen ska bero på den.

klasstest utökar PHPUnit_Framework_TestCase funktion testItCanReadAPDFBook () $ b = ny PDFBook (); $ r = ny EBookReader ($ b); $ this-> assertRegExp ('/ pdf book /', $ r-> read ());  gränssnittet EBook funktion läs ();  klass EBookReader privat $ bok; funktion __construct (eBook $ book) $ this-> book = $ book;  funktionsläsning () returnera $ this-> book-> read ();  klass PDFBook implementerar EBook funktion läsning () return läser en pdf-bok. "; 

Den vanligaste och mest använda lösningen för att invertera beroendet är att införa en mer abstrakt modul i vår design. "Det mest abstrakta elementet i OOP är ett gränssnitt. Således kan någon annan klass bero på ett gränssnitt och respekterar fortfarande DIP".

Vi skapade ett gränssnitt för vår läsare. Gränssnittet heter EBook och representerar behoven hos Eboksläsare. Detta är ett direkt resultat av att respektera gränssnittssegregationsprincipen (ISP) som främjar idén om att gränssnitt ska återspegla kundernas behov. Gränssnitt tillhör kunderna, och de namnges därför för att spegla de typer och objekt som kunderna behöver och de kommer att innehålla metoder som kunderna vill använda. Det är bara naturligt för en Eboksläsare att använda e-böcker och ha en läsa() metod.


Istället för ett enda beroende har vi två beroenden nu.

  • De första beroendepunkterna från Eboksläsare mot EBook gränssnittet och det är av typanvändning. Eboksläsare användningar e-böcker.
  • Det andra beroendet är annorlunda. Det pekar ut från PDFBook mot samma EBook gränssnitt men det är av typ implementering. en PDFBook är bara en särskild form av EBook, och implementerar sålunda det gränssnittet för att tillgodose kundens behov.

Otroligt, med den här lösningen kan vi också plugga in olika typer av e-böcker i vår läsare. Det enda villkoret för alla dessa böcker är att tillfredsställa EBook gränssnitt och implementera det.

klasstest utökar PHPUnit_Framework_TestCase funktion testItCanReadAPDFBook () $ b = ny PDFBook (); $ r = ny EBookReader ($ b); $ this-> assertRegExp ('/ pdf book /', $ r-> read ());  funktion testItCanReadAMobiBook () $ b = nya MobiBook (); $ r = ny EBookReader ($ b); $ this-> assertRegExp ('/ mobi book /', $ r-> read ());  gränssnittet EBook funktion läs ();  klass EBookReader privat $ bok; funktion __construct (eBook $ book) $ this-> book = $ book;  funktionsläsning () returnera $ this-> book-> read ();  klass PDFBook implementerar EBook funktion läsning () return läser en pdf-bok. ";  klass MobiBook implementerar EBook funktion läsning () returnera "läser en mobibok."; 

Som i sin tur leder oss till det öppna / stängda principen, och cirkeln är stängd.

Dependency Inversion Principle är en som leder eller hjälper oss att respektera alla andra principer. Respektera DIP kommer att:

  • Nästan tvingar dig att respektera OCP.
  • Låt dig skilja ansvar.
  • Gör det rätt att använda subtyping.
  • Erbjuder dig möjlighet att segregera dina gränssnitt.

Slutgiltiga tankar

Det är allt. Vi är klara. Alla handledning om SOLID-principerna är kompletta. För mig personligen upptäckte dessa principer och genomförande av projekt med dem i åtanke en enorm förändring. Jag har helt förändrat hur jag tänker på design och arkitektur och jag kan säga sedan dess är alla projekt jag arbetar på exponentiellt lättare att hantera och förstå.

Jag anser att SOLID-principerna är en av de viktigaste begreppen objektorienterad design. Dessa begrepp som måste vägleda oss för att göra vår kod bättre och vårt liv som programmerare mycket enklare. Väl utformad kod är lättare för programmerare att förstå. Datorer är smarta, de kan förstå kod oavsett komplexitet. Människor har å andra sidan ett begränsat antal saker de kan behålla i sitt aktiva, fokuserade sinne. Mer specifikt är antalet sådana saker The Magical Number Seven, Plus eller Minus Two.

Vi bör sträva efter att ha vår kod strukturerade runt dessa nummer och det finns flera tekniker som hjälper oss att göra det. Funktioner med högst fyra linjer i längd (fem med definitionslinjen medföljer) så att de alla kan passa på en gång i vårt sinne. Indryckningar som inte passerar fem nivåer djupt. Klasser med högst nio metoder. Designmönster som brukar använda ett antal fem till nio klasser. Vår högkvalitativa design i scheman ovan använder fyra till fem koncept. Det finns fem SOLID-principer, var och en kräver fem till nio delkoncept / moduler / klasser som exemplifieras. Den ideala storleken på ett programmeringslag är mellan fem och nio. Det ideala antalet lag i ett företag är mellan fem och nio.

Som du kan se är det magiska sju, plus eller minus två runt oss, så varför skulle din kod vara annorlunda?