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