DIP (Dependency Inversion Principle) — pátý princip SOLID, který určuje pravidla budování závislostí między modułami: moduły wyższego poziomu nie powinny zależeć od modułów niższego poziomu, oba powinny zależeć od abstrakcji. Abstrakcje nie powinny zależeć od szczegółów — szczegóły powinny zależeć od abstrakcji. Ta zasada, opisana przez Roberta Martina w Clean Architecture (2017), je základem volně propojené architektury. Według tej książki zasada inwersji zależności eliminuje sztywne powiązania między warstwami aplikacji.
Hlavní
DIP (Dependency Inversion Principle) — zasada inwersji zależności, która odwraca tradycyjne pojęcie kierunku zależności między modułami. Moduły wysokiego poziomu (logika biznesowa) nie powinny bezpośrednio zależeć od modułów niskiego poziomu (baza danych, sieć, UI). Zamiast tego oba poziomy zależą od abstrakcji, które są definiowane w module wyższego poziomu.
Formalne sformułowanie DIP zawiera dwie reguły: A — moduły wyższego poziomu nie powinny zależeć od modułów niższego poziomu, oba powinny zależeć od abstrakcji. B — abstrakcje nie powinny zależeć od szczegółów, szczegóły powinny zależeć od abstrakcji. Druga reguła jest konsekwencją pierwszej: jeśli abstrakcja zależy od szczegółów, nie może być stabilną podstawą dla modułu wyższego poziomu.
Bez DIP typowa architektura wygląda tak: BusinessLogic → DatabaseRepository — logika biznesowa bezpośrednio zależy od konkretnego repozytorium. Z DIP: BusinessLogic → DatabaseServiceInterface ← DatabaseRepository. BusinessLogic nie wie o istnieniu DatabaseRepository, zna tylko interfejs DatabaseService, który jest implementowany poza logiką biznesową.
Inwersja oznacza, że przepływ sterowania i przepływ zależności są skierowane w przeciwne strony. Przepływ sterowania idzie z góry na dół: UI → ViewModel → UseCase → Repository. Przepływ zależności idzie z dołu do góry: Repository implementuje interfejs zdefiniowany w UseCase. Repository (niższy poziom) zależy od UseCase (wyższy poziom).
Ta inwersja jest kluczową różnicą między DIP a zwykłym podziałem na warstwy. W tradycyjnej architekturze warstwowej każda warstwa zależy od warstwy poniżej. W architekturze z DIP wszystkie warstwy zależą od abstrakcji, a implementacja tych abstrakcji znajduje się w warstwie infrastruktury, która jest podłączana
do górnych warstw przez mechanizmy DI.
Mechanizm DIP jest realizowany poprzez definiowanie abstrakcji w modułach wyższego poziomu i ich implementację w modułach niższego poziomu. Moduł wyższego poziomu deklaruje interfejs dla potrzebnej mu funkcjonalności. Moduł niższego poziomu implementuje ten interfejs. Składanie (wiring) odbywa się na poziomie korzenia kompozycji aplikacji.
Proces wprowadzania DIP do istniejącego kodu: wyodrębnij interfejs dla modułu niskiego poziomu, przenieś ten interfejs do modułu wyższego poziomu (lub do osobnej warstwy abstrakcji), przepisz zależność górnego modułu na interfejs, spraw aby moduł niskiego poziomu implementował ten interfejs. Po tych krokach kierunek zależności zmienił się na przeciwny.
DIP wymaga mechanizmu korzenia kompozycji — punktu w aplikacji, gdzie tworzone są wszystkie zależności i łączone ze sobą. W Androidzie jest to Application.get() lub komponent Hilt, w iOS — AppDelegate lub SceneDelegate. Korzeń kompozycji to jedyne miejsce, gdzie kod zna konkretne implementacje.
DIP tworzy granice architektoniczne między warstwami aplikacji. Gdy ViewModel zależy od interfejsu UserRepository, między warstwą presentation a domain powstaje granica: ViewModel (presentation) nie wie, skąd pochodzą dane. Ta granica pozwala zmieniać implementację UserRepository (Room → REST → Mock) bez wpływania na ViewModel. Im więcej takich granic, tym aplikacja jest bardziej odporna na zmiany frameworków i bibliotek.
W architekturze Androida zalecanej przez Google, DIP jest realizowany poprzez UseCase, które znajdują się w warstwie domain i zależą od interfejsów Repository. RepositoryImpl znajdują się w warstwie data i implementują te interfejsy. Warstwa presentation (ViewModel) zależy od UseCase. Kierunek zależności idzie od presentation do domain, od domain do data — ale żadna warstwa nie zna konkretnych implementacji innej warstwy.
DIP i DI są często mylone, ale to różne koncepcje. DIP — zasada architektoniczna (CO należy robić: zależeć od abstrakcji). DI — wzorzec implementacji (JAK to zrobić: przekazywać zależności przez konstruktor). DIP odpowiada na pytanie na czym powinny opierać się moduły?
, DI — jak obiekty otrzymują swoje zależności?
.
Dependency Injection — sposób wstrzykiwania zależności do obiektu przez konstruktor, metodę lub właściwość. Gdy w klasie Kotlin interfejs Repository jest przekazywany przez konstruktor — to jest DI. A to, że klasa ViewModel zależy od interfejsu Repository, a nie od konkretnej implementacji RoomRepository — to jest DIP. DI — narzędzie, DIP — cel.
Można przestrzegać DIP bez frameworka DI: ręczne wiązanie zależności w korzeniu kompozycji to też DI (manual DI). Można używać frameworka DI (Dagger, Hilt, Koin), naruszając DIP: jeśli ViewModel bezpośrednio tworzy obiekt Repository przez new() — DIP jest naruszony, nawet jeśli framework jest zainstalowany. DIP — decyzja architektoniczna, DI — szczegół techniczny.
Rozważmy przykład z Androida zastosowania DIP do warstwy danych. Bez DIP ViewModel bezpośrednio tworzy RoomDatabase i DAO. Z DIP — ViewModel zależy od interfejsu UserRepository, a konkretna implementacja RoomUserRepository jest dostarczana z zewnątrz.
// Abstrakce patří do doménové vrstvy (vyšší úroveň)
interface UserRepository {
fun getUser(id: Int): User
}
// Doménová vrstva závisí pouze na abstrakci
class GetUserUseCase(
private val repo: UserRepository
) {
fun execute(id: Int): User = repo.getUser(id)
}
// Implementace v datové vrstvě závisí na abstrakci doménové vrstvy
class RoomUserRepository(
private val dao: UserDao
) : UserRepository {
override fun getUser(id: Int): User {
return dao.getById(id)
}
}
// Kořen kompozice
class AppModule {
fun provideUserRepository(dao: UserDao): UserRepository {
return RoomUserRepository(dao)
}
}
Przykład z iOS z Application Coordinator i protokołem do nawigacji:
// Abstrakce navigace v doménové vrstvě
protocol AuthNavigation {
func navigateToHome()
func navigateToLogin()
}
// ViewModel závisí na abstrakci, ne na UIKit
final class AuthViewModel {
private let navigation: AuthNavigation
init(navigation: AuthNavigation) {
self.navigation = navigation
}
func onLoginSuccess() {
navigation.navigateToHome()
}
}
// Coordinator (UIKit vrstva) implementuje protokol doménové vrstvy
final class AppCoordinator: AuthNavigation {
func navigateToHome() {
// UIKit navigační kód
}
func navigateToLogin() {
// UIKit navigační kód
}
}
Kluczowy moment: AuthViewModel (domain) nie wie o istnieniu AppCoordinator (UIKit). Zna tylko protokół AuthNavigation. Jeśli jutro UIKit zostanie zastąpiony przez SwiftUI — AuthViewModel nie wymaga zmian. DIP czyni warstwę domain niezależną od frameworków i bibliotek UI.
Hilt — standardowe narzędzie DI dla Androida, zalecane przez Google. Wbudowane w Jetpack, obsługuje ViewModel, Fragment, Service i inne komponenty Androida. Hilt automatyzuje tworzenie korzenia kompozycji przez adnotacje @Module, @Provides, @Inject. Używanie Hilt nie gwarantuje przestrzegania DIP — interfejs UserRepository musi być zdefiniowany w warstwie domain, a nie w warstwie data.
Koin — lekki framework DI dla Kotlin bez generowania kodu i procesora adnotacji. DSL Koina (module, single, factory) jest łatwiejszy do nauki, ale sprawdzanie zależności odbywa się w runtime, a nie na etapie kompilacji. Koin jest popularny w projektach wieloplatformowych (KMP) dzięki obsłudze iOS.
Dagger 2 — poprzednik Hilt, nadal używany w dużych projektach. Dagger generuje kod DI na etapie kompilacji, co daje maksymalną wydajność i diagnostykę błędów na etapie budowania. Hilt jest zbudowany na Dagger i zapewnia uproszczone API. Dla nowych projektów Google zaleca Hilt jako główny framework DI.
Moduły DI powinny odpowiadać warstwom architektonicznym i być podzielone na DomainModule, DataModule, PresentationModule. DomainModule dostarcza tylko abstrakcje i UseCase. DataModule dostarcza implementacje dla abstrakcji. PresentationModule łączy ViewModel z UseCase. Taka organizacja gwarantuje, że warstwa domain pozostaje niezależna od bibliotek infrastrukturalnych.
Podczas migracji między frameworkami DI (np. z Koin na Hilt) struktura DomainModule się nie zmienia — zmieniają się tylko sposoby wiązania w DataModule i PresentationModule. DIP zapewnia izolację logiki domain, a framework DI jest technicznym mechanizmem wiązania.
Nejčastější dotazy
DIP jest niezbędny na granicach architektonicznych — między warstwami aplikacji (domain → data, presentation → domain). Wewnątrz jednej warstwy DIP może być zbędny. Na przykład, klasa narzędziowa StringFormatter wewnątrz warstwy domain nie wymaga interfejsu — jeśli nie ma przesłanek do jej zastąpienia.
Nie. DIP — zasada: moduły powinny zależeć od abstrakcji. DI — wzorzec: obiekt otrzymuje zależności z zewnątrz, a nie tworzy je sam. DI — sposób implementacji DIP, ale DIP można przestrzegać bez DI (przez fabryki lub lokalizator usług). DI bez DIP jest możliwy, ale nie ma wartości architektonicznej.
Interfejsy należą do modułu, który ich używa, a nie do tego, który je implementuje. UserRepository jest deklarowany w warstwie domain, a implementowany w warstwie data. To kluczowa zasada DIP: właścicielem abstrakcji jest konsument, a nie dostawca implementacji.
DIP umożliwia testowanie na izolowanych warstwach. ViewModel zależny od UserRepository (interfejs) jest testowany z mock-implementacją bez bazy danych. Bez DIP ViewModel zależałby od RoomUserRepository i wymagał konfiguracji bazy danych dla każdego testu. DIP + DI zapewniają pełną izolację modułów podczas testowania.
Hilt — standardowy wybór dla projektów Android, zalecany przez Google. Koin — alternatywa dla projektów Kotlin Multiplatform. Dagger 2 — dla istniejących projektów, gdzie migracja na Hilt jest nieuzasadniona. Wybór frameworka nie zwalnia z konieczności przestrzegania DIP na poziomie architektury.
Shrnutí
Vyvineme mobilní aplikaci na klíč
IT Sectr vytváří aplikace pro iOS a Android pro startupy a podniky od roku 2017. Poradíme vám a navrhneme nejlepší řešení.
Přečtěte si také