Principiul inversării dependenței este implementat folosind. Principiul inversării dependenței. Arhitectură stratificată cu inversare a dependenței


DISCLAIMER: Autorul acestui articol nu are niciun scop să submineze autoritatea sau să ofenseze în vreun fel un astfel de tovarăș respectat precum „unchiul” Bob Martin. Este mai mult să te gândești mai atent la principiul inversării dependenței și să analizezi exemplele folosite pentru a-l descrie.

Pe parcursul articolului, voi oferi toate citatele și exemplele necesare din sursele menționate mai sus. Dar pentru a evita „spoilerele” și opinia ta rămâne obiectivă, aș recomanda să petreci 10-15 minute și să citești descrierea originală a acestui principiu într-un articol sau carte.

Principiul inversării dependenței decurge astfel:

A. Modulele de nivel superior nu ar trebui să depindă de modulele de nivel inferior. Ambele trebuie să depindă de abstracții.
B. Abstracțiile nu ar trebui să depindă de detalii. Detaliile trebuie să depindă de abstracții.
Să începem cu primul punct.

Stratificare

Ceapa are straturi, prăjiturile au straturi, canibalii au straturi, iar sistemele software au și ele! – Shrek (c)
Orice sistem complex este ierarhic: fiecare strat este construit pe baza unui strat dovedit și funcțional de un nivel inferior. Acest lucru vă permite să vă concentrați pe un set limitat de concepte la un moment dat, fără să vă gândiți la modul în care sunt implementate straturile de bază.
Ca rezultat, obținem ceva de genul următoarei diagrame:

Figura 1 – Schema de stratificare „naivă”.

Din punctul de vedere al lui Bob Martin, o astfel de schemă de împărțire a sistemului în straturi este naiv. Dezavantajul acestui design este „trăsătura insidioasă: stratul Politică depinde de schimbările din toate straturile pe drum spre Utilitate. Această dependență este tranzitivă.» .

Hmm... O afirmație foarte neobișnuită. Dacă vorbim despre platforma .NET, atunci dependența va fi tranzitivă doar dacă modulul curent „expune” module de niveluri inferioare în interfața sa publică. Cu alte cuvinte, dacă în MecanismStrat există o clasă publică care ia ca argument o instanță StringUtil(din UtilitateStrat), apoi toți clienții nivelului MecanismStrat deveni dependent de UtilitateStrat. În caz contrar, nu există tranzitivitatea modificărilor: toate modificările de nivel inferior sunt limitate la nivelul curent și nu se propagă mai sus.

Pentru a înțelege ideea lui Bob Martin, trebuie să rețineți că principiul inversării dependenței a fost descris pentru prima dată în 1996, iar limbajul C++ a fost folosit ca exemple. În articolul original, autorul însuși scrie că problema tranzitivității există doar în limbi fără o separare clară a interfeței de clasă de implementare. În C++, problema dependențelor tranzitive este într-adevăr relevantă: dacă un fișier PolicyLayer. h include prin directiva „include”. Stratul de mecanism. h, care la rândul său include UtilityLayer. h, apoi pentru orice modificare a fișierului antet UtilityLayer. h(chiar și în secțiunea „închisă” a claselor declarate în acest fișier) va trebui să recompilăm și să redistribuim toți clienții. Cu toate acestea, în C++ această problemă este rezolvată prin utilizarea idiomului PIml, propus de Herb Sutter și acum, de asemenea, nu atât de relevant.

Soluția la această problemă din punctul de vedere al lui Bob Martin este următoarea:

„Layer-ul de nivel superior declară o interfață abstractă pentru serviciile de care are nevoie. Straturile inferioare sunt apoi implementate pentru a satisface aceste interfețe. Orice clasă situată la nivelul superior accesează stratul stratului de lângă acesta printr-o interfață abstractă. Astfel, straturile superioare sunt independente de cele inferioare. Dimpotrivă, straturile inferioare depind de interfața abstractă a serviciilor, a anunţat la un nivel superior... Astfel, inversând dependențele, am creat o structură care este în același timp mai flexibilă, mai durabilă și mai mobilă



Figura 2 – Straturi inversate

În anumite privințe, această împărțire are sens. Deci, de exemplu, atunci când se folosește modelul observator, obiectul observat (observabil) este cel care definește interfața pentru interacțiunea cu lumea exterioară, astfel încât nicio modificare externă nu o poate afecta.

Dar, pe de altă parte, când vine vorba în mod specific de straturi, care sunt de obicei reprezentate ca ansambluri (sau pachete în termeni UML), abordarea propusă cu greu poate fi numită viabilă. Prin definiție, clasele de ajutor de nivel inferior sunt utilizate într-o duzină de module diferite de nivel superior. Stratul utilitar va fi folosit nu numai în Stratul de mecanism, dar și în Stratul de acces la date, Stratul de transport, Un alt strat. Ar trebui să implementeze apoi interfețele definite în toate modulele de nivel superior?

Evident, această soluție nu este ideală, mai ales că rezolvăm o problemă care nu există pe multe platforme, precum .NET sau Java.

Conceptul de abstractizare

Mulți termeni devin atât de înrădăcinați în creierul nostru încât încetăm să le acordăm atenție. Pentru majoritatea programatorilor „orientați pe obiecte”, aceasta înseamnă că nu ne mai gândim la mulți dintre termenii suprautilizați precum „abstracție”, „polimorfism”, „încapsulare”. De ce să te gândești la ele, deoarece totul este deja clar? ;)

Cu toate acestea, pentru a înțelege cu exactitate semnificația Principiului inversării dependenței și a doua parte a definiției, trebuie să revenim la unul dintre aceste concepte fundamentale. Să ne uităm la definiția termenului „abstracție” din cartea lui Gradi Bucha:

Abstracția identifică caracteristicile esențiale ale unui obiect care îl deosebesc de toate celelalte tipuri de obiecte și, astfel, își definește clar granițele conceptuale din punctul de vedere al observatorului.

Cu alte cuvinte, o abstractizare definește comportamentul vizibil al unui obiect, care în termenii limbajelor de programare este determinat de interfața publică (și protejată) a obiectului. Foarte des modelăm abstracții folosind interfețe sau clase abstracte, deși din punct de vedere OOP acest lucru nu este necesar.

Să revenim la definiție: Abstracțiile nu ar trebui să depindă de detalii. Detaliile trebuie să depindă de abstracții.

Ce exemplu ne vine în minte acum, după ce ne amintim ce este? abstractizare? Când începe abstracția să depindă de detalii? Un exemplu de încălcare a acestui principiu este clasa abstractă GZipStream, care ia MemoryStream, nu o clasă abstractă Curent:

Clasa abstractă GZipStream ( // Abstracția GZipStream acceptă un flux concret protejat GZipStream(MemoryStream memoryStream) () )

Un alt exemplu de încălcare a acestui principiu ar fi clasa de depozit abstractă din stratul de acces la date care preia constructorul PostgreSqlConnection sau un șir de conexiune pentru SQL Server, care face ca orice implementare a unei astfel de abstractizări să fie legată de o anumită implementare. Dar asta înseamnă Bob Martin? Judecând după exemplele date în articol sau în carte, Bob Martin înțelege ceva complet diferit prin conceptul de „abstracție”.

PrincipiuDIPconform lui Martin

Pentru a explica definiția sa, Bob Martin oferă următoarea explicație.

O interpretare ușor simplificată, dar totuși foarte eficientă a principiului DIP este exprimată printr-o regulă euristică simplă: „Trebuie să te bazezi pe abstracții”. Afirmă că nu ar trebui să existe dependențe de anumite clase; toate conexiunile din program trebuie să conducă la o clasă sau interfață abstractă.

  • Nu ar trebui să existe variabile care să stocheze referințe la clase specifice.
  • Nu ar trebui să existe clase care derivă din clase concrete.
  • Nu ar trebui să existe metode care să suprascrie o metodă implementată într-una dintre clasele de bază.

Pentru a ilustra încălcarea principiului DIP în general și primul punct „clarificator”, în special, este dat următorul exemplu:

Buton de clasă publică ( lampă privată lampă; public void Poll() ( dacă (/* o anumită stare */) lampă.TurnOn(); ) )

Acum să ne amintim încă o dată ce este abstractizareși răspunde la întrebarea: există aici o „abstracție” care depinde de detalii? În timp ce te gândești la asta sau cauți paragraful care conține răspunsul la această întrebare, vreau să fac o mică digresiune.

Codul are o caracteristică interesantă. Cu rare excepții, codul în sine nu poate fi corect sau incorect; Dacă este o eroare sau o caracteristică, depinde de ceea ce se așteaptă de la aceasta. Chiar dacă nu există o specificație formală (care este norma), codul este incorect numai dacă face altceva decât ceea ce este necesar sau intenționat să facă. Acest principiu stă la baza programării contractelor, în care specificațiile (intențiile) sunt exprimate direct în cod sub formă de precondiții, postcondiții și invarianți.

Privind clasa Buton Nu pot spune dacă designul este defect sau nu. Pot spune cu siguranță că numele clasei nu se potrivește cu implementarea sa. Clasa trebuie redenumită în LampButton sau scoateți din clasă Buton camp Lampă.

Bob Martin insistă că acest design este defectuos deoarece „strategia aplicației de nivel înalt nu este separată de implementarea de nivel scăzut. Abstracțiile nu sunt separate de detalii. În absența unei astfel de separări, strategia de nivel superior depinde automat de modulele de nivel inferior, iar abstracția depinde automat de detalii.”

In primul rand, Nu văd „strategii de nivel superior” și „module de nivel inferior” în acest exemplu: din punctul meu de vedere, cursuri ButonȘi Lampă sunt la același nivel de abstractizare (cel puțin eu nu văd argumente contrare). Faptul că clasa Buton A fi capabil să controlezi pe cineva nu îl face să fie la un nivel mai înalt. În al doilea rând, aici nu există o „abstracție dependentă de detaliu”, există o „implementare a abstracției dependentă de detalii”, care nu este deloc același lucru.

Soluția lui Martin este:



Figura 3 – „Inversarea dependențelor”

Este aceasta solutie mai buna? Să aruncăm o privire…

Principalul avantaj al inversării dependențelor „conform lui Martin” este inversarea proprietății. În designul original, la schimbarea clasei Lampă clasa ar trebui să se schimbe Buton. Acum clasa Buton„deține” interfața ButtonServer, dar nu se poate modifica din cauza modificărilor „nivelurilor inferioare”, cum ar fi Lampă. Este exact invers: schimbarea clasei ButtonServer posibil doar sub influența modificărilor din clasa Button, ceea ce va duce la schimbări în toți descendenții clasei ButonServer!

Formularea principiului inversării dependenței constă din două reguli, a căror respectare are un efect extrem de pozitiv asupra structurii codului:

  • Modulele de nivel superior nu ar trebui să depindă de modulele de nivel inferior. Ambele trebuie să depindă de o abstractizare.
  • abstracțiile nu ar trebui să depindă de detalii. Detaliile trebuie să depindă de abstracții.

La început nu sună foarte atractiv, iar cititorul este probabil deja pregătit pentru un articol foarte plictisitor, cu o grămadă de termeni, figuri complexe de stil și exemple, despre care oricum nimic nu este clar. Dar degeaba, pentru că, în principiu, sub rezerva principiului inversării dependenței, totul se rezumă din nou la o utilizare corectă și în contextul ideii principale - reutilizarea codului.

Un alt concept care va fi relevant aici este cuplarea slabă a tipurilor, adică reducerea sau eliminarea dependenței lor unele de altele, ceea ce, de fapt, se realizează prin abstracție și polimorfism. Aceasta este, de fapt, esența principiului inversării dependenței.

Acum să ne uităm la un exemplu care va demonstra clar cum arată cuplajul liber în acțiune.

Să presupunem că decidem să comandăm un tort de ziua de naștere. Pentru asta am mers la o brutărie minunată de la colțul străzii. Am aflat dacă ne pot coace o prăjitură sub numele zgomotos „Lux” și, după ce am primit un răspuns pozitiv, l-am comandat. E simplu.

Acum să decidem ce abstracții trebuie incluse în acest cod. Pentru a face acest lucru, pune-ți doar câteva întrebări:

  • De ce tort? De asemenea, puteți comanda o plăcintă sau cupcakes
  • De ce anume de ziua ta? Dacă ar fi o nuntă sau o absolvire?
  • De ce „Lux”, ce se întâmplă dacă îmi place mai mult „Anthill” sau „Praga”?
  • De ce tocmai această brutărie, și nu o patiserie în centrul orașului sau altundeva?

Și fiecare dintre aceste „ce ar fi dacă” și „ce-ar fi dacă” sunt puncte în care este necesară extensibilitatea codului și, în consecință, cuplarea liberă și definirea tipurilor prin abstracții.

Acum să începem să construim tipurile în sine.

Vom defini o interfață pentru orice capodopera de cofetărie pe care am putea-o comanda.

Interfață IPastry ( șir Nume ( get; set; ) )

Și iată o implementare specifică, pentru cazul nostru - un tort aniversar. După cum puteți vedea, în mod tradițional, un tort de ziua de naștere implică lumânări)))

Clasa Birthday Cake: IPastry ( public int NumberOfCandles ( get; set; ) public string Name ( get; set; ) public override string ToString() ( return String.Format("(0) with (1) candle nices", Name, NumberOfCandles ) ; ) )

Acum, dacă avem nevoie de un tort pentru o nuntă sau doar pentru ceai, sau vrem cupcakes sau plăcinte cu cremă, avem o interfață de bază pentru toată lumea.

Următoarea întrebare este cum diferă produsele de cofetărie unele de altele (cu excepția numelui, desigur). Desigur, cu o rețetă!

Și conceptul de rețetă include o listă de ingrediente și o descriere a procesului de gătit. Prin urmare, vom avea o interfață separată pentru conceptul de ingredient:

Interfață IIngredient ( șir Nume Ingredient ( get; set;) double Cantitate ( get; set; ) șir Unități ( get; set; ) )

Și iată ingredientele pentru tortul nostru: făină, unt, zahăr și smântână:

Clasa Făină:IIngredient ( șir public NumeIngredient ( get; set; ) public double Cantitate ( get; set; ) public șir Unități ( get; set; ) public șir Quality ( get; set; ) ) class Butter: IIngredient ( șir public IngredientName ( get; set; ) public double Cantitate ( get; set; ) public șir Unități ( get; set; ) ) class Sugar: IIngredient ( public string IngredientName ( get; set; ) public double Cantitate ( get; set; ) public string Unitati ( get; set; ) public string Kind ( get; set; ) ) class Creme: IIngredient ( public string IngredientName ( get; set; ) public double Cantitate ( get; set; ) public string Units ( get; set; ) public dublu grăsime ( obține; setează; ) )

Lista ingredientelor poate diferi în diferite rețete și diferite produse de cofetărie, dar pentru rețeta noastră această listă este suficientă.

Acum este timpul să trecem la conceptul de rețetă. Orice am găti, știm în orice caz cum se numește, ce este, ce ingrediente sunt incluse în fel de mâncare și cum să-l gătim.

Interfață IRecipe ( Tip PastryType ( get; set; ) șir Nume ( get; set;) IList Ingrediente ( get;set;) string Descriere ( get;set;) )

Mai exact, clasa care reprezintă o rețetă de tort de ziua de naștere arată astfel:

Clasa BirthdayCakeRecipe: IRecipe ( public Type PastryType ( get; set; ) public string Nume ( get; set;) public IList Ingrediente ( get; set; ) public șir Descriere ( get; set; ) public BirthdayCakeReipe() ( Ingrediente = listă nouă (); } }

Acum să trecem la minunata noastră brutărie de la colțul străzii.

Desigur, am putea aplica la multe alte brutării, așa că vom defini o interfață de bază și pentru asta. Care este cel mai important lucru pentru o brutărie? Capacitatea de a coace produse.

Interfață I Bakery ( IPastry Bake (rețetă IRecipe); )

Și iată clasa care reprezintă brutăria noastră:

Clasa NiceBakeryOnTheCornerOFMyStreet ( Dicționar meniu = nou Dicționar (); public void AddToMenu(IRecipe recipe) ( if (!menu.ContainsKey(recipe.Name)) ( menu.Add(recipe.Name, recipe); ) else ( Console.WriteLine("Este deja în meniu"); ) ) public IRecipe FindInMenu(nume șir) ( if (menu.ContainsKey(nume)) (retur meniu; ) Console.WriteLine("Ne pare rău... în prezent nu avem " + nume); return null; ) public IPastry Bake (Rețetă IRecipe) ( dacă (rețetă != null) ( IPastry pastry = Activator.CreateInstance(recipe.PastryType) ca IPastry; if (patitry != null) ( pastry.Name = recipe.Name; returnează patiserie ca IPastry; ) ) returnează nul; ))

Tot ce rămâne este să testăm codul:

Programul de clasă ( static void Main() ( //crearea unei inctanțe a clasei de panificație var bakery = new NiceBakeryOnTheCornerOFMyStreet(); //pregătirea ingredientelor pentru rețetă var flour = new Flour() ( IngredientName = „Făină”, Cantitate = 1,5 , Units = „kg” ); var unt = new Butter() ( IngredientName = „Unt”, Cantitate = 0,5, Unități = „kg” ); var sugar = new Sugar() ( IngredientName = „Sugar”, Cantitate = 0,7 , Units = "kg" ); var creme = new Creme() ( IngredientName = "Creme", Cantitate = 1,0, Units = "litri" ); //și aceasta este rețeta în sine var weddingCakeRecipe = new BirthdayCakeRecipe() (PatryType = typeof(BirthdayCake), Name = „Birthday Cake Luxury”, Description = „descrierea cum să faci un tort frumos de ziua de naștere” ); weddingCakeRecipe.Ingredients.Add(făină); weddingCakeRecipe.Ingredients.Add(unt); weddingCakeRecipe.Ingredients .Add(zahăr); weddingCakeRecipe.Ingredientes.Add(weddingCakeRecipe); //adăugarea rețetei noastre de tort în meniul brutăriei. //acum haideți să-l comandăm!! Tort de aniversare = panificație.Coaceți(brutărie.GăseșteInMenu(„Prăjitură de aniversare de lux”)) ca Tort de aniversare; //adăugând niște lumânări ;) tort.NumărDeLumânări = 10; //și aici suntem !!! Consola.WriteLine(tort); ) )

Acum să ne uităm din nou la întregul cod și să-l evaluăm. Codul este destul de simplu, tipurile, abstracțiile lor, datele și funcționalitatea sunt clar delimitate, codul permite extinderea și reutilizarea. Fiecare segment poate fi înlocuit fără durere cu altul care se potrivește cu tipul de bază, iar acest lucru nu va bloca restul codului.

Puteți adăuga la nesfârșit tipuri de ingrediente, rețete pentru diferite tipuri de produse de cofetărie, puteți crea alte clase care descriu brutării, cofetării și alte unități similare.

Nu este un rezultat rău. Și totul datorită faptului că am încercat să facem orele să fie minim legate între ele.

Acum să ne gândim la consecințele încălcării principiului inversării dependenței:

  1. rigiditate (ar fi foarte dificil să faci orice modificare a sistemului, deoarece fiecare modificare a afectat multe părți diferite ale acestuia).
  2. fragilitate (atunci când se fac modificări la o parte a sistemului, alte părți ale acestuia devin vulnerabile și uneori acest lucru nu este prea evident la prima vedere).
  3. imobilitate (puteți uita de reutilizarea codului în alte sisteme, deoarece modulele sunt puternic interconectate).

Ei bine, acum trageți propriile concluzii despre cât de util este principiul inversării dependenței în cod. Cred că răspunsul este evident.

14 răspunsuri

Practic spune:

  • Abstracțiile nu ar trebui să depindă niciodată de detalii. Detaliile trebuie să depindă de abstracții.

În ceea ce privește motivul pentru care acest lucru este important, într-un cuvânt: schimbarea este riscantă și, în funcție de concept, mai degrabă decât de implementare, reduceți nevoia de schimbare pe site-urile de apel.

În mod efectiv, DIP reduce cuplarea dintre diferite părți ale codului. Ideea este că, deși există multe moduri de a implementa, de exemplu, un logger, modul în care îl utilizați ar trebui să fie relativ stabil în timp. Dacă puteți extrage o interfață care reprezintă conceptul de logare, acea interfață ar trebui să fie mult mai stabilă în timp decât implementarea ei, iar site-urile care apelează ar trebui să fie mult mai puțin susceptibile la modificările pe care le-ați putea face prin menținerea sau extinderea acelui mecanism de înregistrare .

Deoarece implementarea este specifică interfeței, aveți posibilitatea de a alege în timpul execuției care implementare este cea mai potrivită pentru mediul dumneavoastră specific. În funcție de caz, acest lucru poate fi și interesant.

Cărțile Agile Software Development, Principles, Patterns and Practices și Agile Principles, Patterns and Practices in C# sunt cele mai bune resurse pentru a înțelege pe deplin obiectivele și motivațiile originale din spatele Principiului inversării dependenței. Articolul „The Dependency Reversal Principle” este și el o resursă bună, dar datorită faptului că este o versiune condensată a proiectului care a ajuns în cele din urmă în cărțile menționate anterior, lasă în urmă câteva discuții importante despre conceptul de proprietate a pachetului. și interfețe care sunt esențiale pentru Acest principiu diferă de sfatul mai general de „programare pentru interfață, nu implementare” găsit în cartea Design Patterns (Gamma, et al.).

Pentru un scurt rezumat, principiul inversării dependenței urmărește în primul rând să Schimbare canalizarea tradițională a dependențelor de la componentele de „nivel superior” la componente de „nivel inferior”, astfel încât componentele de „nivel inferior” depind de interfețe, apartenenta la componentele „de nivel superior” (Notă: componenta „de nivel superior” se referă aici la componenta care necesită dependențe/servicii externe și nu neapărat la poziția sa conceptuală în arhitectura stratificată.) Cu toate acestea, relația nu este scade la fel de mult ca ea schimburi de la componente care sunt teoretic mai puțin valoroase la componente care sunt teoretic mai valoroase.

Acest lucru se realizează prin proiectarea componentelor ale căror dependențe externe sunt exprimate ca o interfață pentru care consumatorul componentei trebuie să ofere o implementare. Cu alte cuvinte, anumite interfețe exprimă ceea ce are nevoie componenta, nu modul în care utilizați componenta (de exemplu, „INeedSomething” mai degrabă decât „IDoSomething”).

Ceea ce principiul inversării dependenței nu abordează este practica simplă de abstracție a dependențelor folosind interfețe (de exemplu, MyService -> ). Deși acest lucru decuplă componenta de detaliul specific de implementare al dependenței, nu inversează relația dintre consumator și dependență (de exemplu, ⇐ Logger.

Importanța principiului inversării dependenței se poate reduce la un singur scop - capacitatea de a reutiliza componente software care se bazează pe dependențe externe pentru o parte a funcționalității lor (înregistrare, verificare etc.)

În cadrul acestui obiectiv general de reutilizare, putem distinge două subtipuri de reutilizare:

    Utilizarea unei componente software în mai multe aplicații cu implementări de dependență (de exemplu, ați dezvoltat un container DI și doriți să furnizați înregistrarea în jurnal, dar nu doriți să vă legați containerul de un anumit logger, astfel încât toți cei care vă folosesc containerul trebuie să folosească și înregistrarea în jurnal). biblioteca pe care o alegeți) .

    Utilizarea componentelor software într-un context în evoluție (de exemplu, ați dezvoltat componente de logică de afaceri care rămân aceleași în diferite versiuni ale aplicației, unde detaliile de implementare evoluează).

În primul caz de reutilizare a componentelor în mai multe aplicații, cum ar fi cu o bibliotecă de infrastructură, scopul este de a oferi consumatorilor infrastructura de bază fără a-ți lega consumatorii de dependențele propriei biblioteci, deoarece obținerea dependențelor de la astfel de dependențe necesită de asemenea consumatorii să solicite aceleași dependențe. Acest lucru poate fi problematic atunci când consumatorii bibliotecii dvs. decid să folosească o bibliotecă diferită pentru aceleași nevoi de infrastructură (cum ar fi NLog și log4net) sau dacă decid să folosească o versiune ulterioară a unei biblioteci necesare care nu este compatibilă cu versiunea necesară. de biblioteca dvs.

În al doilea caz de reutilizare a componentelor logicii de afaceri (adică „Componente de nivel superior”), scopul este de a izola implementarea aplicației în domeniul principal de nevoile în schimbare ale detaliilor de implementare (de exemplu, modificarea/actualizarea bibliotecilor persistente, mesajele bibliotecilor de schimb) . strategii de criptare etc.). În mod ideal, modificarea detaliilor de implementare a aplicației nu ar trebui să distrugă componentele care încapsulează logica de afaceri a aplicației.

Notă. Unii pot obiecta asupra descrierii acestui al doilea caz ca reutilizare reală, considerând că componentele precum componentele logicii de afaceri utilizate într-o singură aplicație în curs de dezvoltare constituie doar o singură utilizare. Ideea aici, totuși, este că fiecare modificare a detaliilor de implementare a aplicației reprezintă un context nou și, prin urmare, un caz de utilizare diferit, deși obiectivele finale pot fi distinse ca izolare și portabilitate.

Deși poate exista un anumit beneficiu în urma principiului inversării dependenței în al doilea caz, trebuie remarcat faptul că importanța acestuia, aplicată limbilor moderne, cum ar fi Java și C#, a fost mult redusă, poate până la punctul în care este irelevant. După cum sa discutat mai devreme, DIP implică separarea completă a detaliilor de implementare în pachete separate. În cazul unei aplicații în evoluție, totuși, simpla utilizare a interfețelor definite în termeni de domeniu de afaceri va proteja împotriva necesității de a modifica componentele de nivel superior din cauza nevoilor în schimbare ale componentelor de detalii de implementare, chiar dacă detaliile de implementare se află în cele din urmă în același pachet . Această parte a principiului reflectă aspecte care erau relevante pentru limba la momentul codificării sale (de exemplu, C++), care nu sunt relevante pentru limbile mai noi. Cu toate acestea, importanța principiului inversării dependenței este legată în primul rând de dezvoltarea de componente/biblioteci software reutilizabile.

O discuție mai detaliată a acestui principiu, în legătură cu utilizarea simplă a interfețelor, injecția de dependență și modelul de interfață împărțită poate fi găsită.

Când dezvoltăm aplicații software, putem lua în considerare clase de nivel scăzut - clase care implementează operațiuni de bază și primare (acces la disc, protocoale de rețea etc.) și clase de nivel înalt - clase care încapsulează logica complexă (fluxuri de afaceri,... ) .

Aceștia din urmă se bazează pe clase de nivel scăzut. Modul natural de a implementa astfel de structuri ar fi să scriem clase de nivel scăzut și ori de câte ori suntem forțați să scriem clase complexe de nivel înalt. Deoarece clasele de nivel înalt sunt definite din perspectiva celorlalți, aceasta pare a fi o modalitate logică de a face acest lucru. Dar acesta nu este un design receptiv. Ce se întâmplă dacă trebuie să înlocuim o clasă de nivel scăzut?

Principiul inversării dependenței prevede că:

  • Modulele de nivel înalt nu ar trebui să depindă de modulele de nivel scăzut. Ambele trebuie să depindă de abstracții.

Acest principiu urmărește să „inverseze” ideea obișnuită că modulele de nivel înalt din software trebuie să depindă de modulele de nivel inferior. Aici, modulele de nivel înalt dețin abstracția (de exemplu, metodele de interfață de decizie) care sunt implementate de modulele de nivel inferior. Astfel, modulele de nivel inferior depind de modulele de nivel superior.

Utilizarea eficientă a inversării dependenței oferă flexibilitate și stabilitate în întreaga arhitectură a aplicației. Acest lucru va permite aplicației dvs. să se dezvolte mai sigur și mai stabil.

Arhitectură tradițională stratificată

În mod tradițional, interfața de utilizator a unei arhitecturi stratificate depindea de stratul de afaceri, care, la rândul său, depindea de stratul de acces la date.

Trebuie să înțelegeți stratul, pachetul sau biblioteca. Să vedem cum merge codul.

Am avea o bibliotecă sau un pachet pentru stratul de acces la date.

// Clasa publică DataAccessLayer.dll ProductDAO ( )

// BusinessLogicLayer.dll utilizând DataAccessLayer; clasa publică ProductBO (privat ProductDAO productDAO; )

Arhitectură stratificată cu inversare a dependenței

Inversarea dependenței indică următoarele:

Modulele de nivel înalt nu ar trebui să depindă de modulele de nivel scăzut. Ambele trebuie să depindă de abstracții.

Abstracțiile nu ar trebui să depindă de detalii. Detaliile trebuie să depindă de abstracții.

Ce sunt modulele de nivel înalt și de nivel scăzut? Gândindu-ne la module precum biblioteci sau pachete, modulele de nivel înalt vor fi cele care au în mod tradițional dependențe și cele de nivel scăzut de care depind.

Cu alte cuvinte, nivelul înalt al modulului va fi acolo unde este chemată acțiunea, iar nivelul scăzut, unde este executată acțiunea.

Din acest principiu se poate trage o concluzie rezonabilă: nu ar trebui să existe dependență între noduli, dar ar trebui să existe o dependență de abstractizare. Dar, conform abordării pe care o adoptăm, este posibil să folosim incorect dependența de investiții, dar aceasta este o abstractizare.

Imaginați-vă că ne adaptăm codul astfel:

Am avea o bibliotecă sau un pachet pentru stratul de acces la date care definește abstracția.

// DataAccessLayer.dll interfață publică IProductDAO clasă publică ProductDAO: IProductDAO( )

Și altă logică de afaceri la nivel de bibliotecă sau pachet care depinde de nivelul de acces la date.

// BusinessLogicLayer.dll utilizând DataAccessLayer; Public class ProductBO (privat IProductDAO productDAO; )

Deși depindem de abstractizare, relația dintre afaceri și accesul la date rămâne aceeași.

Pentru a realiza inversarea dependenței, interfața de persistență trebuie definită în modulul sau pachetul în care se află logica sau domeniul de nivel înalt, nu în modulul de nivel scăzut.

Mai întâi definiți ce este un strat de domeniu și abstractizarea comunicării acestuia este definită de persistență.

// Interfața publică Domain.dll IProductRepository; folosind DataAccessLayer; clasă publică ProductBO (private IProductRepository productRepository; )

Odată ce nivelul de persistență depinde de domeniu, acum este posibil să se inverseze dacă este definită o dependență.

// Clasa publică Persistence.dll ProductDAO: IProductRepository( )

Aprofundarea principiului

Este important să înțelegeți bine conceptul, aprofundând scopul și beneficiile. Dacă rămânem în mecanică și studiem un depozit tipic, nu vom putea determina unde putem aplica principiul dependenței.

Dar de ce inversăm dependența? Care este scopul principal dincolo de exemplele specifice?

Aceasta este de obicei permite ca lucrurile cele mai stabile, care sunt independente de lucrurile mai puțin stabile, să se schimbe mai des.

Este mai ușor să schimbați tipul de persistență sau baza de date sau tehnologia pentru a accesa aceeași bază de date decât logica domeniului sau acțiunile concepute pentru a comunica cu persistență. Acest lucru face ca dependența să fie inversată, deoarece este mai ușor să modificați persistența dacă are loc această modificare. Astfel nu va trebui să schimbăm domeniul. Stratul de domeniu este cel mai stabil dintre toate, deci nu ar trebui să depindă de nimic.

Dar există mai mult decât acest exemplu de stocare. Există multe scenarii în care se aplică acest principiu și există arhitecturi bazate pe acest principiu.

arhitectură

Există arhitecturi în care inversarea dependenței este cheia definiției sale. În toate domeniile, acest lucru este cel mai important și abstracțiile sunt cele care vor specifica protocolul de comunicare între domeniu și restul pachetelor sau bibliotecilor.

Arhitectură curată

Pentru mine principiul inversării dependenței descris în articolul oficial

Problema în C++ este că fișierele antet conțin de obicei declarații de câmpuri și metode private. Deci, dacă un modul C++ de nivel înalt conține un fișier antet pentru un modul de nivel scăzut, acesta va depinde de real implementare detaliile acestui modul. Și acest lucru evident nu este foarte bun. Dar aceasta nu este o problemă în limbile mai moderne care sunt utilizate în mod curent astăzi.

Modulele de nivel înalt sunt în mod inerent mai puțin reutilizabile decât modulele de nivel scăzut, deoarece primele sunt de obicei mai specifice aplicației/contextului decât cele din urmă. De exemplu, componenta care implementează ecranul UI este de cel mai înalt nivel și, de asemenea, foarte (în întregime?) specifică aplicației. Încercarea de a reutiliza o astfel de componentă într-o altă aplicație este contraproductivă și poate duce doar la supradezvoltare.

Astfel, crearea unei abstracție separată la același nivel al componentei A care depinde de componenta B (care nu depinde de A) se poate face numai dacă componenta A va fi de fapt utilă pentru reutilizare în diferite aplicații sau contexte. Dacă nu este cazul, atunci aplicația DIP va fi un design prost.

O modalitate mai clară de a afirma principiul inversării dependenței:

Modulele dvs. care încapsulează logica de afaceri complexă nu ar trebui să depindă direct de alte module care încapsulează logica de afaceri. În schimb, ar trebui să depindă doar de interfețele cu date simple.

Adică, în loc să implementezi clasa ta Logic așa cum fac oamenii de obicei:

Dependență de clasă ( ... ) Logica de clasă ( dependență privată dep; int doSomething() ( // Logica de afaceri folosind dep aici ) )

ar trebui să faci ceva de genul:

Dependență de clasă ( ... ) interfață Data ( ... ) clasa DataFromDependency implementează Data ( private Dependency dep; ... ) clasa Logic ( int doSomething(Data data) ( // calculează ceva cu date ) )

Data și DataFromDependency ar trebui să locuiască în același modul ca Logic, nu cu Dependency.

De ce asta?

Răspunsuri bune și exemple bune au fost deja date de alții aici.

Punctul de inversare a dependenței este de a face software-ul reutilizabil.

Ideea este că în loc să se bazeze două bucăți de cod una pe cealaltă, ele se bazează pe o interfață abstractă. Apoi puteți reutiliza orice parte fără cealaltă.

Acest lucru se realizează de obicei prin inversarea containerului de control (IoC), cum ar fi Spring în Java. În acest model, proprietățile obiectelor sunt stabilite prin configurația XML, mai degrabă decât obiectele care ies și își găsesc dependența.

Imaginați-vă acest pseudocod...

Clasa publică MyClass ( serviciul public myService = ServiceLocator.service; )

MyClass depinde direct atât de clasa Service, cât și de clasa ServiceLocator. Acest lucru este necesar pentru ambele dacă doriți să îl utilizați într-o altă aplicație. Acum imaginați-vă asta...

Clasa publică MyClass (public IService myService; )

MyClass folosește acum o singură interfață, interfața IService. Am lăsa containerul IoC să stabilească de fapt valoarea acestei variabile.

Să existe un hotel care să-i ceară proviziile producătorului de alimente. Hotelul dă numele mâncării (să spunem pui) Generatorului de Alimente, iar Generatorul returnează hrana solicitată la hotel. Dar hotelului nu îi pasă de tipul de mâncare pe care îl primește și îl servește. Astfel, Generatorul furnizează hotelului alimente cu eticheta „Food”.

Această implementare în JAVA

FactoryClass cu o metodă din fabrică. Generator de alimente

Clasa publică FoodGenerator ( Food food; public Food getFood(Nume șir)( if(name.equals("pește"))( food = new Fish(); )else if(name.equals("pui"))( food = nou Pui(); )altfel mâncare = null; returnare mâncare; ) )

Adnotare/Interfață de clasă

Clasa abstractă publică Food ( //Nimeni din clasa copil nu va suprascrie această metodă pentru a asigura calitatea... public void quality())( String fresh = „Acesta este un fresh” + getName(); String tasty = „Acesta este un gustos " + getName(); System.out.println(proaspăt); System.out.println(gustos); ) public abstract String getName(); )

Pui vinde alimente (clasa de beton)

Public class Chicken extinde Mâncarea ( /*Toate tipurile de mâncare trebuie să fie proaspete și gustoase, așa că * Ele nu vor suprascrie metoda super-clasă „property()”*/ public String getName())( return „Chicken”; ) )

Peștele vinde alimente (clasa specifică)

Clasa publică Pește extinde Mâncare ( /*Toate tipurile de alimente trebuie să fie proaspete și gustoase, așa că * Ele nu vor suprascrie metoda super-clasei „property()”*/ public String getName())( return „Fish”; ) )

In cele din urma

Hotel

Clasa publică Hotel ( public static void main(String args)( //Folosind o clasă Factory.... FoodGenerator foodGenerator = new FoodGenerator(); //O metodă din fabrică pentru a instanția alimentele... Food food = foodGenerator.getFood( „pui”); calitate.alimentare(); ) )

După cum puteți vedea, hotelul nu știe dacă este pui sau pește. Se știe doar că acesta este un produs alimentar, adică. Hotelul depinde de clasa de mâncare.

De asemenea, puteți observa că clasa Pește și Pui implementează clasa Mâncare și nu este direct asociată cu hotelul. acestea. puiul și peștele depind și de clasa alimentară.

Aceasta înseamnă că o componentă de nivel înalt (hotel) și o componentă de nivel scăzut (pește și pui) depind de o abstractizare (hrană).

Aceasta se numește inversare a dependenței.

Principiul inversării dependenței (DIP) prevede că

i) Modulele de nivel înalt nu ar trebui să depindă de modulele de nivel scăzut. Ambele trebuie să depindă de abstracții.

ii) Abstracțiile nu ar trebui să depindă niciodată de detalii. Detaliile trebuie să depindă de abstracții.

Interfață publică ICustomer ( șir GetCustomerNameById(int id); ) clasă publică Client: ICustomer ( //ctor public Customer()() șir public GetCustomerNameById(int id) ( returnează „Nume client inactiv”; ) ) clasă publică CustomerFactory ( static public ICustomer GetCustomerData() ( return new Customer(); ) ) clasă publică CustomerBLL ( ICustomer _customer; public CustomerBLL() ( _customer = CustomerFactory.GetCustomerData(); ) public șir GetCustomerNameById(int id) ( return _customer(GetYCustomerData).); ) ) program de clasă publică ( static void Main() ( CustomerBLL customerBLL = new CustomerBLL(); int customerId = 25; string customerName = customerBLL.GetCustomerNameById(customerId); Console.WriteLine(customerName); Console.ReadKey(); ) )

Notă. O clasă ar trebui să depindă de abstracții, cum ar fi o interfață sau clase abstracte, mai degrabă decât de detalii concrete (implementarea interfeței).

acțiune

Ultima actualizare: 03/11/2016

Principiul inversării dependenței Principiul inversării dependenței este utilizat pentru a crea entități slab cuplate care sunt ușor de testat, modificat și actualizat. Acest principiu poate fi formulat astfel:

Modulele de nivel superior nu ar trebui să depindă de modulele de nivel inferior. Ambele trebuie să depindă de abstracții.

Abstracțiile nu ar trebui să depindă de detalii. Detaliile trebuie să depindă de abstracții.

Pentru a înțelege principiul, luați în considerare următorul exemplu:

Class Book ( public șir Text ( get; set; ) public ConsolePrinter Printer ( get; set; ) public void Print() ( Printer.Print(Text); ) ) class ConsolePrinter ( public void Print(text text) ( Console.WriteLine (text); ) )

Clasa Book, care reprezintă o carte, folosește clasa ConsolePrinter pentru a imprima. Cu această definiție, clasa Book depinde de clasa ConsolePrinter. Mai mult, am definit strict că imprimarea unei cărți se poate face doar pe consolă folosind clasa ConsolePrinter. Alte opțiuni, de exemplu, ieșirea către o imprimantă, ieșirea într-un fișier sau utilizarea unor elemente de interfață grafică - toate acestea sunt excluse în acest caz. Abstracția tipăririi cărții nu este separată de detaliile clasei ConsolePrinter. Toate acestea reprezintă o încălcare a principiului inversării dependenței.

Acum să încercăm să aducem clasele noastre în conformitate cu principiul inversării dependenței, separând abstracțiile de implementarea la nivel scăzut:

Interfață IPrinter ( void Print( șir text); ) class Book ( public șir Text ( get; set; ) public IPrinter Printer ( get; set; ) public Book(IPrinter printer) ( this.Printer = imprimantă; ) public void Print( ) ( Printer.Print(Text); ) ) class ConsolePrinter: IPrinter ( public void Print(string text) ( Console.WriteLine("Print to console"); ) ) class HtmlPrinter: IPrinter ( public void Print(string text) ( Console.WriteLine("Tipărește în html"); ) )

Abstracția tipăririi cărților este acum separată de implementările concrete. Ca rezultat, atât clasa Book, cât și clasa ConsolePrinter depind de abstractizarea IPrinter. În plus, acum putem crea și implementări suplimentare la nivel scăzut ale abstractizării IPrinter și să le aplicăm dinamic în program:

Book book = new Book(new ConsolePrinter()); carte.Imprimare(); book.Printer = nou HtmlPrinter(); carte.Imprimare();