Der Begriff klingt akademisch. “Umkehrung der Kontrolle” – wer kontrolliert was, und warum wird umgekehrt? Die Antwort ist überraschend praktisch und verändert die Art, wie du Code schreibst.
In klassischem Java-Code kontrolliert jede Klasse ihre eigenen Abhängigkeiten. Sie entscheidet, welche Objekte sie braucht, und beschafft sie sich selbst.
public class ScoreService {
private ScoreRepository repository = new MongoScoreRepository();
private NotificationService notifications = new EmailNotificationService();
public void recordScore(String player, int points) {
repository.save(new Score(player, points));
if (points > 10000) {
notifications.sendHighscoreAlert(player, points);
}
}
}Der ScoreService erzeugt seine Abhängigkeiten mit
new. Er weiß genau, welche Implementierungen er verwendet:
MongoScoreRepository für Persistenz,
EmailNotificationService für Benachrichtigungen.
Das funktioniert – bis es nicht mehr funktioniert.
Problem 1: Starre Kopplung
Der ScoreService ist an MongoDB gebunden. Willst du
PostgreSQL verwenden? Code ändern. Willst du in der Entwicklung eine
In-Memory-Datenbank? Code ändern. Jede Änderung an der Infrastruktur
erfordert Änderungen an der Geschäftslogik.
Problem 2: Untestbarkeit
Um den ScoreService zu testen, brauchst du eine echte
MongoDB und einen echten E-Mail-Server. Unit-Tests werden zu
Integrationstests. Sie sind langsam, fragil und aufwändig
einzurichten.
Problem 3: Versteckte Abhängigkeiten
Von außen ist nicht erkennbar, was der ScoreService
braucht. Die Abhängigkeiten sind im Code versteckt. Wer die Klasse
verwenden will, muss den Quellcode lesen.
Inversion of Control dreht das Verhältnis um. Statt dass die Klasse ihre Abhängigkeiten kontrolliert, werden ihr die Abhängigkeiten von außen geliefert.
public class ScoreService {
private final ScoreRepository repository;
private final NotificationService notifications;
public ScoreService(ScoreRepository repository, NotificationService notifications) {
this.repository = repository;
this.notifications = notifications;
}
public void recordScore(String player, int points) {
repository.save(new Score(player, points));
if (points > 10000) {
notifications.sendHighscoreAlert(player, points);
}
}
}Der Unterschied ist subtil aber fundamental. Der
ScoreService kennt nur noch Interfaces:
ScoreRepository und NotificationService.
Welche konkreten Implementierungen dahinterstecken, weiß er nicht – und
muss er nicht wissen.
Die Kontrolle ist umgekehrt: Nicht die Klasse beschafft sich ihre Abhängigkeiten, sondern ein externer Container liefert sie.
Der Name ergibt sich aus der Perspektive. Im traditionellen Modell ruft dein Code Bibliotheken auf – du hast die Kontrolle. Bei IoC ruft das Framework deinen Code auf – das Framework hat die Kontrolle.
Das ist ein Paradigmenwechsel. Du schreibst nicht mehr Skripte, die von oben nach unten ablaufen. Du schreibst Komponenten, die das Framework orchestriert.
Spring’s IoC Container ist das Herzstück des Frameworks. Er verwaltet alle Objekte – in Spring-Terminologie “Beans” genannt. Er weiß, welche Beans existieren, welche Abhängigkeiten sie haben und in welcher Reihenfolge sie erzeugt werden müssen.
Beim Start der Anwendung passiert Folgendes:
Das alles geschieht automatisch. Du definierst nur die Komponenten und ihre Beziehungen – Spring kümmert sich um den Rest.
Stell dir den IoC Container als Baukasten vor. Du lieferst die Bauteile (deine Klassen) und eine Anleitung (Annotationen). Der Container baut das fertige Produkt zusammen.
Du musst nicht wissen, wie der Container intern funktioniert. Du musst wissen, wie du ihm sagst, was du willst. Das ist das Thema der nächsten Kapitel.