Entwurfsmuster: Singleton

Objektorientierung ist in der heutigen Programmierung das „ganz große Ding“. Alles, womit man arbeitet, wird entsprechend durch Klassen abgebildet, die eine gewisse Struktur vorgeben, wiederverwendbar sind und durch Vererbung auch erweitert werden können. Es ist aber nicht immer ratsam, für jede Anwendung eines Konzeptes jeweils ein neues Objekt zu erzeugen. In bestimmten Fällen kann es sogar durchaus sinnvoll sein, aus einer bestimmten Klasse während der Laufzeit nur ein einzelnes Objekt zu erzeugen. Um genau das zu erleichtern, kann man das Singleton-Erzeugungsmuster verwenden. Dieser Beitrag erklärt anhand von PHP5-Beispielen, wie es funktioniert.

Anwendungsbeispiel Datenbankklasse

Denken wir uns einfach mal eine recht umfangreiche Webseite, die auf einem komplexen Content Management System aufbaut. Meistens steckt dahinter nur eine einzige Datenbank, die dem CMS die entsprechenden Inhaltsdaten und weiterführende Metadaten (z.B. Reihenfolge der Navigation) liefert. Natürlich wurde auf Seiten des CMS eine Klasse implementiert, um Datenbankverbindungen und Abfragen zu behandeln.

Diese Datenbankklasse könnte jetzt (natürlich enorm vereinfacht) so aussehen:

class myDatabase {

	private static 	$_aryConData = array(),
			$_intCounter = 0;

	private  	$_strStatus = 'disconnected',
			$_intID;
	// ... weitere Eigenschaften

	public function __construct($aryConData = array()) {
		if (!empty($aryConData))
			self::$_aryConData = $aryConData;
		$this->_intID = ++self::$_intCounter;
		$this->connect();	
	}

	public function connect() {
		// Connect to your database
		$this->_strStatus = 'connected';
	}

	public function getStatus() {
		return '['.$this->_intID.'] '
			.$this->_strStatus.': '
			.self::$_aryConData[1]
			.':***@'.self::$_aryConData[0]
			.'/'.self::$_aryConData[3];
	}
	// ... weitere Methoden
}

Wir können sie ganz einfach mit den folgenden Codezeilen testen:

$aryConData = array('myhost', 'myuser', 'mypassword', 'mydb');
$objDatabase = new myDatabase($aryConData);
echo $objDatabase->getStatus();

Daraus resultiert die folgende Ausgabe:

[1] connected: myuser:***@myhost/mydb

Um ein wenig die Übersicht zu behalten, befindet sich in der Klasse die Eigenschaft $_intID und die statische Variable $_intCounter. $_intCounter macht nichts anderes, als die aus der Klasse erzeugten Objekte zu zählen, und ist für alle Objekte gleich. $_intID enthält hingegen die dem jeweiligen Objekt zugehörige Nummer, also 1 für das erste Objekt, 2 für das zweite Objekt und so weiter.

Unnötige Instanzen

Jetzt stellen wir uns vor, dass an einer ganz anderen Stelle wieder auf die Datenbank zugegriffen werden soll. Dummerweise geschieht das aber innerhalb einer Funktion oder Klasse, weshalb nicht auf das ursprüngliche $objDatabase zurückgegriffen werden kann. Also wird ein neues Objekt erzeugt und da die Verbindungsdaten auch in einer statischen Variable gespeichert wurden, geht das ganz einfach. Der Testcode dazu sieht jetzt so aus:

$aryConData = array('myhost', 'myuser', 'mypassword', 'mydb');
$objDatabase = new myDatabase($aryConData);
echo $objDatabase->getStatus().'
'; // Ganz viel Magie $objDatabase = new myDatabase(); echo $objDatabase->getStatus();

Nun die Ausgabe dazu:

[1] connected: myuser:***@myhost/mydb
[2] connected: myuser:***@myhost/mydb

Obwohl das neue Objekt eigentlich dem Vorherigen entspricht, wird eine gänzlich neue Instanz (Nr. 2) geschaffen. Geschieht das recht häufig innerhalb des komplexen Systems, dann kommt auf Dauer doch einiges an Speicher zusammen.

Nun kann man natürlich das $objDatabase global verfügbar machen oder via Parameter und Referenzen durch die gesamte Codegeschichte reichen. Ersteres halte ich (mit bestem Gruß an WordPress) für ganz schlechten Stil, letzteres ist unnötig aufwendig. Beide Varianten werden schnell unübersichtlich und damit fehleranfällig, zudem schützen sie nicht davor, dass z.B. ein Plugin-Entwickler aus Unwissenheit doch neue Objekte erzeugt.

Singleton als Lösung

An dieser Stelle kommt jetzt das Entwurfs- bzw. Erzeugungsmuster Singleton in’s Spiel. Damit wird der Klasse sozusagen beigebracht, auf sich selbst aufzupassen. Statt über den Constructor, also mit new Klassenname, eine neue Instanz zu erzeugen, wird die statische Singleton-Methode verwendet. Diese überprüft, ob bereits eine Instanz erzeugt wurde. Ist dies der Fall, so wird die bestehende Instanz geliefert, ansonsten eine neue erzeugt. Schauen wir uns das mal an unserer Pseudo-Datenbankklasse an:

class myDatabase {

	private static 	$_objInstance = NULL,
			$_aryConData = array(),
			$_intCounter = 0;

	private  	$_strStatus = 'disconnected',
			$_intID;
	// ... weitere Eigenschaften


	static function singleton($aryConData = array()) {
		if (self::$_objInstance === NULL)
			self::$_objInstance = new self($aryConData);
		return self::$_objInstance;
	}

	public function __construct($aryConData = array()) {
		if (!empty($aryConData))
			self::$_aryConData = $aryConData;
		$this->_intID = ++self::$_intCounter;
		$this->connect();	
	}

	public function connect() {
		// Connect to your database
		$this->_strStatus = 'connected';
	}

	public function getStatus() {
		return '['.$this->_intID.'] '
			.$this->_strStatus.': '
			.self::$_aryConData[1]
			.':***@'.self::$_aryConData[0]
			.'/'.self::$_aryConData[3];
	}
	// ... weitere Methoden
}

Wir haben eine neue statische Variable $_objInstance eingefügt, die das einzige aus der Klasse erzeugte Objekt speichern soll und die zuvor beschriebene Methode singleton ergänzt. Nun passen wir natürlich auch unseren Testcode entsprechend an:

$aryConData = array('myhost', 'myuser', 'mypassword', 'mydb');
$objDatabase = myDatabase::singleton($aryConData);
echo $objDatabase->getStatus().'
'; // Ganz viel Magie $objDatabase = myDatabase::singleton(); echo $objDatabase->getStatus();

Werfen wir wieder einen Blick auf die Ausgabe:

[1] connected: myuser:***@myhost/mydb
[1] connected: myuser:***@myhost/mydb

Siehe da: Es existiert nur noch eine einzige Klasseninstanz, d.h. wir müssen uns keine Sorgen mehr um unnötige Duplikate machen. Umständliche Lösung wie global sind damit unnötig.

Erzeugung ohne Singleton unterbinden

Einziger Haken: Natürlich könnte immer noch ein Entwickler aus Unwissenheit neue Objekte erzeugen. Hier können wir mit einer kleinen Anpassung an der Definition des Constructors Abhilfe schaffen:

	private function __construct($aryConData = array()) {

Da der Constructor jetzt private ist, kann er nur noch aus dem Klassenkontext heraus verwendet werden, d.h. singleton kann nach wie vor darauf zugreifen, aber eine Instanzerzeugung von außerhalb via new ist jetzt ausgeschlossen.

Verwendung für mehrere Datenbankverbindungen

Oft kommt natürlich immer alles vollkommen anders, als man zu Beginn denkt. Kaum wurde das Entwurfsmuster Singleton umgesetzt, schon muss man feststellen, dass aus irgendwelchen Gründen doch eine Verbindung zu einer zweiten, unabhängigen Datenbank notwendig ist. Nun muss aber nicht die bisherige Klasse vollständig kopiert und als Alternativklasse angeboten werden. Stattdessen machen wir unser singleton etwas flexibler, um mehrere Instanzen für verschiedene Datenbanken verwalten zu können.

Hier der angepasste Code:

class myDatabase {

	private static 	$_aryInstances = array(),
			$_intCounter = 0;

	private 	$_strStatus = 'disconnected',
			$_aryConData = array(),
			$_intID;

	// ... weitere Eigenschaften

	static function singleton($aryConData = array()) {
		$strFingerprint = serialize($aryConData);
		if (!isset(self::$_aryInstances[$strFingerprint]))
			self::$_aryInstances[$strFingerprint] = new self($aryConData);
		return self::$_aryInstances[$strFingerprint];
	}

	private function __construct($aryConData = array()) {
		if (!empty($aryConData))
			$this->_aryConData = $aryConData;
		$this->_intID = ++self::$_intCounter;
		$this->connect();	
	}

	public function connect() {
		// Connect to your database
		$this->_strStatus = 'connected';
	}

	public function getStatus() {
		return '['.$this->_intID.'] '
			.$this->_strStatus.': '
			.$this->_aryConData[1]
			.':***@'.$this->_aryConData[0]
			.'/'.$this->_aryConData[3];
	}
	// ... weitere Methoden
}

Folgende Änderungen wurden vorgenommen:

  1. Die statische Instanz-Variable ist nun ein Array, um mehrere Instanzen beinhalten zu können.
  2. Die statische Variable für die Verbindungsdaten ist nun eine normale Eigenschaft, da diese sich ja in jeder Instanz ändern soll.
  3. singleton bildet einen „Fingerabdruck“ aus den Verbindungsdaten, um einzelne Instanzen über den Key des Instanz-Arrays unterscheiden zu können.

Auch hierzu nochmal ein Testcode:

$aryConData = array('myhost', 'myuser', 'mypassword', 'mydb');
$aryConData2 = array('yourhost', 'youruser', 'yourpassword', 'yourdb');
$objDatabase = myDatabase::singleton($aryConData);
echo $objDatabase->getStatus().'
'; // Ganz viel Magie $objDatabase = myDatabase::singleton($aryConData2); echo $objDatabase->getStatus().'
'; // Noch mehr Mage $objDatabase = myDatabase::singleton($aryConData); echo $objDatabase->getStatus();

Zwischen den beiden Verwendungen der identischen Verbindung, wird eine weitere Verbindung geöffnet. Das Resultat in der Ausgabe:

[1] connected: myuser:***@myhost/mydb
[2] connected: youruser:***@yourhost/yourdb
[1] connected: myuser:***@myhost/mydb

Wir haben zwei unterschiedliche Instanzen – für jeden Verwendungszweck genau eine. Viel Erfolg beim Schonen der Ressourcen!

Literatur

Mehr zum Thema Entwurfsmuster findet ihr im Softwaretechnik-Standardwerk der sogenannten „Gang of Four“ (Gamma, Helm, Johnson und Vlissides): Design Patterns. Elements of Reusable Object-Oriented Software.*