13 Vorteile von Dependency Injection

DI ist mehr als eine Technik – es ist ein Enabler. Es ermöglicht Praktiken, die ohne DI entweder unmöglich oder extrem aufwändig wären.

13.1 Testbarkeit

Der offensichtlichste Vorteil. Mit DI kannst du echte Komponenten durch Test-Doubles ersetzen.

@Service
public class LeaderboardService {
    
    private final ScoreRepository scoreRepository;
    
    public LeaderboardService(ScoreRepository scoreRepository) {
        this.scoreRepository = scoreRepository;
    }
    
    public boolean isNewHighscore(String gameId, int points) {
        Score currentTop = scoreRepository.findTopByGameId(gameId);
        return currentTop == null || points > currentTop.getPoints();
    }
}

Der Test braucht keine Datenbank:

@Test
void newHighscore_whenNoScoreExists_returnsTrue() {
    // Arrange: Mock gibt null zurück (kein Score vorhanden)
    ScoreRepository mockRepo = mock(ScoreRepository.class);
    when(mockRepo.findTopByGameId("pacman")).thenReturn(null);
    
    LeaderboardService service = new LeaderboardService(mockRepo);
    
    // Act & Assert
    assertTrue(service.isNewHighscore("pacman", 100));
}

@Test
void newHighscore_whenScoreIsHigher_returnsTrue() {
    // Arrange: Mock gibt existierenden Score zurück
    ScoreRepository mockRepo = mock(ScoreRepository.class);
    Score existingScore = new Score("pacman", "player1", 5000);
    when(mockRepo.findTopByGameId("pacman")).thenReturn(existingScore);
    
    LeaderboardService service = new LeaderboardService(mockRepo);
    
    // Act & Assert
    assertTrue(service.isNewHighscore("pacman", 6000));
    assertFalse(service.isNewHighscore("pacman", 4000));
}

Gleicher Service, unterschiedliche Abhängigkeiten. Der Test läuft in Millisekunden, isoliert, wiederholbar.

13.2 Austauschbarkeit

Anforderungen ändern sich. Heute MongoDB, morgen PostgreSQL. Heute E-Mail-Benachrichtigungen, morgen Push-Notifications.

Mit DI ist der Wechsel schmerzlos:

Der HighscoreService nutzt NotificationService. Welche Implementierung aktiv ist, bestimmt die Konfiguration – nicht der Code.

// Entwicklung: Keine echten Benachrichtigungen
@Profile("dev")
@Service
public class LoggingNotificationService implements NotificationService {
    public void sendHighscoreAlert(String player, int points) {
        log.info("Would notify about highscore: {} - {}", player, points);
    }
}

// Produktion: Echte E-Mails
@Profile("prod")
@Service
public class EmailNotificationService implements NotificationService {
    public void sendHighscoreAlert(String player, int points) {
        // Tatsächlich E-Mail senden
    }
}

Ein Profil-Wechsel genügt. Kein Code wird angefasst.

13.3 Modularisierung

DI erzwingt klare Grenzen zwischen Komponenten. Jede Klasse definiert explizit, was sie braucht und was sie bietet.

Module können unabhängig entwickelt werden. Das Score-Team arbeitet am Score-Modul, das Player-Team am Player-Modul. Solange die Interfaces stabil bleiben, stören sie sich nicht gegenseitig.

13.4 Single Responsibility

DI fördert das Single Responsibility Principle. Wenn eine Klasse zu viele Dependencies hat, wird das im Konstruktor sichtbar:

// Das riecht nach zu vielen Verantwortlichkeiten
public class DoEverythingService {
    public DoEverythingService(
            ScoreRepository scores,
            PlayerRepository players,
            GameRepository games,
            NotificationService notifications,
            CacheService cache,
            AnalyticsService analytics,
            AuditService audit,
            ConfigService config) {
        // ...
    }
}

Acht Parameter? Das ist ein Warnsignal. Die Klasse macht wahrscheinlich zu viel. Constructor Injection macht solche Design-Probleme sichtbar.

13.5 Konfigurierbarkeit

DI trennt Konfiguration von Logik. Die Geschäftslogik weiß nicht, ob sie in der Cloud oder lokal läuft:

@Service
public class StorageService {
    
    private final FileStore fileStore;
    
    public StorageService(FileStore fileStore) {
        this.fileStore = fileStore;
    }
    
    public void saveReplay(String gameId, byte[] data) {
        fileStore.write("replays/" + gameId, data);
    }
}

Die Konfiguration entscheidet:

@Configuration
public class StorageConfig {
    
    @Bean
    @Profile("local")
    public FileStore localFileStore() {
        return new LocalFileStore("/var/arcade/storage");
    }
    
    @Bean
    @Profile("cloud")
    public FileStore s3FileStore(S3Client s3Client) {
        return new S3FileStore(s3Client, "arcade-bucket");
    }
}

Der StorageService bleibt unverändert. Er speichert Replays – wo, ist nicht sein Problem.

13.6 Übersicht der Vorteile

Vorteil Ohne DI Mit DI
Unit Tests Brauchen echte Infrastruktur Mocks genügen
Implementierung wechseln Code ändern Konfiguration ändern
Module entwickeln Enge Abstimmung nötig Unabhängig möglich
Design-Probleme erkennen Versteckt im Code Sichtbar im Konstruktor
Umgebungsspezifisch If-Else-Verzweigungen Profile

Diese Vorteile kommen nicht von selbst. Sie erfordern, dass du DI konsequent einsetzt. Jede Abhängigkeit, die du mit new erzeugst, ist eine verpasste Chance.