Cum au influențat serviciile comportamentul de injectare a dependenței de dezvoltatori - Blogul Flagbit

Cel puțin de la popularitatea în continuă creștere a testelor unitare, în special prin PHPUnit, rezolvarea dependențelor claselor prin injectarea dependenței în PHP a jucat un rol din ce în ce mai important în viața de zi cu zi a dezvoltatorilor. Clasele care au una sau mai multe dependențe de alte tipuri de clase sunt transferate atunci când sunt create prin intermediul constructorului sau în timpul rulării prin apeluri de metodă. Acest lucru îmbunătățește menținerea claselor individuale și dependențele pot fi schimbate mai ușor dacă cerințele se schimbă. Testabilitatea componentelor individuale este, de asemenea, îmbunătățită, în care dependențele de clasă pot fi înlocuite cu teste duble simple. Un dezavantaj este totuși numărul crescut de cod boilerplate atunci când aceleași obiecte trebuie întotdeauna create în diferite puncte ale proiectului pentru a acoperi aceleași dependențe.
Așa-numitul localizator de servicii a fost introdus cu cadrele PHP actuale. Sarcina localizatorului de servicii este să rezolve și să instanțieze dependențe de un serviciu intern. Aceasta înseamnă că instanțele de obiect pot fi create printr-un serviciu fără a fi nevoie să adăugați codul boilerplate pentru a rezolva dependențele din logica de afaceri. Mai mult, localizatorul de servicii asigură o singură instanță a serviciului apelat în cadrul unei cereri pentru a evita crearea obiectelor inutile, fără dezavantajele unui single clasic.
Cu timpul, totuși, ușurința de utilizare a localizatorului de servicii a orientat utilizarea injecției de dependență în direcții care pot fi implementate rapid din punct de vedere tehnic, dar care nu-și urmează scopul. Dizolvarea dependențelor s-a transformat într-o îmbinare colorată a claselor.
Mai jos aș dori să prezint pe scurt cazurile pe care le-am întâlnit din când în când. De obicei, acestea pot să nu pară greșite la prima vedere, dar pot avea dezavantaje în dezvoltarea ulterioară.
Injecție localizator service
Probabil cel mai faimos obicei rău atunci când utilizați un Localizator de service este să-l adăugați ca dependență în clasa sa. De obicei, însoțit de argumentul „Poate că mai am nevoie de unul dintre servicii”, acest model este mai degrabă un semn că nu s-a planificat suficient în dezvoltarea sa. Nici o clasă nu are nevoie de toate serviciile. Uneori puteți găsi și comentarii pe Google care cred că este în regulă dacă unei fabrici i se oferă localizatorul de servicii. Chiar și o fabrică are nevoie de puține servicii pentru a funcționa. Dacă există o dependență de mai mult de alte 4 servicii, ar trebui să vă gândiți din nou la sarcina și structura fabricii.
Complexitatea este evidentă în special în scrierea testelor unitare. Clasa poate fi utilizată în mod normal pentru a identifica dependențele din capul de metodă al metodelor constructor sau setter. Când se adaugă un localizator de servicii, nu trebuie creat doar un test dublu pentru acesta, ci și pentru toate dependențele reale care sunt preluate de la acesta. De asemenea, este dificil să se spună din cod care este tipul real de serviciu. Deoarece o clasă în PHP poate conține și metode care nu provin dintr-o interfață, interschimbabilitatea claselor cu aceeași interfață nu mai este garantată.
În mod ideal, dependențele ar trebui transferate și nu luate dintr-un alt obiect.
Dacă vă uitați la Symfony 2, de exemplu, veți vedea că Localizatorul de servicii este folosit și aici. Cele mai cunoscute exemple sunt controlerele și comenzile, dar numai dacă implementează interfața ContainerAware, care este cazul ca standard. Dar există și opțiuni pentru controlere și comenzi pentru a evita acest lucru.
Logare
Informațiile de înregistrare pot fi mai importante pentru un proiect decât pentru altul și pot varia în cantitatea de informații. De aceea există adesea abordarea inserării permanente a unei instanțe logger ca dependență în serviciul unei clase. Cu toate acestea, în foarte puține cazuri, un logger este într-adevăr o dependență, deoarece mai întâi trebuie să vă puneți o întrebare: "Va funcționa în continuare fără logger?"
În funcție de decizia de proiectare, poate fi suficient să aruncați o excepție în cadrul clasei și să adăugați o intrare jurnal într-un bloc de captură, în plus față de gestionarea erorilor.
O altă posibilitate este de a colecta mesajele astfel încât să poată fi afișate mai târziu, așa cum ați face de ex. de la validatori. Și aici, intrările de jurnal pot fi create în afara clasei și astfel loggerul poate fi extras ca dependență.
Acordat, nu este o încălcare gravă a dependenței, dar dacă este ușor să țineți loggerul în afara unei clase, dezvoltatorul ar trebui să o facă. Este o componentă inutilă în testele automate și este irelevantă pentru funcția clasei supuse testului.
Prea multe dependențe
Oricine se uită la numeroasele dependențe din constructorul clasei lor și se simte inconfortabil cu ei, probabil că a recunoscut problema. Manevrarea simplă a configurațiilor de servicii face mai ușoară adăugarea de noi dependențe, deoarece dezvoltatorul de obicei nu trebuie să se îngrijoreze de modificările rezultate atunci când creează instanțe. Ca urmare, numărul dependențelor poate crește pe măsură ce un proiect progresează, creând o complexitate care este reticentă la atingere și schimbare.
Multe containere de injecție de dependență oferă opțiunea de a seta dependențe de servicii folosind metode de setare în configurație. Ca urmare, unii dezvoltatori tind să seteze dependențele folosind metode de setare în loc de constructor, dar acest lucru nu rezolvă problema. Dimpotrivă: Setările trebuie setate în mod explicit și numai atunci când obiectul există deja. Utilizarea setterului este preluată din configurația serviciului, dar această clasă, de ex. în testele unitare, poate fi utilizat numai cu cunoștințele acestor setere și dacă nu sunt setate dependențe obligatorii, dezvoltatorul trebuie să se ocupe și de gestionarea erorilor. Acest lucru face ca clasa afectată inutil să fie mai mare. Nimic din toate acestea nu este necesar cu injecția de dependență prin intermediul constructorului. Deci, cum ar trebui rezolvată această problemă?
Este un semn bun că clasa face mai mult decât ar trebui.
În acest caz, sarcinile lor trebuie împărțite, care reprezintă apoi dependențele reale ale clasei inițiale. Nu se poate exclude faptul că clasa originală în sine nu mai este necesară și, prin urmare, nu este aplicabilă, deoarece singura sa sarcină este de a îndeplini sarcinile dependențelor sale.
Rezolvarea dependențelor
Nu fiecare dependență trebuie rezolvată prin configurația serviciului, darămite o dependență directă pentru o clasă. Cu toate acestea, acestea sunt inserate prin intermediul constructorului prin service. Configurarea rapidă și ușoară a serviciilor înseamnă că modificările pot fi efectuate rapid fără a fi nevoie să schimbați locurile din codul care apelează serviciul. Aceasta invită un dezvoltator să rezolve totul despre configurația serviciului pentru serviciul afectat.
În exemplul următor, clasa Foo are o dependență de $ myService, care este transmisă în constructor.
Cu toate acestea, în exemplu, această dependență este utilizată numai în metoda doSomething. Dacă dependența nu este necesară față de clasa însăși, ci doar de una dintre metodele sale, ar trebui să se ia în considerare dacă următoarea abordare ar putea fi o soluție mai bună:
Dezavantaj: clasele care folosesc Foo și metoda sa doSomething au o dependență de $ myService.
Ca a doua abordare, trebuie de asemenea luat în considerare dacă sarcina metodei doSomething nu aparține clasei sale și trebuie configurată ca un serviciu separat. Cu toate acestea, acest nou serviciu nu trebuie neapărat să fie o dependență în clasa Foo.
În acest exemplu de cod, sarcina metodei doSomething a fost complet eliminată din clasa Foo și implementată într-o nouă clasă „Qux”, care, ca serviciu, depinde de $ myService în exemplul anterior.
Desigur, acest exemplu este păstrat simplu, deoarece în cazul refactorizării există alte obstacole în cale. Când structura clasei este schimbată, modificările trebuie făcute în toate locurile care au folosit Foo. Schimbarea structurii clasei este și mai dificilă dacă este specificată de o interfață.
Injecție și servicii de dependență
Injecția de dependență este un termen general care are multe forme diferite de implementare. Decizia cu privire la care dintre formulare ar trebui să fie utilizate într-un caz specific nu poate fi luată de un container de injectare de dependență sau de un localizator de servicii, ci trebuie decisă chiar de către dezvoltator. Cadrele ne oferă funcționalități care ne ajută cu dezvoltarea și ar trebui să reducă mulți pași obositori. Modul în care este utilizat depinde încă de noi.