DIP (Dependency Inversion Principle) — пети принцип SOLID, који одређује правила изградње зависности између модула: модули вишег нивоа не треба да зависе од модула нижег нивоа, оба треба да зависе од апстракција. Апстракције не треба да зависе од детаља — детаљи треба да зависе од апстракција. Овај принцип, који је описао Роберт Мартин у Clean Architecture (2017), лежи у основи слабо повезане архитектуре. Према подацима ове књиге, принцип инверзије зависности уклања чврсте везе између слојева апликације.
Главно
DIP (Dependency Inversion Principle) — принцип инверзије зависности, који преокреће традиционално схватање о смеру зависности између модула. Високо нивоски модули (пословна логика) не треба директно да зависе од ниско нивоских модула (база података, мрежа, UI). Уместо тога, оба нивоа зависе од апстракција које су дефинисане у модулу вишег нивоа.
Формална формулација DIP укључује два правила: А — модули вишег нивоа не треба да зависе од модула нижег нивоа, оба треба да зависе од апстракција. Б — апстракције не треба да зависе од детаља, детаљи треба да зависе од апстракција. Друго правило је последица првог: ако апстракција зависи од детаља, не може бити стабилна основа за модул вишег нивоа.
Без DIP типична архитектура изгледа овако: BusinessLogic → DatabaseRepository — пословна логика директно зависи од конкретног репозиторијума. Са DIP: BusinessLogic → DatabaseServiceInterface ← DatabaseRepository. BusinessLogic не зна за постојање DatabaseRepository, зна само интерфејс DatabaseService, који се имплементира изван пословне логике.
Инверзија значи да су ток контроле и ток зависности усмерени у супротним смеровима. Ток контроле иде одозго надоле: UI → ViewModel → UseCase → Repository. Ток зависности иде одоздо нагоре: Repository имплементира интерфејс дефинисан у UseCase. Repository (нижи ниво) зависи од UseCase (виши ниво).
Ова инверзија је кључна разлика између DIP и обичне поделе на слојеве. У традиционалној слојевитој архитектури, сваки слој зависи од слоја испод. У архитектури са DIP, сви слојеви зависе од апстракција, а имплементација ових апстракција налази се у инфраструктурном слоју, који се повезује
са горњим слојевима путем DI механизама.
Механизам DIP се реализује кроз дефинисање апстракција у модулима вишег нивоа и њихову имплементацију у модулима нижег нивоа. Модул вишег нивоа декларише интерфејс за потребну функционалност. Модул нижег нивоа имплементира овај интерфејс. Састављање (wiring) се одвија на нивоу композиционог корена апликације.
Процес увођења DIP у постојећи код: издвојити интерфејс за ниско нивоски модул, преместити овај интерфејс у модул вишег нивоа (или у посебан слој апстракција), преписити зависност горњег модула на интерфејс, натерати ниско нивоски модул да имплементира овај интерфејс. Након ових корака, смер зависности се променио у супротан.
DIP захтева механизам композиционог корена — тачку у апликацији где се креирају све зависности и повезују једна са другом. У Android-у је то Application.get() или Hilt компонента, у iOS-у — AppDelegate или SceneDelegate. Композициони корен је једино место где код зна за конкретне имплементације.
DIP формира архитектонске границе између слојева апликације. Када ViewModel зависи од интерфејса UserRepository, између presentation и domain слоја настаје граница: ViewModel (presentation) не зна одакле долазе подаци. Ова граница омогућава промену имплементације UserRepository (Room → REST → Mock) без утицаја на ViewModel. Што је више таквих граница, апликација је отпорнија на промене фрејмворка и библиотека.
У Android архитектури коју препоручује Google, DIP се реализује кроз UseCase, који се налазе у domain слоју и зависе од интерфејса Repository. RepositoryImpl се налазе у data слоју и имплементирају ове интерфејсе. Presentation слој (ViewModel) зависи од UseCase. Смер зависности иде од presentation ка domain, од domain ка data — али ниједан слој не зна за конкретне имплементације другог слоја.
DIP и DI се често мешају, али су различити концепти. DIP — архитектонски принцип (ШТА треба радити: зависити од апстракција). DI — образац имплементације (КАКО то урадити: преносити зависности кроз конструктор). DIP одговара на питање на чему треба да се заснивају модули?
, DI — како објекти добијају своје зависности?
.
Dependency Injection — начин убризгавања зависности у објекат кроз конструктор, метод или својство. Када се у Kotlin класу интерфејс Repository преноси кроз конструктор — то је DI. А то што класа ViewModel зависи од интерфејса Repository, а не од конкретне имплементације RoomRepository — то је DIP. DI — алат, DIP — циљ.
Може се поштовати DIP без DI фрејмворка: ручно повезивање зависности у композиционом корену је такође DI (manual DI). Може се користити DI фрејмворк (Dagger, Hilt, Koin) док се DIP крши: ако ViewModel директно креира објекат Repository кроз new() — DIP је прекршен, чак и ако је фрејмворк инсталиран. DIP — архитектонска одлука, DI — технички детаљ.
Размотримо Android пример примене DIP на слој података. Без DIP, ViewModel директно креира RoomDatabase и DAO. Са DIP — ViewModel зависи од интерфејса UserRepository, а конкретна имплементација RoomUserRepository се доставља споља.
// Апстракција припада domain слоју (виши ниво)
interface UserRepository {
fun getUser(id: Int): User
}
// Domain слој зависи само од апстракције
class GetUserUseCase(
private val repo: UserRepository
) {
fun execute(id: Int): User = repo.getUser(id)
}
// Имплементација у data слоју зависи од апстракције domain слоја
class RoomUserRepository(
private val dao: UserDao
) : UserRepository {
override fun getUser(id: Int): User {
return dao.getById(id)
}
}
// Композициони корен
class AppModule {
fun provideUserRepository(dao: UserDao): UserRepository {
return RoomUserRepository(dao)
}
}
iOS пример са Application Coordinator и протоколом за навигацију:
// Апстракција навигације у domain слоју
protocol AuthNavigation {
func navigateToHome()
func navigateToLogin()
}
// ViewModel зависи од апстракције, не од UIKit
final class AuthViewModel {
private let navigation: AuthNavigation
init(navigation: AuthNavigation) {
self.navigation = navigation
}
func onLoginSuccess() {
navigation.navigateToHome()
}
}
// Coordinator (UIKit слој) имплементира протокол domain слоја
final class AppCoordinator: AuthNavigation {
func navigateToHome() {
// UIKit код навигације
}
func navigateToLogin() {
// UIKit код навигације
}
}
Кључни моменат: AuthViewModel (domain) не зна за постојање AppCoordinator (UIKit). Он зна само протокол AuthNavigation. Ако сутра UIKit буде замењен SwiftUI-јем — AuthViewModel не захтева измене. DIP чини domain слој независним од фрејмворка и UI библиотека.
Hilt — стандардни DI алат за Android, који препоручује Google. Уграђен у Jetpack, подржава ViewModel, Fragment, Service и друге Android компоненте. Hilt аутоматизује креирање композиционог корена кроз анотације @Module, @Provides, @Inject. Коришћење Hilt-а не гарантује поштовање DIP — интерфејс UserRepository мора бити дефинисан у domain слоју, а не у data слоју.
Koin — лагани DI фрејмворк за Kotlin без генерисања кода и процесора анотација. Koin DSL (module, single, factory) је једноставнији за учење, али провера зависности се одвија у рантајму, а не у фази компилације. Koin је популаран у мултиплатформским пројектима (KMP) због подршке за iOS.
Dagger 2 — претходник Hilt-а, још увек се користи у великим пројектима. Dagger генерише DI код у фази компилације, што даје максималне перформансе и дијагностику грешака у фази изградње. Hilt је изграђен на Dagger-у и пружа поједностављени API. За нове пројекте Google препоручује Hilt као главни DI фрејмворк.
DI модули треба да одговарају архитектонским слојевима и буду подељени на DomainModule, DataModule, PresentationModule. DomainModule пружа само апстракције и UseCase. DataModule пружа имплементације за апстракције. PresentationModule повезује ViewModel са UseCase. Оваква организација гарантује да domain слој остаје независан од инфраструктурних библиотека.
При миграцији између DI фрејмворка (на пример, са Koin на Hilt) структура DomainModule се не мења — мењају се само начини повезивања у DataModule и PresentationModule. DIP обезбеђује изолацију domain логике, а DI фрејмворк је технички механизам повезивања.
Често постављана питања
DIP је неопходан на архитектонским границама — између слојева апликације (domain → data, presentation → domain). Унутар једног слоја DIP може бити сувишан. На пример, утилитарна класа StringFormatter унутар domain слоја не захтева интерфејс — ако нема предуслова за њену замену.
Не. DIP — принцип: модули треба да зависе од апстракција. DI — образац: објекат добија зависности споља, а не ствара их сам. DI — начин имплементације DIP, али DIP се може поштовати и без DI (кроз фабрике или сервис локатор). DI без DIP је могућ, али нема архитектонску вредност.
Интерфејси припадају модулу који их користи, а не оном који их имплементира. UserRepository се декларише у domain слоју, а имплементира у data слоју. Ово је кључно правило DIP: власник апстракције је потрошач, а не добављач имплементације.
DIP омогућава тестирање на изолованим слојевима. ViewModel који зависи од UserRepository (интерфејс) тестира се са mock имплементацијом без базе података. Без DIP, ViewModel би зависио од RoomUserRepository и захтевао подешавање базе података за сваки тест. DIP + DI пружају потпуну изолацију модула при тестирању.
Hilt — стандардни избор за Android пројекте, препоручен од Google-а. Koin — алтернатива за Kotlin Multiplatform пројекте. Dagger 2 — за постојеће пројекте где миграција на Hilt није оправдана. Избор фрејмворка не поништава потребу поштовања DIP на нивоу архитектуре.
Резиме
Развићемо мобилну апликацију под кључ
IT Sectr креира iOS и Android апликације за стартапе и предузећа од 2017. године. Саветоваћемо вас и предложити најбоље решење.
Прочитајте такође