Singleton und Dependency Injection
Kurz gesagt: Singleton garantiert genau eine Instanz einer Klasse, Dependency Injection reicht Abhängigkeiten von außen hinein, damit sich der Code mit einem Ersatz testen lässt, etwa mit einer Uhr.
Problem
Der Test, der nur freitags besteht
Freitags gibt der Shop 10 % Rabatt. Das Programm hat eine Uhr, also ist sie ein Singleton: eine Klasse mit nur einem Objekt, von überall erreichbar. Der Bestell-Service fragt sie, welcher Tag heute ist:
class SystemClock { private static final SystemClock INSTANCE = new SystemClock();
private SystemClock() { }
public static SystemClock getInstance() { return INSTANCE; }
public LocalDate today() { return LocalDate.now(); }}
class OrderService { public double price(double amount) { if (SystemClock.getInstance().today().getDayOfWeek() == DayOfWeek.FRIDAY) { return amount * 0.9; } return amount; }}Jemand schreibt einen Test, um sicherzugehen, dass der Rabatt funktioniert:
OrderService service = new OrderService();System.out.printf("Freitagsrabatt: erwartet 90.00 €, bekommen %.2f €%n", service.price(100));An einem Montag, dem 26. Oktober 2026, gibt der Test aus:
Freitagsrabatt: erwartet 90.00 €, bekommen 100.00 €An einem Freitag gibt derselbe Test 90.00 aus. Der Test besteht an einem Tag pro Woche und fällt an den anderen sechs durch, und der Code ist derselbe. Um den Rabatt zu prüfen, musst du auf den Freitag warten oder die Uhr des Computers verstellen.
Die Uhr selbst ist richtig gemacht. Das Problem ist, dass price selbst nach ihr greift. Von außen siehst du nicht, dass der Service von einer Uhr abhängt: new OrderService() verlangt nichts. Der Test kann ihm keinen anderen Tag geben, weil er ihm überhaupt nichts geben kann.
Die Idee
Das eine Objekt und das Hineinreichen
Ein Singleton garantiert zwei Dinge: Es gibt nur ein Objekt, und jeder kann es von überall nehmen. Das Erste braucht man manchmal. Das Zweite ist der Preis:
- eine versteckte Abhängigkeit. Wer die Uhr benutzt, siehst du nur, wenn du jede Zeile jeder Methode liest;
- gemeinsamer Zustand. Was ein Stück Code im Objekt ändert, sehen alle anderen, auch der nächste Test;
- es lässt sich nicht austauschen. Der Code verweist namentlich auf die konkrete Klasse. Eine andere Uhr für den Test hat keinen Weg hinein.
Die Lösung ist nicht, die Uhr zu entfernen, sondern sie von außen hineinzureichen. Der Service sucht seine Abhängigkeit nicht, er bekommt sie im Konstruktor. Welche Uhr es ist, entscheidet, wer den Service erzeugt: Das Programm reicht die echte hinein, der Test seine eigene, mit einem Datum, das der Test wählt.
Das heißt Dependency Injection (DI). Es gibt drei Rollen:
- der Service sagt, was er braucht: ein Parameter mit Interface-Typ im Konstruktor;
- die Abhängigkeit ist das Objekt, das er bekommt: die echte Uhr oder die Testuhr;
- das Verdrahten passiert an einer Stelle, am Anfang des Programms. Dort entscheidest du, welches Objekt wohin kommt.
Der Singleton kann sogar bleiben: Die eine Uhr wird wie jedes andere Objekt hineingereicht. Nur greift niemand mehr von sich aus nach ihr.
Sieh es dir an
Singleton, so wie die Sprache ihn bietet
if (instance == null) gesehen. Wenn zwei Threads gleichzeitig bei der Prüfung ankommen, bekommst du zwei Objekte. Die Sprache hat sicherere Wege.Die Uhr aus dem Problem wird in einem static final-Feld erzeugt. Java erzeugt sie einmal, beim Laden der Klasse, und das ist auch mit vielen Threads sicher. Wenn die Erzeugung teuer ist und erst bei der ersten Anfrage passieren soll, kommt das Objekt in eine verschachtelte Klasse (einen Holder). Java lädt Holder erst beim ersten getInstance():class SystemClock { private SystemClock() { System.out.println("Erzeuge die Uhr"); }
private static class Holder { static final SystemClock INSTANCE = new SystemClock(); }
public static SystemClock getInstance() { return Holder.INSTANCE; }}System.out.println("Start");SystemClock a = SystemClock.getInstance();SystemClock b = SystemClock.getInstance();System.out.println(a == b);StartErzeuge die UhrtrueGenauer, für Neugierige
enum mit einem Wert, INSTANCE (du hast ihn in „Entwurfsmuster“ gesehen). Java erlaubt kein new für ein enum und erzeugt den Wert genau einmal.Sieh es dir an
Die Uhr kommt von außen
Clock: ein Interface mit einer Methode. SystemClock aus dem Problem bekommt nur ein implements Clock dazu:interface Clock { LocalDate today();}
class OrderService { private final Clock clock;
public OrderService(Clock clock) { this.clock = clock; }
public double price(double amount) { if (clock.today().getDayOfWeek() == DayOfWeek.FRIDAY) { return amount * 0.9; } return amount; }}Im Programm wird die echte Uhr einmal hineingereicht, bei der Erzeugung:
OrderService service = new OrderService(SystemClock.getInstance());Jetzt sagt new OrderService(...) laut, dass er ohne Uhr nicht arbeiten kann. Die Abhängigkeit steht in der Signatur des Konstruktors, nicht irgendwo in einer Methode.
In großen Anwendungen übernimmt ein Framework das Verdrahten: Spring erzeugt die Objekte und reicht jedes an den weiter, der es im Konstruktor verlangt. Die Idee ist dieselbe; von Hand in main ist für kleine Programme.
Sieh es dir an
Der Test wählt den Tag selbst
Der Test braucht eine Uhr, der du sagen kannst, welches Datum sie zurückgeben soll:
class FakeClock implements Clock { private LocalDate date;
public FakeClock(LocalDate date) { this.date = date; }
public void set(LocalDate date) { this.date = date; }
@Override public LocalDate today() { return date; }}Der Test reicht sie statt der echten hinein:
FakeClock clock = new FakeClock(LocalDate.of(2026, 10, 30));OrderService service = new OrderService(clock);System.out.printf("Freitag: %.2f €%n", service.price(100));
clock.set(LocalDate.of(2026, 10, 27));System.out.printf("Dienstag: %.2f €%n", service.price(100));Freitag: 90.00 €Dienstag: 100.00 €FakeClock clock = new FakeClock(LocalDate.of(2026, 10, 30));OrderService service = new OrderService(clock);System.out.printf("Freitag: %.2f €%n", service.price(100));
clock.set(LocalDate.of(2026, 10, 27));System.out.printf("Dienstag: %.2f €%n", service.price(100));Schritt für Schritt, in Worten
- Vorher griff price selbst nach SystemClock.getInstance(): eine versteckte Verbindung zu einem globalen Objekt, das der Test nicht ändern kann.
- Der Test baut sich eine eigene Uhr: eine FakeClock, die 2026-10-30 sagt, ein Freitag.
- Die Uhr kommt von außen herein, über den Konstruktor. OrderService hält nur einen Verweis auf sie.
- price(100): Der Service fragt seine Uhr. Es ist Freitag, also ist der Betrag 90.00.
- Der Test verschiebt das Datum auf 2026-10-27, einen Dienstag. In OrderService ändert sich nichts.
- Dasselbe price(100): ein Dienstag, 100.00. Der Test gibt diese beiden Zahlen an jedem Wochentag.
Der Test gibt diese beiden Zahlen an einem Montag, an einem Freitag und an einem Sonntag aus. OrderService ist dieselbe Klasse, die im Shop läuft: Der Test ändert nur, was er hineinreicht.
Genauer, für Neugierige
Clock hat eine Methode, also kann eine Testuhr für einen festen Tag auch ein Lambda sein: new OrderService(() -> LocalDate.of(2026, 10, 30)). Eine Klasse lohnt sich, wenn der Test das Datum verschiebt, wie hier. Java hat auch eine eingebaute java.time.Clock mit Clock.fixed(...) für Tests, und LocalDate.now(clock) fragt sie.Probier es aus
Sag die Ausgabe voraus
Die Klassen stammen von den Karten davor. Der 31. Oktober 2026 ist ein Samstag. Was gibt das Programm aus?
FakeClock clock = new FakeClock(LocalDate.of(2026, 10, 30));OrderService shop = new OrderService(clock);clock.set(LocalDate.of(2026, 10, 31));OrderService app = new OrderService(clock);
System.out.printf("%.2f €%n", shop.price(50));System.out.printf("%.2f €%n", app.price(50));Probier es aus
Finde den Fehler
Der Warenkorb ist ein Singleton, „damit es nur einen gibt“:
class Cart { private static final Cart INSTANCE = new Cart(); private final List<String> items = new ArrayList<>();
private Cart() { }
public static Cart getInstance() { return INSTANCE; }
public void add(String item) { items.add(item); }
public int size() { return items.size(); }}Zwei Tests, jeder mit seiner eigenen Kassenseite. check gibt OK aus oder das, was der Test erwartet hat:
CheckoutPage first = new CheckoutPage();first.add("Kaffee");first.add("Tasse");check("Test 1", first.count(), 2);
CheckoutPage second = new CheckoutPage();second.add("Kaffee");check("Test 2", second.count(), 1);Test 1: OKTest 2: FAIL, erwartet 1, bekommen 3Jeder Test besteht für sich. Zusammen fällt der zweite durch. In welcher Zeile steckt der Fehler?
Wähle die Zeile mit dem Fehler.
Die Seite nimmt sich den einen Warenkorb selbst. Beide Seiten und beide Tests füllen dasselbe Objekt: Der zweite Test sieht den Kaffee und die Tasse des ersten. Im Shop ist es noch schlimmer: Alle Kunden kaufen in einem Warenkorb ein. Ein Warenkorb ist nichts, wovon es im Programm einen gibt; es gibt einen pro Kunde. Die Lösung: Der Warenkorb ist eine gewöhnliche Klasse mit öffentlichem Konstruktor, und die Seite bekommt ihn in ihrem eigenen: new CheckoutPage(new Cart()). Jeder Test macht seinen eigenen Warenkorb, und beide bestehen.
Genauer, für Neugierige
Test-Bibliotheken führen Tests in einer nicht garantierten Reihenfolge aus, manchmal gleichzeitig. Ein Test, der nur besteht, wenn er zuerst läuft, ist ein Test, der zufällig durchfällt. Gemeinsamer Zustand in einem Singleton ist die häufigste Ursache.
Die Idee
Wann du es nicht brauchst
Ein Singleton ist sinnvoll, wenn eine zweite Kopie ein Fehler wäre und das Objekt nichts hält, das sich ändert: Einstellungen, die beim Start einmal gelesen werden, das Log, der Pool der Datenbankverbindungen. Auch dann reichst du es besser von außen hinein, als dass alle danach greifen.
Das Hineinreichen hat auch einen Preis. Es lohnt sich nicht, wenn:
- die Abhängigkeit sich nie ändert und den Tests nicht im Weg steht.
Math.roundist keine Abhängigkeit, die du hineinreichst; - das Programm klein ist. Das Verdrahten von Hand in main reicht; ein DI-Framework ist für große Anwendungen;
- der Konstruktor acht Abhängigkeiten verlangt. Das ist kein DI-Problem: Die Klasse tut zu viel.
Übung
Black Friday
Löse die Aufgabe in deinem Editor.
Black Friday. Am 27. November 2026 gibt der Shop 30 % Rabatt, an anderen Freitagen weiterhin 10 %. Füge die Regel zu OrderService hinzu und prüfe sie mit FakeClock für drei Daten: Black Friday, den 30. Oktober (ein Freitag) und den 27. Oktober (ein Dienstag). Der Test gibt an jedem Tag dieselbe Ausgabe.
Black Friday: 70.00 €Freitag: 90.00 €Dienstag: 100.00 €Weitere Aufgaben
- Bestellnummern. Der Zähler für Bestellnummern ist ein Singleton und beginnt bei 1001. Mach ihn zu einer Abhängigkeit von
OrderServiceund schreibe zwei Tests, die beide 1001 als erste Nummer bekommen. - Ein Warenkorb pro Kunde. Behebe den Fehler aus „Finde den Fehler“ ganz: Der Warenkorb ist kein Singleton, und jede Seite bekommt ihren eigenen. Zwei Kunden legen verschiedene Artikel hinein, und jeder sieht nur seine eigenen.
- Nachtrabatt. Zwischen 22:00 und 6:00 Uhr beträgt der Rabatt 5 %. Die Uhr muss jetzt auch die Stunde liefern. Was änderst du an Clock, und wie viele Tests brauchst du, um sicher zu sein?
Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Klassen in Klassen · Lambdas · Entwurfsmuster · Strategy
- Geht weiter in: Adapter und Facade · Repository · Der Shop: welches Muster wohin · SOLID ohne Dogma
- Aufgaben zum Thema: Entwurfsmuster