kubuszok.com

1 Feb 2018• seria: Random despre Scala * tag-uri: scala tort model dependență injecție

cu mult timp în urmă, în țara Scala a apărut nou mod sigur de tip de injecție dependență. Pe termen lung, aduce mai multe probleme decât merită.

model de tort

să ne reamintim ce este un model de tort. Pentru ao înțelege, trebuie să cunoaștem tipurile de sine. Acestea au fost destinate a fi folosite ca mixine și arată mai mult sau mai puțin așa:

trait UserRepository { def fetchUsers(userIds: Seq): Seq}class UserRepositoryDbImpl(database: Database) extends UserRepository { def fetchUsers(userIds: Seq): Seq = ...}trait UserRepositoryLogging { self: UserRepository => private lazy val logger: Logger = ... override def fetchUsers(userIds: Seq): Seq = { val users = super.fetchUsers(usersIds) logger.debug(s"""Fetched users by ids: ${users.mkString(", ")}""") users }}val userRepository: UserRepository = new UserRepositoryDbImpl(database) with UserRepositoryLogging

aici putem adăuga trasaturaUserRepositoryLoggingla orice esteUserRepositoryimplementare – nu ar compila altfel. În plus, în interiorulUserRepositoryLoggingpresupunem că este implementareaUserRepository. Deci, tot ceea ce este accesibil înUserRepositoryeste accesibil și acolo.

acum, din moment ce putem accesa orice a fost declarat tip(E) utilizate pentru auto-Tip, ne este permis să facă acest lucru:

trait UserRepository { def fetchUsers(userIds: Seq): Seq}trait UserRepositoryComponent { def userRepository: UserRepository}trait UserRepositoryComponentImpl extends UserRepositoryComponent { lazy val userRepository: UserRepository = new UserRepository { // ... }}trait SomeController { self: UserRepositoryComponent => def handleRequest(userIds: Seq): Response = { // ... userRepository.fetchUsers(userIds) // ... }}object SomeController extends SomeController with UserRepositoryComponentImpl

ÎnSomeController declarăm auto-tip, care asigură, că punerea sa în aplicare instantanee va aveauserRepository metodă. Deci, prin adăugarea mixin oferim punerea în aplicare și, astfel încât să ne asigurăm că dependența este injectat la momentul compilării. Fără reflecție în timpul rulării, cu siguranță de tip, fără configurații sau biblioteci suplimentare.

fiecare astfel de componentă ar putea fi un strat al aplicației noastre (strat logica de afaceri, stratul de infrastructură, etc), că cineva în comparație cu straturile de tort. Deci, prin crearea aplicației astfel încât să se coace în mod eficient un tort. Astfel model tort.

acum… de ce este asta o problemă?

adăugarea dependențelor

în proiecte mai mari, numărul de servicii, depozite și utilități va fi cu siguranță mai mare decât 1-2. Deci tortul tău va începe să arate ca:

object PaymentTransactionApiController extends TransactionApiController with ConfigComponentImpl with DatabaseComponentImpl with UserRepositoryComponentImpl with SessionRepositoryComponentImpl with SecurityServicesComponentImpl with ExternalPaymentApiServicesComponentImpl with PaymentServicesComponentImpl with TransactionServicesComponentImpl

ca o chestiune de fapt am văzut odată un tort în careComponentImpl a trecut prin 2-3 ecrane. Nu ai fost injectarea acolo componente numai-cele-necesare-la-un-timp, ai fost combinarea totul împreună la o dată: infrastructură, servicii de domeniu, persistență, logică de afaceri…

acum, imaginați-vă ce se întâmplă atunci când trebuie să adăugați o dependență. Începeți prin a adăuga un alt with Dependency la auto-tipul dvs. Apoi, verificați unde este utilizată trăsătura actualizată. Este posibil să trebuiască să adăugați și auto-Tip aici. Te urci în copacul lui mixins până ajungi la o clasă sau la un obiect. Uff.

cu excepția, de data aceasta ați adăugat acest lucru te, astfel încât să un fel de știu ce trebuie să fie adăugate. Dar când faceți rebase sau rezolvați un conflict, s-ar putea să ajungeți într-o situație în care a apărut o nouă dependență pe care nu o aveți. Eroare compilator spune doar că self-type X does not conform to Y. Cu 50 + componente, s-ar putea ghici la fel de bine, care nu reușește. (Sau începe să faci o căutare binar cu eliminarea componentelor până eroare dispare).

când dimensiunea tortului și cantitatea crește în continuare, s-ar putea dori să pună în aplicare regula uscată și să o împartă în prăjituri mai mici, care vor fi puse împreună mai târziu. Aceasta descoperă noi adâncimi ale iadului.

eliminarea dependențelor

cu DI normal când încetați să utilizați un obiect, IDE / compilator vă pot spune că nu mai este necesar, astfel încât astfel încât s-ar putea elimina. Cu modelul tortului, nu veți fi informat despre astfel de lucruri.

ca urmare, curățarea este mult mai dificilă și este foarte posibil ca la un moment dat să ajungeți cu mult mai multe dependențe decât aveți cu adevărat nevoie.

timpul de compilare

toate acestea se adaugă la timpul de compilare. S-ar putea să nu fie evident la prima vedere, dar puteți ajunge cu:

trait ModuleAComponentImpl { self: ModuleBComponent => }trait ModuleBComponentImpl { self: ModuleAComponent => }

care este o dependență ciclică. (Știm cu toții că sunt rele, dar s-ar putea să nu observați, că tocmai ați creat unul. Mai ales dacă inițializați totul leneș).deci, aceste 2 trăsături vor fi foarte des compilate împreună, iar compilatorul incremental de Zinc nu va ajuta cu asta. În plus, modificările la oricare dintre componentele dintr-un tort vor declanșa recompilarea lucrurilor de deasupra acestuia în graficul de dependență.

testarea

dependențele ciclice creează o altă problemă. S-ar putea foarte bine termina cu ceva de genul:

trait AComponentImpl { self: BComponent => lazy val a: A = new A { b.someMethod() // use b }}trait BComponentImpl { self: AComponent => lazy val b: B = new B { a.someMethod() // use a }}object Cake extends AComponentImpl with BComponentImplCake.a // NPE!Cake.b // NPE!

fiecare componentă va fi în propriul fișier, deci nu veți observa această dependență. Testele cu mock nu vă vor ajuta cu asta:

trait BComponentMock { val b: B = mock }val aComponent = new AComponentImpl with BComponentMock {}aComponent.a // works!

deci va fi bine și dandy până când rulați efectiv codul. Știți – codul este sigur de tip, ceea ce înseamnă că valoarea pe care o veți construi va urma constrângerile de tip. Dar aceste constrângeri nu înseamnă nimic, deoareceNothing(excepție aruncată) este un subtip perfect valid de tipul dorit.

ordinea de inițializare

problema anterioară este legată de aceasta: care este ordinea inițializării componentei?

cu DI normal este evident – înainte de a pune un lucru în celălalt, trebuie să-l creați. Deci, te uiți doar la ordinea în care, vă creează obiecte.

cu un tort poți uita de el. Valorile din componente vor fi adesea lazy vals sauobject s. deci, primul atribut accesat va încerca să-și instantieze dependențele, care va încerca să-și instantieze dependențele etc.

chiar dacă mergem cuvals este o chestiune de ordine în care am compus tortul – știți, liniarizarea trăsăturilor. Atâta timp cât fiecare componentă va fi utilizată exact o singură dată – nici o problemă. Dar dacă te-ai uscat tort, iar unele componente se repetă, atunci când le îmbina…

într-o lume perfectă lucruri, cum ar fi ordinea de inițializare nu ar trebui să conteze, dar destul de des o face, și am dori să știm de exemplu, atunci când unele conexiune la DB sau abonament la subiect Kafka a început, și log lucruri pentru a depana potențiale eșecuri.

Boilerplate și lock-in

după cum probabil ați observat acest model generează o mulțime de boilerplate – practic fiecare depozit sau serviciu ar trebui să aibă înveliș, numai pentru adnota dependențele.

odată ce te duci pe acest drum veți observa că este este destul de dificil de a scăpa de model tort. Fiecare implementare este pusă în interiorul unei trăsături, iar dependențele sale provin din domeniu, nu prin parametri. Deci, pentru fiecare clasă, ar trebui să efectuați mai întâi refactorul pentru a putea să-l instantiați fără înveliș.

dar apoi dependențele ar trebui, de asemenea, să fie refactored și așa mai departe. Modelul tortului este un angajament urât, care te doare și te opune atunci când încerci să te retragi treptat.

literalmente, orice altă formă de DI în Scala, pe care o știu (runtime reflection a ‘la Guice, macro adnotări a’ la MacWire, implicits, combinații ale celor 3) vă permite să spuneți stop! în orice moment și treceți treptat la o altă soluție.

rezumat

modelul tortului este o soluție la o problemă DI, care pe termen lung face mai mult rău decât bine. Din ceea ce am auzit că a fost folosit în Scala compilator, și ei regret. A fost mecanismul DI implicit pentru Cadrul de joc, dar din cauza tuturor acestor probleme autorii au decis să treacă la Guice – da, programatorii funcționali au preferat reflecția în timpul rulării la tortul sigur, astfel încât să spună multe.

deci, care sunt alternativele?

  • plain old manual argument passing – nu este nevoie să explici că
  • implicits – au avantajele sale până când proiectul dvs. este menținut de mai mult decât tine, iar colegii încep să se plângă că nu știu, ce se întâmplă, compilarea lentă, neînțelegerea implică…
  • runtime reflection – Guice este un cadru matur care face DI exact așa cum urăsc. A fost folosit în multe proiecte, programatorii Java îl iubesc – la fel cum iubesc primăvara. Unii dintre ei lasă abstractizarea să se scurgă trecând injectorul în clasă. Unii dintre ei doresc ca configurația lor să fie flexibilă – atât de flexibilă încât cablarea clasei ar putea eșua în timpul rulării, bleh!
  • MacWire-la jumătatea drumului prin trecerea argumente manual și folosind macro – uri pentru tot ceea ce-se scanează domeniul de aplicare curent în timp de compilare și injecteaza dependențe pentru tine. În cazul în care unele lipsesc compilație eșuează. Între timp, scrieți doar wire
  • Airframe – cadru bazat pe macro în care construiți rețetă pentru DI cu un DSL. Ea are suport pentru managementul ciclului de viață obiect
  • pulp – meu nerușinat auto-promovare. Acesta a folosit implicits și adnotări macro, în scopul de a genera furnizori pentru clasele

IMHO fiecare dintre ele ar trebui să funcționeze mai bine, decât model tort.

Lasă un răspuns

Adresa ta de email nu va fi publicată.