Facebook
Twitter
Google+
Kommentare
3
Willkommen bei "the web hates me". Mittlerweile hat unser Team ein tolles neues Monitoringtool für Agenturen gelauncht. Unter dem Namen koality.io haben wir einen Service geschaffen, der er Agenturen ermöglicht mit sehr geringen Kosten alle Ihre Webseiten zu überwachen.

Das Singleton, richtig implementiert

Bücher, Tutorials und andere Artikel über Design Patterns neigen dazu, das Singleton Design Pattern als erstes vorzustellen, aus zwei Gründen:

  • Das Singleton ist ein sehr leicht zu verstehendes Design Pattern, selbst für Programmierer die sich neu mit dem Thema Design Patterns befassen bzw. überhaupt erst kurze Zeit mit objektorientierter Programmierung vertraut sind.
  • Das Singleton löst ein Problem, das 99% dieser Programmierer haben, nämlich einen globalen Zugriffspunkt auf eine Klasse bereitzustellen, die überall in einer Anwendung genutzt wird.

Jedoch hat das Singleton Design Pattern einige grundlegende Probleme. Das ist nicht neu und wird unter Entwicklern bereits seit mehreren Jahren diskutiert (einfach nach „evil singleton“ suchen http://www.google.de/search?hl=de&q=evil+singleton). Das Singleton verletzt mehrere Regeln eines guten objektorientierten Designs. Code welcher die Singleton-Klasse benutzt hängt von dieser ab, er ist nicht ohne sie nutzbar, was ihn dazu auch noch schwer zu testen macht. Der Code ist dann nicht gegen ein Interface programmiert, sondern gegen eine konkrete Klasse, und daher fest an das Singleton gekoppelt, anstatt einer gewünschten losen Kopplung zwischen beiden Klassen. Darüber hinaus sind Klassen, die das Singleton Pattern implementieren selbst schwer zu testen. Singletons ersetzen globale Variablen und verstecken sie in einer Klasse – der globale Zustand bleibt in der Anwendung. Wenn allgemein die Ansicht herscht, dass ein globaler Zustand schlecht ist und das sein Auftauchen als Singleton den Code nicht besser macht, warum wird das Singleton Design Pattern dann so oft benutzt?

Um diese Frage zu beantworten möchte ich zunächst einen Schritt zurückgehen und einmal eine Klasse beleuchten, die als Singleton implementiert ist. Wenn man sich eine typische Implementierung anschaut, dann kann man die Methoden in solchen Klassen in zwei Kategorien einteilen: solche die für die Geschäftslogik zuständig sind und solche, die für das Erzeugen und den Zugriff auf die Instanz zuständig sind. Wie man sieht sind dies zwei verschiedene Verantwortlichkeiten. Bei guter Objektorientierung sollte eine Klasse nur eine Verantwortlichkeit haben, aber trotzdem benutzen wir ein Design Pattern das zwei verschiedene Verantwortlichkeiten in einer Klasse vermischt. Warum sollte eine Klasse entscheiden wie viel Instanzen von ihr in einer Anwendung erzeugt werden dürfen? Eigentlich sollte sie gar nicht wissen wie viel Instanzen von ihr herumfliegen, es ist einfach nicht ihre Aufgabe. Es sollte einfach nur die Geschäftslogik in sich bestmöglich ausführen, und aus meiner Sicht hat sie damit alle Hände voll zu tun.

Andererseits ist es vollkommen ok die Anzahl der Instanzen einer Klasse zu beschränken. Und natürlich möchten wir andere Programmierer davor bewahren, die Beschränkungen unseres Codes zu umgehen. Dennoch, warum sollte sich ein anderer Programmierer darum Gedanken machten, wie viel Instanzen einer bestimmten Klasse in einer Anwendung erlaubt sind (abgesehen von Performance-Gründen)? Der andere Programmierer sollte in seinem Code einfach sagen „Ich benötige eine Instanz von Logger, und mein Code wird nicht ohne arbeiten, und zum Teufel noch mal ist es mir egal wie viel Instanzen von Logger erlaubt sind, gib mir einfach irgendeine!“.

Zurück zu den Verantwortlichkeiten einer Klasse: eine Klasse ist für die in ihr enthaltende Geschäftslogik verantwortlich, und es ist dafür verantwortlich die Abhängigkeiten zu definieren, um die Geschäftslogik korrekt ausführen zu können. Durch die Nutzung einer Singleton-Klasse wird diese Abhängigkeit innerhalb der Klasse versteckt. Es ist nicht möglich, aus der API heraus zu erkennen, dass die Klasse von einer anderen abhängt, sofern es nicht entsprechend dokumentiert ist. Darüber hinaus sollte eine Klasse sich nicht darum kümmern müssen wie Klassen erzeugt werden von denen sie abhängt. Zusammengefasst: eine Klasse sollte ihre Abhängigkeiten explizit deklarieren, und dies erfolgt durch die Definition eines Konstruktors, der sagt „Ich benötige dieses und jenes um korrekt zu arbeiten, und wenn Du mir das nicht gibst bekommst Du auch keine Instanz von mir“. Ein wenig Code um das darzustellen:

Schlecht:

class Foo {
  protected $logger;
  public function __construct() {
    $this->logger = Logger::getInstance();
  }
...
}

Gut:

class Foo {
  protected $logger;
  public function __construct(Logger $logger) {
    $this->logger = $logger;
  }
...
}

Das zweite Beispiel sagt deutlich, dass Foo eine Instanz von Logger benötigt um erzeugt zu werden. Gute Doc Comments vorausgesetzt (im Beispiel weggelassen um es kurz zu halten) wird die generierte API-Dokumentation dem Benutzer von Foo sagen dass man einen Logger benötigt um ein Foo zu erzeugen. Zusätzlich ist Foo sehr viel einfacher zu testen, da man statt einer konkreten Klasse auch ein Mock-Objekt injizieren könnte. Natürlich gibt es für diese Art der Programmierung auch einen Namen: Dependency Injection. Die kürzeste Erklärung dafür könnte wie folgt lauten: „Dependency Injection bedeutet dass eine Klasse ihre Abhängigkeiten als Parameter im Konstruktor oder via Setter-Methoden injiziert bekommt“.

Wenn man dem Dependency Injection-Prinzip folgt kann man seine Klassen in zwei Kategorien einteilen: solche die nur die Geschäftslogik beinhalten, und solche die dafür zuständig sind Instanzen anderer Klassen zu erzeugen und diese zusammen zu fügen.* (Das hatten wir doch schon mal weiter oben, oder?) Das gute daran ist, dass wir die Klassen der Geschäftslogik auch anders zusammensetzrn könnten und ein anderes Verhalten erhalten würden – ohne die Klassen der Geschäftslogik überhaupt anpassen oder ändern zu müssen.

Aber wie stellen wir jetzt sicher, dass es nur eine Instanz von einer bestimmten Klasse gibt? Das liegt einfach in der Verantwortung der Klasse, die für die Erzeugung von Instanzen zuständig sind. Dort kann man eine Logger-Instanz erzeugen und wo immer eine Klasse einen Logger benötigt übergibt man genau diese Instanz. Damit erhält man ebenfalls ein singleton*, aber ohne den Nachteil der mit dem Singleton Design Pattern einhergeht. Ein zusätzlicher Vorteil: sofern man auf einmal feststellt dass man mehr als eine Instanz von Logger benötigt muss man den Logger selbst nicht ändern, und auch nicht den Code der Logger benutzt. Man muss ausschließlich den Code zum Erzeugen und Zusammenfügen von Objekten anpassen.

Zugegebenermaßen erzeugt dieser Weg sehr viel Boilerplate-Code (der zum Erzeugen und Zusammenfügen der Objekte). Solchen Boilerplate-Code zu schreiben ist wirklich langweilig, wird schnell lästig und macht keinen Spaß. Hier kann ein Dependency Injection-Framework helfen. Aber das ist eine Geschichte für einen anderen Tag.

Ein letzter Punkt: Entwickler, die an das Thema Dependency Injection herangeführt werden sind oftmals von der Idee selbst fasziniert, stellen aber meist die gleiche Frage: Muss ich alle in einer tieferen Schicht benötigten Instanzen bereits an die darüber liegende Schicht übergeben, damit diese sie an die darunterliegende Schicht weitergeben kann? Die kurze Antwort: nein. Zumindest nicht wenn man es korrekt implementiert. Ein Beispiel: Ein Haus hat eine Tür, und die Tür hat ein Schloss. Wenn das Haus ein Schloss benötigt um es an die Tür zu übergeben ist das Design falsch. Das Haus sollte nur eine Tür benötigen. Ob die Tür ein Schloss benötigt oder nicht ist völlig unwichtig für das Haus. Eine Implementierung könnte dann so aussehen:

class House {
  protected $door;
  public function __construct(Door $door) {
    $this->door = $door;
  }
  ...
}

class Door {
  protected $lock;
  public function __construct(Lock $lock) {
    $this->lock = $lock;
  }
  ...
}

Das Haus muss nichts über Schlösser wissen – nur über Türen. Selbst wenn es irgendwas mit Schlössern tut sollte dies über Methoden der Door-Klasse erfolgen. (Eigentlich sollte Door ein Interface sein, aber für diese kurze Beispiel ist es ok, wenn Door eine Klasse ist.)

Um die Frage von oben zu beantworten: Das Singleton Design Pattern wird so oft genutzt weil der Rest des Codes Lücken im Design hat, wenn die Geschäftslogik und die Erzeugungslogik nicht korrekt verteilt sind. Es ist kein Problem die Anzahl der Instanzen einer Klasse zu limitieren, aber um das richtig mit einer einzelnen Instanz in der Erzeugungslogik zu tun muss der Rest der Anwendung mit Dependency Injection im Hinterkopf designt werden. Man könnte meinen, dass Code refactored werden muss solange es Spuren von Singletons enthät.

* Eine Ausnahme davon sind Klassen die nur Werte und Daten aber keine Logik enthalten – diese können durchaus von den Klassen der Geschäftslogik durch die Nutzung eines anderen Erzeugungsmusters erzeugt werden.

** Man könnte es „das kleine singleton“ nennen – mit einem kleinen s im Gegensatz zum Design Pattern, das mit einem großen S beginnt.

Über den Autor

Frank Kleine

Kommentare

3 Comments

  1. Super geschrieben! Solche Beiträge sind sowohl lehrreich als auch gut zu verstehen 🙂 Der bisherige Höhepunkt dieses Jahres 😉

    Reply
  2. Um das Singleton zu kritisieren muss man es aber vorher kennen^^. Und auch wenn der Einsatz von Singletons einen Hinweis auf Code bilden kann, der noch zu refactorisieren sei, so hilft es dennoch ganz viel dabei Code zu refactorisieren, in dem es nicht vorkommt.

    Reply

Leave a Comment.

Link erfolgreich vorgeschlagen.

Vielen Dank, dass du einen Link vorgeschlagen hast. Wir werden ihn sobald wie möglich prüfen. Schließen