DIP (Dependency Inversion Principle) — al cincilea principiu SOLID, care definește regulile de construire a dependențelor între module: modulele de nivel superior nu ar trebui să depindă de modulele de nivel inferior, ambele trebuie să depindă de abstractizări. Abstractizările nu ar trebui să depindă de detalii — detaliile trebuie să depindă de abstractizări. Acest principiu, descris de Robert Martin în Clean Architecture (2017), stă la baza arhitecturii slab cuplate. Conform acestei cărți, principiul inversiunii dependențelor elimină legăturile rigide între straturile aplicației.
Principalele
DIP (Dependency Inversion Principle) — principiul inversiunii dependențelor, care inversează concepția tradițională despre direcția dependențelor între module. Modulele de nivel înalt (logica de business) nu trebuie să depindă direct de modulele de nivel jos (bază de date, rețea, UI). În schimb, ambele niveluri depind de abstractizări definite în modulul de nivel superior.
Formularea oficială a DIP include două reguli: A — modulele de nivel superior nu trebuie să depindă de modulele de nivel inferior, ambele trebuie să depindă de abstractizări. B — abstractizările nu trebuie să depindă de detalii, detaliile trebuie să depindă de abstractizări. A doua regulă este consecința primeia: dacă abstractizarea depinde de detalii, nu poate fi o bază stabilă pentru modulul de nivel superior.
Fără DIP, arhitectura tipică arată astfel: BusinessLogic → DatabaseRepository — logica de business depinde direct de un repository specific. Cu DIP: BusinessLogic → DatabaseServiceInterface ← DatabaseRepository. BusinessLogic nu știe de existența DatabaseRepository, cunoaște doar interfața DatabaseService, care este implementată în afara logicii de business.
Inversiunea înseamnă că fluxul de control și fluxul de dependențe sunt direcționate în sensuri opuse. Fluxul de control merge de sus în jos: UI → ViewModel → UseCase → Repository. Fluxul de dependențe merge de jos în sus: Repository implementează interfața definită în UseCase. Repository (nivel inferior) depinde de UseCase (nivel superior).
Această inversiune este diferența cheie între DIP și simpla împărțire pe straturi. În arhitectura tradițională pe straturi, fiecare strat depinde de stratul de dedesubt. În arhitectura cu DIP, toate straturile depind de abstractizări, iar implementarea acestor abstractizări se află în stratul de infrastructură, care este conectat
la straturile superioare prin mecanisme DI.
Mecanismul DIP se realizează prin definirea abstractizărilor în modulele de nivel superior și implementarea lor în modulele de nivel inferior. Modulul de nivel superior declară o interfață pentru funcționalitatea de care are nevoie. Modulul de nivel inferior implementează această interfață. Asamblarea (wiring) are loc la nivelul rădăcinii de compoziție a aplicației.
Procesul de aplicare a DIP în codul existent: extrageți interfața pentru modulul de nivel inferior, mutați această interfață în modulul de nivel superior (sau într-un strat separat de abstractizări), rescrieți dependența modulului superior pe interfață, forțați modulul de nivel inferior să implementeze această interfață. După acești pași, direcția dependenței s-a schimbat în sens opus.
DIP necesită un mecanism de rădăcină de compoziție — punctul în aplicație unde sunt create toate dependențele și legate între ele. În Android, acesta este Application.get() sau componenta Hilt, în iOS — AppDelegate sau SceneDelegate. Rădăcina de compoziție este singurul loc unde codul cunoaște implementările concrete.
DIP formează granițe arhitecturale între straturile aplicației. Când ViewModel depinde de interfața UserRepository, între straturile presentation și domain apare o graniță: ViewModel (presentation) nu știe de unde provin datele. Această graniță permite schimbarea implementării UserRepository (Room → REST → Mock) fără a afecta ViewModel. Cu cât mai multe astfel de granițe, cu atât aplicația este mai rezistentă la schimbări de frameworkuri și biblioteci.
În arhitectura Android recomandată de Google, DIP se implementează prin UseCase, care se află în stratul domain și depind de interfețele Repository. RepositoryImpl se află în stratul data și implementează aceste interfețe. Stratul presentation (ViewModel) depinde de UseCase. Direcția dependențelor merge de la presentation la domain, de la domain la data — dar niciun strat nu cunoaște implementările concrete ale altui strat.
DIP și DI sunt adesea confundate, dar sunt concepte diferite. DIP — principiu arhitectural (CE trebuie făcut: să depindem de abstractizări). DI — pattern de implementare (CUM să facem: să transmitem dependențe prin constructor). DIP răspunde la întrebarea pe ce ar trebui să se bazeze modulele?
, DI — cum obțin obiectele dependențele?
.
Dependency Injection — mod de injectare a dependențelor într-un obiect prin constructor, metodă sau proprietate. Când într-o clasă Kotlin interfața Repository este transmisă prin constructor — acesta este DI. Iar faptul că clasa ViewModel depinde de interfața Repository, nu de implementarea concretă RoomRepository — acesta este DIP. DI — instrument, DIP — scop.
Se poate respecta DIP fără un framework DI: legarea manuală a dependențelor în rădăcina de compoziție este tot DI (manual DI). Se poate folosi un framework DI (Dagger, Hilt, Koin) încălcând DIP: dacă ViewModel creează direct obiectul Repository prin new() — DIP este încălcat, chiar dacă frameworkul este instalat. DIP — decizie arhitecturală, DI — detaliu tehnic.
Să analizăm exemplul Android de aplicare a DIP la stratul de date. Fără DIP, ViewModel creează direct RoomDatabase și DAO. Cu DIP — ViewModel depinde de interfața UserRepository, iar implementarea concretă RoomUserRepository este furnizată din exterior.
// Abstractizarea aparține stratului domain (nivel superior)
interface UserRepository {
fun getUser(id: Int): User
}
// Stratul domain depinde doar de abstractizare
class GetUserUseCase(
private val repo: UserRepository
) {
fun execute(id: Int): User = repo.getUser(id)
}
// Implementarea în stratul data depinde de abstractizarea stratului domain
class RoomUserRepository(
private val dao: UserDao
) : UserRepository {
override fun getUser(id: Int): User {
return dao.getById(id)
}
}
// Rădăcina de compoziție
class AppModule {
fun provideUserRepository(dao: UserDao): UserRepository {
return RoomUserRepository(dao)
}
}
Exemplu iOS cu Application Coordinator și protocol pentru navigare:
// Abstractizarea navigării în stratul domain
protocol AuthNavigation {
func navigateToHome()
func navigateToLogin()
}
// ViewModel depinde de abstractizare, nu de UIKit
final class AuthViewModel {
private let navigation: AuthNavigation
init(navigation: AuthNavigation) {
self.navigation = navigation
}
func onLoginSuccess() {
navigation.navigateToHome()
}
}
// Coordinator (stratul UIKit) implementează protocolul stratului domain
final class AppCoordinator: AuthNavigation {
func navigateToHome() {
// Cod de navigare UIKit
}
func navigateToLogin() {
// Cod de navigare UIKit
}
}
Momentul cheie: AuthViewModel (domain) nu știe de existența AppCoordinator (UIKit). El cunoaște doar protocolul AuthNavigation. Dacă mâine UIKit este înlocuit cu SwiftUI — AuthViewModel nu necesită modificări. DIP face stratul domain independent de frameworkuri și biblioteci UI.
Hilt — instrumentul standard DI pentru Android, recomandat de Google. Integrat în Jetpack, suportă ViewModel, Fragment, Service și alte componente Android. Hilt automatizează crearea rădăcinii de compoziție prin adnotări @Module, @Provides, @Inject. Utilizarea Hilt nu garantează respectarea DIP — interfața UserRepository trebuie definită în stratul domain, nu în stratul data.
Koin — framework DI ușor pentru Kotlin fără generare de cod și procesor de adnotări. DSL-ul Koin (module, single, factory) este mai ușor de învățat, dar verificarea dependențelor are loc în runtime, nu la compilare. Koin este popular în proiecte multi-platformă (KMP) datorită suportului pentru iOS.
Dagger 2 — predecesorul lui Hilt, încă folosit în proiecte mari. Dagger generează cod DI la compilare, oferind performanță maximă și diagnosticare a erorilor la build. Hilt este construit pe Dagger și oferă un API simplificat. Pentru proiecte noi, Google recomandă Hilt ca framework DI principal.
Modulele DI trebuie să corespundă straturilor arhitecturale și să fie împărțite în DomainModule, DataModule, PresentationModule. DomainModule oferă doar abstractizări și UseCase. DataModule oferă implementări pentru abstractizări. PresentationModule leagă ViewModel de UseCase. O astfel de organizare garantează că stratul domain rămâne independent de bibliotecile de infrastructură.
La migrarea între frameworkuri DI (de exemplu, de la Koin la Hilt), structura DomainModule nu se schimbă — se schimbă doar modurile de legare în DataModule și PresentationModule. DIP asigură izolarea logicii domain, iar frameworkul DI este mecanismul tehnic de legare.
Întrebări frecvente
DIP este necesar la granițele arhitecturale — între straturile aplicației (domain → data, presentation → domain). În interiorul unui strat, DIP poate fi redundant. De exemplu, o clasă utilitară StringFormatter în stratul domain nu necesită o interfață — dacă nu există premise pentru înlocuirea ei.
Nu. DIP — principiu: modulele trebuie să depindă de abstractizări. DI — pattern: obiectul primește dependențele din exterior, nu le creează singur. DI este modul de implementare a DIP, dar DIP poate fi respectat și fără DI (prin fabrici sau service locator). DI fără DIP este posibil, dar nu are valoare arhitecturală.
Interfețele aparțin modulului care le folosește, nu celui care le implementează. UserRepository este declarat în stratul domain și implementat în stratul data. Aceasta este regula cheie a DIP: proprietarul abstractizării este consumatorul, nu furnizorul de implementare.
DIP face testarea posibilă pe straturi izolate. ViewModel care depinde de UserRepository (interfață) este testat cu o implementare mock fără bază de date. Fără DIP, ViewModel ar depinde de RoomUserRepository și ar necesita configurarea bazei de date pentru fiecare test. DIP + DI oferă izolarea completă a modulelor la testare.
Hilt — alegerea standard pentru proiecte Android, recomandat de Google. Koin — alternativă pentru proiecte Kotlin Multiplatform. Dagger 2 — pentru proiecte existente unde migrarea la Hilt este nejustificată. Alegerea frameworkului nu elimină necesitatea respectării DIP la nivel de arhitectură.
Rezumat
Vom dezvolta o aplicație mobilă la cheie
IT Sectr creează aplicații iOS și Android pentru startup-uri și afaceri din 2017. Vă vom consilia și vă vom propune cea mai bună soluție.
Citiți și