Singleton je jedním z nejpopulárnějších návrhových vzorů. Jeho úkolem je zajistit existenci pouze jediné instance určité třídy a zároveň poskytnout globální přístup k ní. Pro úplnost malý příklad:

class Database
{
	private static $instance;

	private function __construct()
	{}

	public static function getInstance()
	{
		if (self::$instance === null) {
			self::$instance = new self;
		}
		return self::$instance;
	}

	...
}

// singleton je globálně dostupný
$result = Database::getInstance()->query('...');

Typickými rysy jsou:

  • privátní konstruktor, znemožňující vytvoření instance mimo třídu
  • statická vlastnost $instance, kde je unikátní instance uložená
  • statická metoda getInstance(), která zpřístupní instanci a při prvním volání ji vytvoří (lazy loading)

Jednoduchý a snadno pochopitelný kód, který řeší dva problémy objektového programování. Přesto v dibi nebo Nette Framework žádné singletony nenajdete. Proč?

Unikátnost jen zdánlivá

Podívejme se na kód pozorněji – skutečně ručí za existenci pouze jedné instance? Obávám se, že nikoliv:

$dolly = clone Database::getInstance();

// nebo
$dolly = unserialize(serialize(Database::getInstance()));

// nebo
class Dolly extends Database {}
$dolly = Dolly::getInstance();

Proti tomu existuje obrana:

	final public static function getInstance()
	{
		// finální getInstance
	}

	final public function __clone()
	{
		throw new Exception('Clone is not allowed');
	}

	final public function __wakeup()
	{
		throw new Exception('Unserialization is not allowed');
	}

Jednoduchost implementace singletonu je ta tam. Ba co hůř – s každým dalším singletonem se nám bude opakovat kusanec stejného kódu. Také třída najednou plní dva zcela odlišné úkoly: kromě svého původního úkolu se stará o to být dost single. Obojí je varovný signál, že něco není v pořádku a kód by si zasloužil refaktoring. Vydržte, za chvíli se k tomu vrátím.

Globální = ošklivé?

Singletony poskytují globální přístupový bod k objektům. Není nutné si odkaz na ně neustále předávat. Zlí jazykové však tvrdí, že taková technika se nijak neliší od používání globálních proměnných a ty jsou přece čiré zlo.

(Pokud metoda pracuje s objektem, který jí byl nějak explicitně předán, parametrem nebo jde o proměnnou objektu, říkám tomu „drátové spojení“. Pokud pracuje s objektem, který si získá přes globální bod (např. skrz singleton), říkám tomu „bezdrátové spojení“. Docela fajn analogie, ne?)

Zlí jazykové se v jedné věci mýlí – na „globálním“ není a priori nic špatného. Stačí si uvědomit, že název každé třídy a metody není nic jiného, než globální identifikátor. Mezi bezproblémovou konstrukcí $obj = new MyClass a kritizovanou $obj = MyClass::getInstance() není ve skutečnosti zásadní rozdíl. Tím méně u dynamických jazyků jako je PHP, kde lze psát $obj = $class::getInstance().

Co ale může způsobit bolení hlavy, to jsou

  1. skryté závislosti na globálních proměnných
  2. nečekané používání „bezdrátových spojení“, které z API tříd nevyčtete (viz Singletons are Pathological Liars)

První bod se dá eliminovat, pokud se nebudou singletony chovat jako globální proměnné, ale spíš jako globální funkce či služby. Jak tomu rozumět? Vezměte si třeba google.com – hezký příklad singletonu jakožto globální služby. Existuje jedna instance (fyzická serverová farma kdesi v USA) globálně dostupná přes identifikátor www.google.com. (Dokonce ani clone www.google.com nefunguje, jak zjistil Microsoft, maj to holt kluci vychytaný.) Důležité je, že tato služba nemá skryté závislosti typické pro globální proměnné – vrací odpovědi bez neočekávatelné souvislosti s tím, co před chvíli hledal někdo úplně jiný. Naopak nenápadná funkce strtok trpí vážnou závislostí na globální proměnné a její používání může vést k velmi těžko odhalitelným chybám. Jinými slovy – problém není v „globálnosti“, ale v návrhu.

Také druhý bod je čistě záležitostí návrhu kódu. Není chybou použít „bezdrátové spojení“ a přistoupit ke globální službě, chybou je to dělat proti očekávání čili skrytě. Programátor by měl přesně vědět, jaký objekt využívá které třídy. Poměrně čistým řešením je mít v objektu proměnnou odkazující na objekt služby, která se inicializuje na globální službu, pokud programátor nerozhodne jinak (technika convention over configuration).

Unikátnost může být na škodu

K singletonům se váže problém, na který narazíme nejpozději při testování kódu. A tím je potřeba podstrčit jiný, testovací objekt. Vraťme se ke Google jakožto ukázkovému singletonu. Chceme otestovat aplikaci, která jej využívá, jenže po pár stech testech začne Google protestovat We're sorry… a jsme kde? A jsme někde. Řešením je pod identifikátor www.google.com podstrčit fiktivní (mock) službu. Potřebujeme upravit soubor hosts – jenže (zpět z analogie do světa OOP) jak toho dosáhnout u singletonů?

Jednou z možností je implementovat statickou metodu setInstance($mockObj). Ale ouha! Copak asi chcete slečně metodě předat, když žádná jiná instance, než ona jedna jediná, neexistuje?

Jakýkoliv pokus odpovědět na tuto otázku povede nevyhnutelně k rozpadu všeho, co singleton dělá singletonem.

Pokud odstraníme restrikce na existenci jen jedné instance, přestane být singleton single a my řešíme pouze potřebu globálního úložiště. Pak je nabíledni otázka, proč v kódu stále opakovat stejnou metodu getInstance() a nepřesunout ji do extra třídy, do nějakého globálního registru?

Anebo ponecháme restrikce, pouze identifikátor v podobě třídy nahradíme za interface (DatabaseIDatabase), čímž se vynoří problém nemožnosti implementovat IDatabase::getInstance() a řešením je opět globální registr.

O pár odstavců výše jsem sliboval vrátit se k otázce opakujícího se kódu ve všech singletonech a možnému refaktoringu. Jak vidíte, problém se mezitím vyřešil sám. Singleton zemřel.