DIP (Dependency Inversion Principle) — az SOLID ötödik elve, amely meghatározza a modulok közötti függőségek felépítésének szabályait: a magasabb szintű modulok nem függhetnek az alacsonyabb szintű moduloktól, mindkettőnek absztrakcióktól kell függenie. Az absztrakciók nem függhetnek a részletektől — a részleteknek az absztrakcióktól kell függeniük. Ezt az elvet Robert Martin írta le a Clean Architecture (2017) című könyvében, amely a lazán kapcsolódó architektúra alapját képezi. A könyv adatai szerint a függőségek invertálásának elve megszünteti a merev kapcsolatokat az alkalmazás rétegei között.
Fontos
DIP (Dependency Inversion Principle) — a függőségek invertálásának elve, amely megfordítja a hagyományos felfogást a modulok közötti függőségek irányáról. A magas szintű modulok (üzleti logika) nem függhetnek közvetlenül az alacsony szintű moduloktól (adatbázis, hálózat, UI). Ehelyett mindkét szint a magasabb szintű modulban meghatározott absztrakcióktól függ.
A DIP formális megfogalmazása két szabályt tartalmaz: A — a magasabb szintű modulok nem függhetnek az alacsonyabb szintű moduloktól, mindkettőnek absztrakcióktól kell függenie. B — az absztrakciók nem függhetnek a részletektől, a részleteknek az absztrakcióktól kell függeniük. A második szabály az első következménye: ha az absztrakció a részletektől függ, nem lehet stabil alap a magasabb szintű modul számára.
DIP nélkül a tipikus architektúra így néz ki: BusinessLogic → DatabaseRepository — az üzleti logika közvetlenül egy konkrét repozitóriumtól függ. DIP-pel: BusinessLogic → DatabaseServiceInterface ← DatabaseRepository. A BusinessLogic nem tud a DatabaseRepository létezéséről, csak a DatabaseService interfészt ismeri, amelyet az üzleti logikán kívül implementálnak.
Az invertálás azt jelenti, hogy a vezérlés áramlása és a függőségek áramlása ellentétes irányba mutat. A vezérlés áramlása felülről lefelé halad: UI → ViewModel → UseCase → Repository. A függőségek áramlása alulról felfelé halad: A Repository implementálja a UseCase-ben meghatározott interfészt. A Repository (alacsonyabb szint) függ a UseCase-től (magasabb szint).
Ez az invertálás a legfontosabb különbség a DIP és a szokásos rétegekre bontás között. A hagyományos réteges architektúrában minden réteg az alatta lévő rétegtől függ. A DIP-pel rendelkező architektúrában minden réteg absztrakcióktól függ, és ezen absztrakciók megvalósítása az infrastruktúra rétegben található, amely DI mechanizmusokon keresztül csatlakozik
a felső rétegekhez.
A DIP mechanizmusa az absztrakciók magasabb szintű modulokban történő meghatározásával és azok alacsonyabb szintű modulokban történő megvalósításával valósul meg. A magasabb szintű modul deklarál egy interfészt a szükséges funkcióhoz. Az alacsonyabb szintű modul implementálja ezt az interfészt. Az összeillesztés (wiring) az alkalmazás kompozíciós gyökerének szintjén történik.
A DIP meglévő kódba történő bevezetésének folyamata: emelje ki az interfészt az alacsony szintű modul számára, helyezze át ezt az interfészt a magasabb szintű modulba (vagy egy külön absztrakciós rétegbe), írja át a felső modul függőségét az interfészre, kényszerítse az alacsony szintű modult az interfész implementálására. Ezen lépések után a függőség iránya az ellenkezőjére változott.
A DIP megköveteli a kompozíciós gyökér mechanizmusát — az alkalmazás azon pontját, ahol az összes függőség létrejön és összekapcsolódik. Androidban ez az Application.get() vagy a Hilt komponens, iOS-ben — az AppDelegate vagy SceneDelegate. A kompozíciós gyökér az egyetlen hely, ahol a kód ismeri a konkrét megvalósításokat.
A DIP architekturális határokat képez az alkalmazás rétegei között. Amikor a ViewModel függ a UserRepository interfésztől, a presentation és domain réteg között határ jön létre: a ViewModel (presentation) nem tudja, honnan származnak az adatok. Ez a határ lehetővé teszi a UserRepository megvalósításának megváltoztatását (Room → REST → Mock) anélkül, hogy befolyásolná a ViewModel-t. Minél több ilyen határ van, annál ellenállóbb az alkalmazás a keretrendszerek és könyvtárak változásaival szemben.
A Google által ajánlott Android architektúrában a DIP a UseCase-eken keresztül valósul meg, amelyek a domain rétegben találhatók és a Repository interfészektől függenek. A RepositoryImpl a data rétegben található és implementálja ezeket az interfészeket. A presentation réteg (ViewModel) a UseCase-től függ. A függőségek iránya a presentation-től a domain felé, a domain-től a data felé halad — de egyik réteg sem ismeri a másik réteg konkrét megvalósításait.
A DIP-et és a DI-t gyakran összekeverik, de különböző fogalmak. DIP — architekturális elv (MIT kell tenni: absztrakcióktól függeni). DI — megvalósítási minta (HOGYAN kell ezt tenni: függőségek átadása a konstruktoron keresztül). A DIP arra a kérdésre válaszol, hogy mire kell a moduloknak épülniük?
, a DI arra, hogy hogyan kapják meg az objektumok a függőségeiket?
.
Dependency Injection — a függőségek objektumba történő befecskendezésének módja konstruktoron, metóduson vagy tulajdonságon keresztül. Amikor egy Kotlin osztályban a Repository interfész a konstruktoron keresztül kerül átadásra — ez DI. És az a tény, hogy a ViewModel osztály a Repository interfésztől függ, nem a konkrét RoomRepository megvalósítástól — ez DIP. DI — eszköz, DIP — cél.
A DIP betartható DI keretrendszer nélkül is: a függőségek manuális összekapcsolása a kompozíciós gyökérben szintén DI (manual DI). Használható DI keretrendszer (Dagger, Hilt, Koin) a DIP megsértése mellett: ha a ViewModel közvetlenül new() segítségével hoz létre Repository objektumot — a DIP sérül, még akkor is, ha a keretrendszer telepítve van. DIP — architekturális döntés, DI — technikai részlet.
Vizsgáljuk meg a Android példát a DIP adatrétegre történő alkalmazására. DIP nélkül a ViewModel közvetlenül létrehozza a RoomDatabase-t és a DAO-t. DIP-pel — a ViewModel a UserRepository interfésztől függ, és a konkrét RoomUserRepository megvalósítás kívülről kerül biztosításra.
// Az absztrakció a domain réteghez tartozik (magasabb szint)
interface UserRepository {
fun getUser(id: Int): User
}
// A domain réteg csak az absztrakciótól függ
class GetUserUseCase(
private val repo: UserRepository
) {
fun execute(id: Int): User = repo.getUser(id)
}
// A data rétegben lévő megvalósítás a domain réteg absztrakciójától függ
class RoomUserRepository(
private val dao: UserDao
) : UserRepository {
override fun getUser(id: Int): User {
return dao.getById(id)
}
}
// Kompozíciós gyökér
class AppModule {
fun provideUserRepository(dao: UserDao): UserRepository {
return RoomUserRepository(dao)
}
}
iOS példa Application Coordinator és protokoll használatával a navigációhoz:
// Navigációs absztrakció a domain rétegben
protocol AuthNavigation {
func navigateToHome()
func navigateToLogin()
}
// A ViewModel az absztrakciótól függ, nem az UIKit-től
final class AuthViewModel {
private let navigation: AuthNavigation
init(navigation: AuthNavigation) {
self.navigation = navigation
}
func onLoginSuccess() {
navigation.navigateToHome()
}
}
// Coordinator (UIKit réteg) implementálja a domain réteg protokollját
final class AppCoordinator: AuthNavigation {
func navigateToHome() {
// UIKit navigációs kód
}
func navigateToLogin() {
// UIKit navigációs kód
}
}
Kulcsfontosságú pont: Az AuthViewModel (domain) nem tud az AppCoordinator (UIKit) létezéséről. Csak az AuthNavigation protokollt ismeri. Ha holnap az UIKit-et lecserélik SwiftUI-ra — az AuthViewModel nem igényel változtatásokat. A DIP függetlenné teszi a domain réteget a keretrendszerektől és UI könyvtáraktól.
Hilt — a szabványos DI eszköz Androidhoz, amelyet a Google ajánl. Beépítve a Jetpackbe, támogatja a ViewModel-t, Fragment-et, Service-t és más Android komponenseket. A Hilt automatizálja a kompozíciós gyökér létrehozását @Module, @Provides, @Inject annotációkon keresztül. A Hilt használata nem garantálja a DIP betartását — a UserRepository interfészt a domain rétegben kell meghatározni, nem a data rétegben.
Koin — könnyűsúlyú DI keretrendszer Kotlinhoz kódgenerálás és annotáció-feldolgozó nélkül. A Koin DSL (module, single, factory) könnyebben megtanulható, de a függőségek ellenőrzése futásidőben történik, nem fordítási időben. A Koin népszerű a többplatformos projektekben (KMP) az iOS támogatásnak köszönhetően.
Dagger 2 — a Hilt elődje, még mindig használják nagy projektekben. A Dagger fordítási időben generál DI kódot, ami maximális teljesítményt és hibadiagnosztikát biztosít a build szakaszban. A Hilt a Daggerre épül és egyszerűsített API-t nyújt. Új projektekhez a Google a Hilt-et ajánlja elsődleges DI keretrendszerként.
A DI moduloknak meg kell felelniük az architekturális rétegeknek, és DomainModule, DataModule, PresentationModule részekre kell osztani őket. A DomainModule csak absztrakciókat és UseCase-eket biztosít. A DataModule megvalósításokat biztosít az absztrakciókhoz. A PresentationModule összeköti a ViewModel-t a UseCase-szel. Ez a szervezés garantálja, hogy a domain réteg független marad az infrastrukturális könyvtáraktól.
A DI keretrendszerek közötti migráció során (például Koin-ról Hilt-re) a DomainModule szerkezete nem változik — csak a DataModule és PresentationModule összekapcsolási módszerei változnak. A DIP biztosítja a domain logika elkülönítését, a DI keretrendszer pedig a technikai összekapcsolási mechanizmus.
Gyakran Ismételt Kérdések
DIP szükséges az architekturális határokon — az alkalmazás rétegei között (domain → data, presentation → domain). Egy rétegen belül a DIP felesleges lehet. Például egy StringFormatter segédosztály a domain rétegen belül nem igényel interfészt — ha nincsenek előfeltételek a lecserélésére.
Nem. DIP — elv: a moduloknak absztrakcióktól kell függeniük. DI — minta: az objektum kívülről kapja a függőségeit, nem maga hozza létre azokat. A DI a DIP megvalósításának módja, de a DIP betartható DI nélkül is (gyárak vagy szolgáltatás-lokátor segítségével). A DI DIP nélkül lehetséges, de nincs architekturális értéke.
Az interfészek ahhoz a modulhoz tartoznak, amelyik használja azokat, nem ahhoz, amelyik implementálja őket. A UserRepository a domain rétegben kerül deklarálásra és a data rétegben implementálásra. Ez a DIP legfontosabb szabálya: az absztrakció tulajdonosa a fogyasztó, nem a megvalósítás szolgáltatója.
A DIP lehetővé teszi a tesztelést elkülönített rétegeken. A UserRepository-tól (interfész) függő ViewModel mock megvalósítással tesztelhető adatbázis nélkül. DIP nélkül a ViewModel a RoomUserRepository-tól függne, és minden teszthez adatbázis konfigurációra lenne szükség. A DIP + DI teljes modul-elkülönítést biztosít a tesztelés során.
Hilt — a szabványos választás Android projektekhez, a Google ajánlásával. Koin — alternatíva Kotlin Multiplatform projektekhez. Dagger 2 — meglévő projektekhez, ahol a Hilt-re való migráció indokolatlan. A keretrendszer kiválasztása nem szünteti meg a DIP architekturális szintű betartásának szükségességét.
Összefoglalás
Kulcsrakész mobilalkalmazást fejlesztünk
Az IT Sectr 2017 óta készít iOS és Android alkalmazásokat induló vállalkozásoknak és vállalkozásoknak. Tanácsot adunk, és a legjobb megoldást javasoljuk.
Olvassa el is