PHP 5.2 Abstract Singletons: “abstract public static function” issue

Ein Problem über das ich schon häufiger gestolpert bin ist folgendes: Man will aus irgeneinem Grund eine abstrakte Singleton-Klasse definieren, sprich alle abgeleiteten Klassen sollen nur jeweils einmal instanziert werden.

In PHP 5.3 dank late static binding sehr einfach lösbar: Beispiel

In vorherigen Versionen ist es auf diese Weise nicht lösbar, die naive Herangehensweise wäre daher so etwas:

abstract class AbstractSingleton
{
	protected function __construct() { }
	private function __clone() { }
	abstract public static function getInstance();
}
class ConcreteSingleton extends AbstractSingleton
{
	private static $instance;
	public static function getInstance()
	{
		if (empty(self::$instance)) {
			self::$instance = new ConcreteSingleton();
		}
		return self::$instance;
	}
}

was jedoch seit PHP 5.2 berechtigerweise einen E_STRICT Fehler wegen “abstract static” wirft.

Nun sollte man sich fragen, warum so etwas überhaupt gewünscht sein könnte. In meinem aktuellen Fall ging es um eine Klasse für Plugins, die wiederum von anderen Plugins abhängig sein können. Geplant war so etwas:

Fallbeispiel: Plugin-System

interface Plugin
{
	public function getDependencies();

	// ...
}
abstract class BasePlugin implements Plugin
{
	protected $dependencies = array();

	public function getDependencies()
	{
		// returns array of Plugins
		$result = array();
		foreach($this->dependencies as $_plugin) {
			if (is_subclass_of($_plugin, __CLASS__)) {
				$result[] = call_user_func(array($_plugin,'getInstance'));
			}
		}
		return $result;
	}

	// ...
}
class ConcretePlugin extends BasePlugin
{
	protected $dependencies = array('OtherPlugin2', 'OtherPlugin2');
}

Ich wollte also in den konkreten Plugins nur die Klassenamen der benötigten anderen Plugins angeben, mit getDependencies() aber konkrete Objekte erhalten. Weiterhin sollte von jedem Plugin nur eine Instanz existieren, damit in der Hauptanwendung einfach geprüft werden kann, ob ein Plugin bzw. seine Dependencies bereits registriert sind.

Normalerweise würde man nun einfach in jeder konkreten Klasse eine getInstance()-Klassenmethode schreiben, da sie sowieso als ConcretePlugin::getInstance() aufgerufen würde und an dieser Stelle die Existenz der Methode auch ohne irgendeine Abstraktion bekannt ist.

Warum also Abstract Singleton?

Man sieht an der hervorgehobenen Zeile, warum ich nun trotzdem gerne eine abstrakte getInstance() Methode gehabt hätte: Es soll geprüft werden, ob der gegebene Klassenname eine Realisierung von AbstractPlugin repräsentiert und in dem Fall deren Methode getInstance() aufgerufen werden.

Es gäbe hier einige mögliche Workarounds, z.B. mit zusätzlicher Prüfung von method_exists() oder einer einzigen parametrisierten getInstance()-Methode in der Basis-Klasse:

	public static function getInstance($concreteClass)
	{
		if (empty(self::$instances[$concreteClass])) {
			self::$instances[$concreteClass] = new $concreteClass();
		}
		return self::$instances[$concreteClass];
	}

jedoch sind dies eben Workarounds, die einen schalen Beigeschmack hinterlassen. Also zurück zur Frage:

Muss das denn sein?

Schon der ursprüngliche Entwurf enthät mit call_user_func() einen Code Smell. Auch die Entscheidung, ein Singleton einzusetzen, war eigentlich nur durch Bequemlichkeit gerechtfertigt. Also mehr als ein Grund, sich etwas ganz anderes zu überlegen 😉

Ich denke, das selbe trifft in den meisten Fällen zu, in denen derartiges versucht wird. Bei jedem Wunsch nach einer abstract static Methode sollte also zunächst das gesamte zugrundeliegende Design in Frage gestellt werden bevor nach irgendwelchen Workarounds gesucht wird!

Konkret: Lösungen für das Plugin-System

Die naheliegendste objektorientierte Lösung wäre, $dependencies direkt mit den Plugin-Objekten anstelle deren Klassennamen zu füllen. Sollte die Implementierung der konkreten Plugins damit zu unübersichtlich aussehen, ließe sich die Definition der Abhängigkeiten natürlich in Textform in eine Konfigurationsdatei auslagern.

Ich habe mich allerdings dafür entschieden, die Abhängigkeiten innerhalb der Plugin-Klassen nur noch mit Klassennamen zu behandeln. Sollten Instanzen benötigt werden, werden diese von der Haupt-Applikation bereitgestellt, diese sorgt für die Instanzierung, enthält die Plugins in einem assoziativen Array mit den Klassennamen als Schlüsseln und kann so problemlos sicherstellen dass jedes Plugin nur einmal registriert wird ohne dass ich mir eine weitere Einschränkung (die das Singleton bedeutet hätte) aufhalse.