Zum Inhalt
wannacode

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:
def today(self):
return date.today()
clock = SystemClock()
class OrderService:
def price(self, amount):
if clock.today().weekday() == FRIDAY:
return amount * 0.9
return amount

Jemand schreibt einen Test, um sicherzugehen, dass der Rabatt funktioniert:

service = OrderService()
print(f"Freitagsrabatt: erwartet 90.00 €, bekommen {service.price(100):.2f} €")

An einem Montag, dem 26. Oktober 2026, gibt der Test aus:

Ausgabe
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: 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 im Konstruktor. Was das Objekt ist, spielt keine Rolle, solange es eine Methode today() hat;
  • 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

In der Lektion „Entwurfsmuster“ hast du einen Singleton als Klasse gesehen, die ihr eines Objekt hält. In Python macht man das selten so, weil die Sprache schon etwas hat, das nur einmal läuft: das Modul.Ein Modul (die Datei clock.py) läuft einmal, beim ersten import. Jedes spätere from clock import clock liefert dasselbe Objekt. Ein Objekt auf oberster Ebene eines Moduls ist ein Singleton ohne eine einzige zusätzliche Zeile Code:
class SystemClock:
def __init__(self):
print("Erzeuge die Uhr")
def today(self):
return date.today()
clock = SystemClock()
print("Start")
Ausgabe
Erzeuge die Uhr
Start
Das Objekt wird erzeugt, wenn das Modul läuft, vor der ersten Zeile, die es benutzt. In diesem Programm stehen Modul und Code in einer Datei. In einem echten Projekt liegt die Uhr in einer eigenen Datei, und egal, an wie vielen Stellen sie importiert wird, es gibt ein Objekt.
Genauer, für Neugierige
Der Trick mit __new__ aus „Entwurfsmuster“ wird selten gebraucht. Wenn du doch eine Klasse baust, die ein Objekt hält, denk daran, dass __init__ bei jeder Erzeugung aufgerufen wird, auch wenn __new__ das alte Objekt zurückgibt.

Sieh es dir an

Die Uhr kommt von außen

Der Service verlangt im Konstruktor eine Uhr. Was sie ist, spielt keine Rolle, solange sie eine Methode today() hat. Das Modul mit clock = SystemClock() bleibt, wie es ist:
class OrderService:
def __init__(self, clock):
self.clock = clock
def price(self, amount):
if self.clock.today().weekday() == FRIDAY:
return amount * 0.9
return amount

Im Programm wird die echte Uhr einmal hineingereicht, bei der Erzeugung:

service = OrderService(clock)

Jetzt sagt OrderService(...) laut, dass er ohne Uhr nicht arbeiten kann. Die Abhängigkeit steht in der Signatur des Konstruktors, nicht irgendwo in einer Methode.

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:
def __init__(self, day):
self.day = day
def today(self):
return self.day

Der Test reicht sie statt der echten hinein:

clock = FakeClock(date(2026, 10, 30))
service = OrderService(clock)
print(f"Freitag: {service.price(100):.2f} €")
clock.day = date(2026, 10, 27)
print(f"Dienstag: {service.price(100):.2f} €")
Ausgabe
Freitag: 90.00 €
Dienstag: 100.00 €

clock = FakeClock(date(2026, 10, 30))
service = OrderService(clock)
print(f"Freitag: {service.price(100):.2f} €")
clock.day = date(2026, 10, 27)
print(f"Dienstag: {service.price(100):.2f} €")
Schritt für Schritt, in Worten
  1. Vorher griff price selbst nach der clock auf Modulebene: eine versteckte Verbindung zu einem globalen Objekt, das der Test nicht ändern kann.
  2. Der Test baut sich eine eigene Uhr: eine FakeClock, die 2026-10-30 sagt, ein Freitag.
  3. Die Uhr kommt von außen herein, über den Konstruktor. OrderService hält nur einen Verweis auf sie.
  4. price(100): Der Service fragt seine Uhr. Es ist Freitag, also ist der Betrag 90.00.
  5. Der Test verschiebt das Datum auf 2026-10-27, einen Dienstag. In OrderService ändert sich nichts.
  6. 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
Eine Uhr mit einer Methode kann auch eine Funktion sein. Der Service nimmt today und ruft self.today() auf. Das Programm reicht date.today hinein, ohne Klammern, und der Test lambda: date(2026, 10, 30). Eine Funktion ist eine Abhängigkeit wie jedes Objekt.

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?

clock = FakeClock(date(2026, 10, 30))
shop = OrderService(clock)
clock.day = date(2026, 10, 31)
app = OrderService(clock)
print(f"{shop.price(50):.2f} €")
print(f"{app.price(50):.2f} €")
Antwort

Probier es aus

Finde den Fehler

Der Warenkorb ist ein Singleton, „damit es nur einen gibt“:

class Cart:
def __init__(self):
self.items = []
def add(self, item):
self.items.append(item)
cart = Cart()

Zwei Tests, jeder mit seiner eigenen Kassenseite. check gibt OK aus oder das, was der Test erwartet hat:

first = CheckoutPage()
first.add("Kaffee")
first.add("Tasse")
check("Test 1", first.count(), 2)
second = CheckoutPage()
second.add("Kaffee")
check("Test 2", second.count(), 1)
Ausgabe
Test 1: OK
Test 2: FAIL, erwartet 1, bekommen 3

Jeder Test besteht für sich. Zusammen fällt der zweite durch. In welcher Zeile steckt der Fehler?

Wähle die Zeile mit dem Fehler.

Genauer, für Neugierige

Derselbe durchsickernde Warenkorb taucht auch ohne Singleton auf, durch eine Liste als Standardwert. items=[] läuft einmal, wenn Python das def liest, und jede CheckoutPage() bekommt dieselbe Liste:

class CheckoutPage:
def __init__(self, items=[]):
self.items = items
def add(self, item):
self.items.append(item)
def count(self):
return len(self.items)
first = CheckoutPage()
first.add("Kaffee")
first.add("Tasse")
check("Test 1", first.count(), 2)
second = CheckoutPage()
second.add("Kaffee")
check("Test 2", second.count(), 1)
Ausgabe
Test 1: OK
Test 2: FAIL, erwartet 1, bekommen 3

Die Lösung ist items=None und self.items = [] if items is None else items.

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. round ist keine Abhängigkeit, die du hineinreichst;
  • das Programm klein ist. Das Verdrahten von Hand in dem Programmanfang 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.

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 OrderService und 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 der Uhr, und wie viele Tests brauchst du, um sicher zu sein?

Test

Teste dich

1. Warum besteht der Test für den Freitagsrabatt nur freitags?
2. Was ändert das Hineinreichen der Abhängigkeit (Dependency Injection)?
3. Zwei Tests benutzen denselben Warenkorb, ein Singleton. Der erste legt zwei Artikel hinein, der zweite einen und erwartet 1. Was passiert?
4. Wann passt ein Singleton?

Erstellt von Ivo Gergov im Rahmen der Kampagne von Evo Tech für zugängliche Programmierausbildung ·