DIP: podstawy, inwersja zależności w rozwoju

Autor: IT Sectr Opublikowano: 2026-05-12 Czas czytania: 9 min

DIP (Dependency Inversion Principle) — piąta zasada SOLID, która określa reguły budowania zależności 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), leży u podstaw luźno powiązanej architektury. Według tej książki zasada inwersji zależności eliminuje sztywne powiązania między warstwami aplikacji.

Najważniejsze

  • DIP — zasada inwersji zależności, piąta w SOLID, o granicach architektonicznych
  • Moduły wyższego poziomu nie powinny importować modułów niższego poziomu — tylko abstrakcje
  • DIP ≠ DI: Dependency Inversion — zasada architektoniczna, Dependency Injection — sposób jej implementacji
  • Abstrakcje należą do modułu wyższego poziomu, a implementacje — do niższego
  • DIP odwraca tradycyjną hierarchię zależności w architekturach wielowarstwowych

Czym jest DIP (Dependency Inversion Principle)?

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ą.

Kierunek zależności w DIP

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.

Jak działa zasada inwersji zależności

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.

Izolacja warstw przez DIP

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.

Różnica między DIP a DI (Dependency Injection)

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.

Przykłady DIP w programowaniu mobilnym

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.

kotlin
// Abstrakcja należy do warstwy domain (poziom wyższy)
interface UserRepository {
    fun getUser(id: Int): User
}

// Warstwa domain zależy tylko od abstrakcji
class GetUserUseCase(
    private val repo: UserRepository
) {
    fun execute(id: Int): User = repo.getUser(id)
}

// Implementacja w warstwie data zależy od abstrakcji warstwy domain
class RoomUserRepository(
    private val dao: UserDao
) : UserRepository {
    override fun getUser(id: Int): User {
        return dao.getById(id)
    }
}

// Korzeń kompozycji
class AppModule {
    fun provideUserRepository(dao: UserDao): UserRepository {
        return RoomUserRepository(dao)
    }
}

Przykład z iOS z Application Coordinator i protokołem do nawigacji:

swift
// Abstrakcja nawigacji w warstwie domain
protocol AuthNavigation {
    func navigateToHome()
    func navigateToLogin()
}

// ViewModel zależy od abstrakcji, nie od UIKit
final class AuthViewModel {
    private let navigation: AuthNavigation

    init(navigation: AuthNavigation) {
        self.navigation = navigation
    }

    func onLoginSuccess() {
        navigation.navigateToHome()
    }
}

// Coordinator (warstwa UIKit) implementuje protokół warstwy domain
final class AppCoordinator: AuthNavigation {
    func navigateToHome() {
        // Kod nawigacji UIKit
    }
    func navigateToLogin() {
        // Kod nawigacji UIKit
    }
}

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.

Narzędzia dla DIP: Dagger, Hilt, Koin

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.

Organizacja modułów DI według warstw

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.

Często zadawane pytania

Czy zawsze trzeba stosować DIP?

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.

Czy DIP to to samo co wstrzykiwanie zależności?

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.

Gdzie definiować interfejsy dla DIP?

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.

Jak DIP wpływa na testowanie?

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.

Jaki framework DI wybrać dla Androida?

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.

Podsumowanie

  • DIP (Dependency Inversion Principle) — piąta zasada SOLID o granicach architektonicznych przez abstrakcje
  • Moduły wyższego poziomu nie zależą od modułów niższego — oba zależą od abstrakcji
  • DIP ≠ DI: zasada kontra wzorzec implementacji; DI — sposób, DIP — cel
  • Abstrakcje należą do konsumenta (warstwa domain), a nie do dostawcy (warstwa data)
  • Korzeń kompozycji — jedyne miejsce w aplikacji, gdzie gromadzone są konkretne zależności
  • Hilt, Koin, Dagger — narzędzia DI automatyzujące wiązanie, ale nie zastępujące decyzji architektonicznej DIP
  • Warstwa domain zbudowana według DIP pozostaje niezależna od frameworków, UI i bibliotek infrastrukturalnych

Opracujemy aplikację mobilną pod klucz

IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.

Omów projekt

Przeczytaj również