Nette Documentation Preview

syntax
Globaler Zustand und Singletons
*******************************

.[perex]
Achtung: Die folgenden Konstrukte sind Symptome schlecht entworfenen Codes:

- `Foo::getInstance()`
- `DB::insert(...)`
- `Article::setDb($db)`
- `ClassName::$var` oder `static::$var`

Kommt eines dieser Konstrukte in Ihrem Code vor? Dann haben Sie die Gelegenheit, ihn zu verbessern. Vielleicht denken Sie, das seien gängige Konstrukte, die man auch in Beispiellösungen verschiedener Bibliotheken und Frameworks sieht. Wenn ja, ist deren Code-Entwurf mangelhaft.

Wir reden hier nicht von akademischer Reinheit. Alle diese Konstrukte haben eines gemeinsam: Sie nutzen globalen Zustand. Und globaler Zustand wirkt sich verheerend auf die Qualität des Codes aus. Klassen täuschen über ihre Abhängigkeiten hinweg. Der Code wird unvorhersehbar. Er verwirrt Entwickler und mindert ihre Effizienz.

In diesem Kapitel erklären wir, warum das so ist und wie sich globaler Zustand vermeiden lässt.


Globale Verflechtung
--------------------

In einer idealen Welt sollte ein Objekt nur mit Objekten kommunizieren, die ihm [direkt übergeben wurden |passing-dependencies]. Erzeuge ich zwei Objekte `A` und `B` und übergebe zwischen ihnen nie eine Referenz, kann weder `A` noch `B` auf den Zustand des anderen zugreifen oder ihn verändern. Das ist eine höchst wünschenswerte Eigenschaft von Code. Es ist wie mit einer Batterie und einer Glühbirne; die Birne leuchtet nicht, bis Sie sie mit einem Draht an die Batterie anschließen.

Für globale (statische) Variablen oder Singletons gilt das jedoch nicht. Objekt `A` könnte *drahtlos* auf Objekt `C` zugreifen und es verändern, ohne dass eine Referenz übergeben wurde, indem es `C::changeSomething()` aufruft. Greift auch Objekt `B` auf das globale `C` zu, können sich `A` und `B` über `C` gegenseitig beeinflussen.

Der Einsatz globaler Variablen führt eine neue Form der *drahtlosen* Kopplung ein, die von außen unsichtbar ist. Er erzeugt eine Nebelwand, die den Code schwerer verständlich und schwerer benutzbar macht. Um die Abhängigkeiten wirklich zu erfassen, müssen Entwickler jede Zeile des Quellcodes lesen, statt sich einfach auf die Interfaces der Klassen zu verlassen. Und diese Kopplung ist obendrein völlig unnötig. Globaler Zustand wird verwendet, weil er von überall leicht zugänglich ist und es zum Beispiel erlaubt, über eine globale (statische) Methode `DB::insert()` in die Datenbank zu schreiben. Wie wir aber zeigen werden, ist die vermeintliche Bequemlichkeit verschwindend klein gegenüber den schweren Komplikationen, die er mit sich bringt.

.[note]
Was das Verhalten angeht, gibt es zwischen einer globalen und einer statischen Variablen keinen Unterschied. Sie sind gleichermaßen schädlich.


Die spukhafte Fernwirkung
-------------------------

"Spukhafte Fernwirkung" - so nannte Albert Einstein bekanntlich ein Phänomen der Quantenphysik, das ihm nicht geheuer war.
Gemeint ist die Quantenverschränkung, bei der das Messen einer Eigenschaft eines Teilchens augenblicklich ein anderes, verschränktes Teilchen beeinflusst, ganz gleich, wie weit sie voneinander entfernt sind, und seien es Millionen Lichtjahre, was scheinbar das grundlegende Gesetz des Universums verletzt, dass sich nichts schneller als das Licht bewegen kann.

In der Welt der Software beschreibt "spukhafte Fernwirkung" eine Situation, in der wir einen Vorgang ausführen, den wir für isoliert halten (weil keine Abhängigkeiten ausdrücklich übergeben wurden), und dennoch treten in entfernten Teilen des Systems unerwartete Wechselwirkungen und Zustandsänderungen auf, ohne dass wir davon wissen. Das kann nur über globalen Zustand geschehen.

Stellen Sie sich vor, Sie stoßen zu einem Entwicklungsteam mit einer großen, gereiften Codebasis. Ihr neuer Vorgesetzter bittet Sie, eine neue Funktion umzusetzen, und wie ein guter Entwickler beginnen Sie damit, einen Test zu schreiben. Weil Sie im Projekt aber neu sind, machen Sie eine Menge erkundende Tests der Art "was passiert, wenn ich diese Methode aufrufe". Und Sie versuchen, den folgenden Test zu schreiben:

```php
function testCreditCardCharge()
{
	$cc = new CreditCard('1234567890123456', 5, 2028); // Ihre Kartennummer
	$cc->charge(100);
}
```

Sie führen den Code aus, vielleicht mehrmals, und nach einer Weile bemerken Sie Benachrichtigungen der Bank auf Ihrem Telefon: Bei jedem Durchlauf wurden Ihrer Kreditkarte 100 $ belastet! 🤦‍♂️

Wie um alles in der Welt konnte der Test eine echte Belastung auslösen? Mit einer Kreditkarte zu arbeiten ist nicht einfach. Man muss mit einem Webservice eines Dritten kommunizieren, dessen URL kennen, sich authentifizieren und so weiter. Nichts davon steht im Test. Schlimmer noch: Sie wissen nicht, wo diese Informationen liegen, und können deshalb die externen Abhängigkeiten nicht mocken, um die Belastung von 100 $ bei jedem Testlauf zu verhindern. Und woher hätten Sie als neuer Entwickler wissen sollen, dass das, was Sie vorhatten, Sie um 100 $ ärmer macht?

Das ist eine spukhafte Fernwirkung!

Sie sind gezwungen, umfangreichen Quellcode zu durchforsten und erfahrene Kollegen zu befragen, um die Verflechtungen des Projekts zu verstehen. Diese Schwierigkeit entsteht, weil das Interface der Klasse `CreditCard` die nötige Initialisierung des globalen Zustands nicht offenlegt. Selbst ein Blick in den Quellcode der Klasse verrät womöglich nicht, welche Initialisierungsmethode aufzurufen ist. Bestenfalls finden Sie die globale Variable, auf die zugegriffen wird, und versuchen daraus abzuleiten, wie sie zu initialisieren ist.

Die Klassen in einem solchen Projekt sind pathologische Lügner. Die Klasse `CreditCard` tut so, als ließe sie sich einfach instanziieren und ihre Methode `charge()` aufrufen. In Wirklichkeit arbeitet sie heimlich mit einer anderen Klasse zusammen, `PaymentGateway`, die das Zahlungs-Gateway darstellt. Auch das Interface von `PaymentGateway` legt womöglich eine eigenständige Initialisierung nahe, in Wirklichkeit zieht es sich die Zugangsdaten aber vielleicht aus einer Konfigurationsdatei und so weiter. Die ursprünglichen Entwickler wissen, dass `CreditCard` das `PaymentGateway` braucht. Sie haben den Code so geschrieben. Für Neuankömmlinge ist es aber ein völliges Rätsel, das sie daran hindert, sich einzuarbeiten und wirksam beizutragen.

Wie lässt sich die Lage in Ordnung bringen? Ganz leicht. **Lassen Sie die API die Abhängigkeiten deklarieren.**

```php
function testCreditCardCharge()
{
	$gateway = new PaymentGateway(/* ... */);
	$cc = new CreditCard('1234567890123456', 5, 2028);
	$cc->charge($gateway, 100);
}
```

Beachten Sie, wie die gegenseitigen Abhängigkeiten im Code sofort sichtbar werden. Weil die Methode `charge()` deklariert, dass sie ein `PaymentGateway` braucht, müssen Sie diese Abhängigkeit nicht mehr erraten oder erfragen. Sie wissen, dass Sie eine Instanz erzeugen müssen, und dabei entdecken Sie die nötigen Zugangsparameter. Ohne sie liefe der Code nicht einmal.

Und vor allem können Sie das Zahlungs-Gateway jetzt mocken, sodass Ihnen nicht bei jedem Testlauf 100 $ belastet werden.

Globaler Zustand erlaubt es Objekten, heimlich auf Abhängigkeiten zuzugreifen, die in ihren APIs nicht deklariert sind, und macht Ihre APIs damit zu pathologischen Lügnern.

Vielleicht haben Sie das bisher nicht so gesehen, aber immer wenn Sie globalen Zustand verwenden, schaffen Sie geheime drahtlose Kommunikationskanäle. Diese spukhafte Fernwirkung zwingt Entwickler dazu, jede Codezeile zu lesen, um mögliche Wechselwirkungen zu verstehen, senkt die Produktivität und verwirrt neue Teammitglieder. Wenn Sie derjenige sind, der den Code geschrieben hat, kennen Sie die wahren Abhängigkeiten, aber jeder, der nach Ihnen kommt, tappt im Dunkeln.

Schreiben Sie keinen Code, der sich auf globalen Zustand stützt; übergeben Sie Abhängigkeiten lieber ausdrücklich. Machen Sie sich Dependency Injection zu eigen.


Zerbrechlichkeit des globalen Zustands
--------------------------------------

In Code, der globalen Zustand und Singletons nutzt, können Sie nie sicher sein, wann und von wem der Zustand verändert wurde. Dieses Risiko zeigt sich schon bei der Initialisierung. Der folgende Code will eine Datenbankverbindung erzeugen und ein Zahlungs-Gateway initialisieren, wirft aber immer wieder Exceptions, und die Ursache zu finden ist äußerst mühsam:

```php
PaymentGateway::init();
DB::init('mysql:', 'user', 'password');
```

Sie müssen den Code sorgfältig verfolgen, um herauszufinden, dass das Objekt `PaymentGateway` drahtlos auf andere Objekte zugreift, von denen einige eine Datenbankverbindung brauchen. Die Datenbank muss also vor `PaymentGateway` initialisiert werden. Die Nebelwand des globalen Zustands verbirgt das aber vor Ihnen. Wie viel Zeit ließe sich sparen, wenn die APIs dieser Klassen ehrlich wären und ihre Abhängigkeiten deklarierten?

```php
$db = new DB('mysql:', 'user', 'password');
$gateway = new PaymentGateway($db, /* ... */);
```

Ein ähnliches Problem entsteht beim globalen Zugriff auf eine Datenbankverbindung:

```php
use Illuminate\Support\Facades\DB;

class Article
{
	public function save(): void
	{
		DB::insert(/* ... */);
	}
}
```

Beim Aufruf der Methode `save()` ist unklar, ob eine Datenbankverbindung aufgebaut wurde und wer dafür zuständig ist. Müssen wir die Datenbankverbindung dynamisch ändern (etwa für Tests), greifen wir womöglich zu Methoden wie `DB::reconnect(...)` oder `DB::reconnectForTest()`.

Betrachten wir ein Beispiel:

```php
$article = new Article;
// ...
DB::reconnectForTest();
Foo::doSomething();
$article->save();
```

Wie können wir sicher sein, dass beim Aufruf von `$article->save()` tatsächlich die Testdatenbank verwendet wird? Was, wenn die Methode `Foo::doSomething()` die globale Datenbankverbindung geändert hat? Um das festzustellen, müssten wir den Quellcode von `Foo` und womöglich vieler weiterer Klassen prüfen. Diese Untersuchung gäbe uns nur eine vorübergehende Antwort, denn die Lage könnte sich später ändern.

Was, wenn wir die Datenbankverbindung in eine statische Variable innerhalb der Klasse `Article` verschieben?

```php
class Article
{
	private static DB $db;

	public static function setDb(DB $db): void
	{
		self::$db = $db;
	}

	public function save(): void
	{
		self::$db->insert(/* ... */);
	}
}
```

Das ändert überhaupt nichts. Das Problem ist der globale Zustand selbst, ganz gleich, in welcher Klasse er versteckt ist. In diesem Szenario haben wir wie im vorigen beim Aufruf von `$article->save()` keine Gewissheit, in welche Datenbank die Daten geschrieben werden. Jeder könnte irgendwo in der Anwendung jederzeit über `Article::setDb()` die Datenbank geändert haben. Ohne dass wir davon wissen.

Der globale Zustand macht unsere Anwendung **äußerst zerbrechlich**.

Es gibt jedoch einen einfachen Weg, mit diesem Problem umzugehen. Lassen Sie die API einfach die Abhängigkeiten deklarieren, die für die einwandfreie Funktion nötig sind.

```php
class Article
{
	public function __construct(
		private DB $db,
	) {
	}

	public function save(): void
	{
		$this->db->insert(/* ... */);
	}
}

$article = new Article($db);
// ...
Foo::doSomething();
$article->save();
```

Dieser Ansatz nimmt die Sorge vor versteckten oder unerwarteten Änderungen der Datenbankverbindung. Wir wissen jetzt sicher, wohin der Artikel gespeichert wird, und Änderungen in unbeteiligten Klassen können daran nichts mehr ändern. Der Code ist nicht mehr zerbrechlich, sondern stabil.

Schreiben Sie keinen Code, der sich auf globalen Zustand stützt; übergeben Sie Abhängigkeiten lieber ausdrücklich. Machen Sie sich Dependency Injection zu eigen.


Singleton
---------

Singleton ist ein Entwurfsmuster, das nach der [Definition |https://en.wikipedia.org/wiki/Singleton_pattern] aus der berühmten Veröffentlichung der Gang of Four eine Klasse auf eine einzige Instanz beschränkt und globalen Zugriff darauf bietet. Die Umsetzung dieses Musters sieht üblicherweise so aus:

```php
class Singleton
{
	private static self $instance;

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

	// und weitere Methoden, die die Aufgaben der Klasse erfüllen
}
```

Leider führt das Singleton globalen Zustand in die Anwendung ein. Und wie wir oben gezeigt haben, ist globaler Zustand unerwünscht. Deshalb gilt das Singleton als Antipattern.

Verwenden Sie in Ihrem Code keine Singletons und ersetzen Sie sie durch andere Mechanismen. Sie brauchen Singletons wirklich nicht. Wenn Sie aber sicherstellen müssen, dass es in der gesamten Anwendung nur eine einzige Instanz einer Klasse gibt, überlassen Sie diese Verantwortung dem [DI-Container |container]. So entsteht ein Singleton im Gültigkeitsbereich der Anwendung, das üblicherweise Service genannt wird. Die Klasse selbst ist damit davon befreit, ihre Einzigartigkeit zu verwalten (sie hat also weder eine Methode `getInstance()` noch eine statische Property für die Instanz) und kann sich ganz auf ihre Aufgaben konzentrieren. Damit verstößt sie nicht länger gegen das Prinzip der einzigen Verantwortung.


Globaler Zustand und Tests
--------------------------

Beim Schreiben von Tests gehen wir im Idealfall davon aus, dass jeder Test eine isolierte Einheit ist, in die kein Zustand von außen hinein- und aus der keiner hinausgelangt. Nachdem ein Test durchgelaufen ist, sollte jeder mit ihm verbundene Zustand automatisch vom Garbage Collector aufgeräumt werden. Das macht die Tests isoliert. Deshalb können wir sie in beliebiger Reihenfolge ausführen.

Sobald aber globaler Zustand oder Singletons im Spiel sind, zerfallen diese nützlichen Annahmen. Zustand kann in Tests hinein- und aus ihnen hinausdringen. Plötzlich kann die Reihenfolge der Tests eine Rolle spielen.

Um Code mit Singletons überhaupt testen zu können, müssen Entwickler oft Abstriche an deren Unversehrtheit machen und zum Beispiel erlauben, die Instanz des Singletons zu ersetzen. Solche Lösungen sind bestenfalls Hacks, die zu Code führen, der schwer zu warten und zu verstehen ist. Jeder Test (oder seine Methode `tearDown()`), der globalen Zustand verändert, muss diese Änderungen sorgfältig zurücknehmen.

Globaler Zustand ist das größte Kopfzerbrechen beim Unit-Testing!

Wie bringt man das in Ordnung? Ganz einfach. Schreiben Sie keinen Code, der Singletons verwendet; übergeben Sie Abhängigkeiten lieber ausdrücklich. Machen Sie sich Dependency Injection zu eigen.


Globale Konstanten
------------------

Globaler Zustand beschränkt sich nicht auf Singletons und statische Variablen, er kann auch globale Konstanten betreffen.

Konstanten, deren Werte universelle Wahrheiten darstellen (`M_PI`) oder in sich abgeschlossene Informationen liefern (`PREG_BACKTRACK_LIMIT_ERROR`), sind in der Regel unbedenklich. Umgekehrt sind Konstanten, die als Weg dienen, Informationen *drahtlos* in den Code zu schleusen, faktisch versteckte Abhängigkeiten. So wie `LOG_FILE` im folgenden Beispiel. Die Verwendung der Konstanten `FILE_APPEND` ist dagegen völlig richtig.

```php
const LOG_FILE = '...';

class Foo
{
	public function doSomething()
	{
		// ...
		file_put_contents(LOG_FILE, $message . "\n", FILE_APPEND);
		// ...
	}
}
```

Stattdessen sollten wir den Pfad zur Logdatei als Parameter im Konstruktor der Klasse `Foo` deklarieren und ihn damit zu einem ausdrücklichen Teil ihrer API machen:

```php
class Foo
{
	public function __construct(
		private string $logFile,
	) {
	}

	public function doSomething()
	{
		// ...
		file_put_contents($this->logFile, $message . "\n", FILE_APPEND);
		// ...
	}
}
```

Jetzt übergeben wir den Pfad zur Logdatei ausdrücklich. Wir können ihn nach Bedarf leicht ändern, was das Testen und die Wartung des Codes vereinfacht.


Globale Funktionen und statische Methoden
-----------------------------------------

Wir möchten betonen, dass die Verwendung statischer Methoden und globaler Funktionen an sich kein Problem ist. Wir haben die Schwierigkeiten mit Methoden wie `DB::insert()` erklärt, aber das eigentliche Problem war immer der dahinterliegende globale Zustand, üblicherweise in einer statischen Variablen. Die Methode `DB::insert()` verlässt sich auf eine statische Variable, die die Datenbankverbindung hält. Ohne diese Variable ließe sich die Methode gar nicht umsetzen.

Deterministische statische Methoden und Funktionen wie `Closure::fromCallable()`, `strlen()` und viele andere zu verwenden, ist mit Dependency Injection vollkommen vereinbar. Diese Funktionen sind vorhersehbar, weil sie für dieselben Eingabeparameter immer dasselbe Ergebnis liefern. Sie verwenden keinerlei globalen Zustand.

In PHP gibt es allerdings Funktionen, die nicht deterministisch sind. Dazu zählt zum Beispiel die Funktion `htmlspecialchars()`. Ihr dritter Parameter `$encoding` nimmt, wenn er weggelassen wird, standardmäßig den Wert der Konfigurationsoption `default_charset` an (`ini_get('default_charset')`). Es empfiehlt sich deshalb, diesen Parameter immer anzugeben, um mögliches unvorhersehbares Verhalten zu vermeiden. Nette macht das durchgängig.

Manche Funktionen wie `strtolower()` und `strtoupper()` verhielten sich in der jüngeren Vergangenheit nicht deterministisch, sondern abhängig von der Einstellung des Locale (`setlocale()`). Das führte zu vielen Komplikationen, am häufigsten bei der Arbeit mit der türkischen Sprache. Das Türkische unterscheidet nämlich sowohl in Klein- als auch in Großbuchstaben zwischen dem 'I' mit und ohne Punkt. Folglich gab `strtolower('I')` den Wert `ı` zurück (kleines i ohne Punkt) und `strtoupper('i')` den Wert `İ` (großes I mit Punkt), was zu zahlreichen rätselhaften Fehlern in Anwendungen führte. Dieses Problem wurde in PHP 8.2 behoben, die Funktionen hängen nicht mehr vom Locale ab.

Das ist ein gutes Beispiel dafür, wie globaler Zustand (die Einstellung des Locale) Tausende Entwickler weltweit geplagt hat. Die endgültige Lösung bestand darin, die Funktionen unabhängig vom Locale zu machen und damit die versteckte Abhängigkeit zu entfernen.


Wann lässt sich globaler Zustand verwenden?
-------------------------------------------

Es gibt bestimmte, eng begrenzte Situationen, in denen die Verwendung von globalem Zustand vertretbar ist. Zum Beispiel beim Debuggen, wenn Sie den Wert einer Variablen ausgeben oder die Laufzeit eines bestimmten Codeabschnitts messen müssen. In diesen Fällen, in denen es um vorübergehende Eingriffe geht, die später wieder aus dem Code verschwinden, kann ein global zugänglicher Dumper oder Timer legitim sein. Diese Werkzeuge sind nicht Teil des eigentlichen Entwurfs der Anwendung.

Ein weiteres Beispiel sind die Funktionen von PHP für reguläre Ausdrücke (`preg_*`), die kompilierte reguläre Ausdrücke intern in einem statischen Speicher zwischenspeichern. Wenn Sie die Funktionen an mehreren Stellen Ihres Codes mit demselben regulären Ausdruck aufrufen, wird der Ausdruck nur einmal kompiliert. Dieses Caching verbessert die Leistung und ist für den Nutzer vollständig unsichtbar, weshalb diese Verwendung von internem statischem Zustand allgemein akzeptabel ist.


Zusammenfassung
---------------

Wir haben besprochen, warum es sinnvoll ist:

1) alle veränderlichen statischen Properties (globalen Zustand) aus Ihrem Code zu entfernen
2) Abhängigkeiten ausdrücklich zu deklarieren
3) und Dependency Injection zu nutzen

Denken Sie beim Entwurf Ihres Codes daran, dass jedes veränderliche `static $foo` eine mögliche Quelle von Problemen ist. Um eine DI-freundliche Umgebung zu schaffen, ist es entscheidend, globalen Zustand vollständig zu beseitigen und durch Dependency Injection zu ersetzen.

Dabei stellen Sie vielleicht fest, dass Sie Klassen mit mehreren Verantwortlichkeiten aufteilen müssen. Zögern Sie nicht; streben Sie das Single Responsibility Principle an.

*Ich möchte Miško Hevery danken, dessen Artikel wie [Flaw: Brittle Global State & Singletons |https://web.archive.org/web/20230321084133/http://misko.hevery.com/code-reviewers-guide/flaw-brittle-global-state-singletons/] die Grundlage dieses Kapitels bilden.*

Globaler Zustand und Singletons

Achtung: Die folgenden Konstrukte sind Symptome schlecht entworfenen Codes:

  • Foo::getInstance()
  • DB::insert(...)
  • Article::setDb($db)
  • ClassName::$var oder static::$var

Kommt eines dieser Konstrukte in Ihrem Code vor? Dann haben Sie die Gelegenheit, ihn zu verbessern. Vielleicht denken Sie, das seien gängige Konstrukte, die man auch in Beispiellösungen verschiedener Bibliotheken und Frameworks sieht. Wenn ja, ist deren Code-Entwurf mangelhaft.

Wir reden hier nicht von akademischer Reinheit. Alle diese Konstrukte haben eines gemeinsam: Sie nutzen globalen Zustand. Und globaler Zustand wirkt sich verheerend auf die Qualität des Codes aus. Klassen täuschen über ihre Abhängigkeiten hinweg. Der Code wird unvorhersehbar. Er verwirrt Entwickler und mindert ihre Effizienz.

In diesem Kapitel erklären wir, warum das so ist und wie sich globaler Zustand vermeiden lässt.

Globale Verflechtung

In einer idealen Welt sollte ein Objekt nur mit Objekten kommunizieren, die ihm direkt übergeben wurden. Erzeuge ich zwei Objekte A und B und übergebe zwischen ihnen nie eine Referenz, kann weder A noch B auf den Zustand des anderen zugreifen oder ihn verändern. Das ist eine höchst wünschenswerte Eigenschaft von Code. Es ist wie mit einer Batterie und einer Glühbirne; die Birne leuchtet nicht, bis Sie sie mit einem Draht an die Batterie anschließen.

Für globale (statische) Variablen oder Singletons gilt das jedoch nicht. Objekt A könnte drahtlos auf Objekt C zugreifen und es verändern, ohne dass eine Referenz übergeben wurde, indem es C::changeSomething() aufruft. Greift auch Objekt B auf das globale C zu, können sich A und B über C gegenseitig beeinflussen.

Der Einsatz globaler Variablen führt eine neue Form der drahtlosen Kopplung ein, die von außen unsichtbar ist. Er erzeugt eine Nebelwand, die den Code schwerer verständlich und schwerer benutzbar macht. Um die Abhängigkeiten wirklich zu erfassen, müssen Entwickler jede Zeile des Quellcodes lesen, statt sich einfach auf die Interfaces der Klassen zu verlassen. Und diese Kopplung ist obendrein völlig unnötig. Globaler Zustand wird verwendet, weil er von überall leicht zugänglich ist und es zum Beispiel erlaubt, über eine globale (statische) Methode DB::insert() in die Datenbank zu schreiben. Wie wir aber zeigen werden, ist die vermeintliche Bequemlichkeit verschwindend klein gegenüber den schweren Komplikationen, die er mit sich bringt.

Was das Verhalten angeht, gibt es zwischen einer globalen und einer statischen Variablen keinen Unterschied. Sie sind gleichermaßen schädlich.

Die spukhafte Fernwirkung

„Spukhafte Fernwirkung“ – so nannte Albert Einstein bekanntlich ein Phänomen der Quantenphysik, das ihm nicht geheuer war. Gemeint ist die Quantenverschränkung, bei der das Messen einer Eigenschaft eines Teilchens augenblicklich ein anderes, verschränktes Teilchen beeinflusst, ganz gleich, wie weit sie voneinander entfernt sind, und seien es Millionen Lichtjahre, was scheinbar das grundlegende Gesetz des Universums verletzt, dass sich nichts schneller als das Licht bewegen kann.

In der Welt der Software beschreibt „spukhafte Fernwirkung“ eine Situation, in der wir einen Vorgang ausführen, den wir für isoliert halten (weil keine Abhängigkeiten ausdrücklich übergeben wurden), und dennoch treten in entfernten Teilen des Systems unerwartete Wechselwirkungen und Zustandsänderungen auf, ohne dass wir davon wissen. Das kann nur über globalen Zustand geschehen.

Stellen Sie sich vor, Sie stoßen zu einem Entwicklungsteam mit einer großen, gereiften Codebasis. Ihr neuer Vorgesetzter bittet Sie, eine neue Funktion umzusetzen, und wie ein guter Entwickler beginnen Sie damit, einen Test zu schreiben. Weil Sie im Projekt aber neu sind, machen Sie eine Menge erkundende Tests der Art „was passiert, wenn ich diese Methode aufrufe“. Und Sie versuchen, den folgenden Test zu schreiben:

function testCreditCardCharge()
{
	$cc = new CreditCard('1234567890123456', 5, 2028); // Ihre Kartennummer
	$cc->charge(100);
}

Sie führen den Code aus, vielleicht mehrmals, und nach einer Weile bemerken Sie Benachrichtigungen der Bank auf Ihrem Telefon: Bei jedem Durchlauf wurden Ihrer Kreditkarte 100 $ belastet! 🤦‍♂️

Wie um alles in der Welt konnte der Test eine echte Belastung auslösen? Mit einer Kreditkarte zu arbeiten ist nicht einfach. Man muss mit einem Webservice eines Dritten kommunizieren, dessen URL kennen, sich authentifizieren und so weiter. Nichts davon steht im Test. Schlimmer noch: Sie wissen nicht, wo diese Informationen liegen, und können deshalb die externen Abhängigkeiten nicht mocken, um die Belastung von 100 $ bei jedem Testlauf zu verhindern. Und woher hätten Sie als neuer Entwickler wissen sollen, dass das, was Sie vorhatten, Sie um 100 $ ärmer macht?

Das ist eine spukhafte Fernwirkung!

Sie sind gezwungen, umfangreichen Quellcode zu durchforsten und erfahrene Kollegen zu befragen, um die Verflechtungen des Projekts zu verstehen. Diese Schwierigkeit entsteht, weil das Interface der Klasse CreditCard die nötige Initialisierung des globalen Zustands nicht offenlegt. Selbst ein Blick in den Quellcode der Klasse verrät womöglich nicht, welche Initialisierungsmethode aufzurufen ist. Bestenfalls finden Sie die globale Variable, auf die zugegriffen wird, und versuchen daraus abzuleiten, wie sie zu initialisieren ist.

Die Klassen in einem solchen Projekt sind pathologische Lügner. Die Klasse CreditCard tut so, als ließe sie sich einfach instanziieren und ihre Methode charge() aufrufen. In Wirklichkeit arbeitet sie heimlich mit einer anderen Klasse zusammen, PaymentGateway, die das Zahlungs-Gateway darstellt. Auch das Interface von PaymentGateway legt womöglich eine eigenständige Initialisierung nahe, in Wirklichkeit zieht es sich die Zugangsdaten aber vielleicht aus einer Konfigurationsdatei und so weiter. Die ursprünglichen Entwickler wissen, dass CreditCard das PaymentGateway braucht. Sie haben den Code so geschrieben. Für Neuankömmlinge ist es aber ein völliges Rätsel, das sie daran hindert, sich einzuarbeiten und wirksam beizutragen.

Wie lässt sich die Lage in Ordnung bringen? Ganz leicht. Lassen Sie die API die Abhängigkeiten deklarieren.

function testCreditCardCharge()
{
	$gateway = new PaymentGateway(/* ... */);
	$cc = new CreditCard('1234567890123456', 5, 2028);
	$cc->charge($gateway, 100);
}

Beachten Sie, wie die gegenseitigen Abhängigkeiten im Code sofort sichtbar werden. Weil die Methode charge() deklariert, dass sie ein PaymentGateway braucht, müssen Sie diese Abhängigkeit nicht mehr erraten oder erfragen. Sie wissen, dass Sie eine Instanz erzeugen müssen, und dabei entdecken Sie die nötigen Zugangsparameter. Ohne sie liefe der Code nicht einmal.

Und vor allem können Sie das Zahlungs-Gateway jetzt mocken, sodass Ihnen nicht bei jedem Testlauf 100 $ belastet werden.

Globaler Zustand erlaubt es Objekten, heimlich auf Abhängigkeiten zuzugreifen, die in ihren APIs nicht deklariert sind, und macht Ihre APIs damit zu pathologischen Lügnern.

Vielleicht haben Sie das bisher nicht so gesehen, aber immer wenn Sie globalen Zustand verwenden, schaffen Sie geheime drahtlose Kommunikationskanäle. Diese spukhafte Fernwirkung zwingt Entwickler dazu, jede Codezeile zu lesen, um mögliche Wechselwirkungen zu verstehen, senkt die Produktivität und verwirrt neue Teammitglieder. Wenn Sie derjenige sind, der den Code geschrieben hat, kennen Sie die wahren Abhängigkeiten, aber jeder, der nach Ihnen kommt, tappt im Dunkeln.

Schreiben Sie keinen Code, der sich auf globalen Zustand stützt; übergeben Sie Abhängigkeiten lieber ausdrücklich. Machen Sie sich Dependency Injection zu eigen.

Zerbrechlichkeit des globalen Zustands

In Code, der globalen Zustand und Singletons nutzt, können Sie nie sicher sein, wann und von wem der Zustand verändert wurde. Dieses Risiko zeigt sich schon bei der Initialisierung. Der folgende Code will eine Datenbankverbindung erzeugen und ein Zahlungs-Gateway initialisieren, wirft aber immer wieder Exceptions, und die Ursache zu finden ist äußerst mühsam:

PaymentGateway::init();
DB::init('mysql:', 'user', 'password');

Sie müssen den Code sorgfältig verfolgen, um herauszufinden, dass das Objekt PaymentGateway drahtlos auf andere Objekte zugreift, von denen einige eine Datenbankverbindung brauchen. Die Datenbank muss also vor PaymentGateway initialisiert werden. Die Nebelwand des globalen Zustands verbirgt das aber vor Ihnen. Wie viel Zeit ließe sich sparen, wenn die APIs dieser Klassen ehrlich wären und ihre Abhängigkeiten deklarierten?

$db = new DB('mysql:', 'user', 'password');
$gateway = new PaymentGateway($db, /* ... */);

Ein ähnliches Problem entsteht beim globalen Zugriff auf eine Datenbankverbindung:

use Illuminate\Support\Facades\DB;

class Article
{
	public function save(): void
	{
		DB::insert(/* ... */);
	}
}

Beim Aufruf der Methode save() ist unklar, ob eine Datenbankverbindung aufgebaut wurde und wer dafür zuständig ist. Müssen wir die Datenbankverbindung dynamisch ändern (etwa für Tests), greifen wir womöglich zu Methoden wie DB::reconnect(...) oder DB::reconnectForTest().

Betrachten wir ein Beispiel:

$article = new Article;
// ...
DB::reconnectForTest();
Foo::doSomething();
$article->save();

Wie können wir sicher sein, dass beim Aufruf von $article->save() tatsächlich die Testdatenbank verwendet wird? Was, wenn die Methode Foo::doSomething() die globale Datenbankverbindung geändert hat? Um das festzustellen, müssten wir den Quellcode von Foo und womöglich vieler weiterer Klassen prüfen. Diese Untersuchung gäbe uns nur eine vorübergehende Antwort, denn die Lage könnte sich später ändern.

Was, wenn wir die Datenbankverbindung in eine statische Variable innerhalb der Klasse Article verschieben?

class Article
{
	private static DB $db;

	public static function setDb(DB $db): void
	{
		self::$db = $db;
	}

	public function save(): void
	{
		self::$db->insert(/* ... */);
	}
}

Das ändert überhaupt nichts. Das Problem ist der globale Zustand selbst, ganz gleich, in welcher Klasse er versteckt ist. In diesem Szenario haben wir wie im vorigen beim Aufruf von $article->save() keine Gewissheit, in welche Datenbank die Daten geschrieben werden. Jeder könnte irgendwo in der Anwendung jederzeit über Article::setDb() die Datenbank geändert haben. Ohne dass wir davon wissen.

Der globale Zustand macht unsere Anwendung äußerst zerbrechlich.

Es gibt jedoch einen einfachen Weg, mit diesem Problem umzugehen. Lassen Sie die API einfach die Abhängigkeiten deklarieren, die für die einwandfreie Funktion nötig sind.

class Article
{
	public function __construct(
		private DB $db,
	) {
	}

	public function save(): void
	{
		$this->db->insert(/* ... */);
	}
}

$article = new Article($db);
// ...
Foo::doSomething();
$article->save();

Dieser Ansatz nimmt die Sorge vor versteckten oder unerwarteten Änderungen der Datenbankverbindung. Wir wissen jetzt sicher, wohin der Artikel gespeichert wird, und Änderungen in unbeteiligten Klassen können daran nichts mehr ändern. Der Code ist nicht mehr zerbrechlich, sondern stabil.

Schreiben Sie keinen Code, der sich auf globalen Zustand stützt; übergeben Sie Abhängigkeiten lieber ausdrücklich. Machen Sie sich Dependency Injection zu eigen.

Singleton

Singleton ist ein Entwurfsmuster, das nach der Definition aus der berühmten Veröffentlichung der Gang of Four eine Klasse auf eine einzige Instanz beschränkt und globalen Zugriff darauf bietet. Die Umsetzung dieses Musters sieht üblicherweise so aus:

class Singleton
{
	private static self $instance;

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

	// und weitere Methoden, die die Aufgaben der Klasse erfüllen
}

Leider führt das Singleton globalen Zustand in die Anwendung ein. Und wie wir oben gezeigt haben, ist globaler Zustand unerwünscht. Deshalb gilt das Singleton als Antipattern.

Verwenden Sie in Ihrem Code keine Singletons und ersetzen Sie sie durch andere Mechanismen. Sie brauchen Singletons wirklich nicht. Wenn Sie aber sicherstellen müssen, dass es in der gesamten Anwendung nur eine einzige Instanz einer Klasse gibt, überlassen Sie diese Verantwortung dem DI-Container. So entsteht ein Singleton im Gültigkeitsbereich der Anwendung, das üblicherweise Service genannt wird. Die Klasse selbst ist damit davon befreit, ihre Einzigartigkeit zu verwalten (sie hat also weder eine Methode getInstance() noch eine statische Property für die Instanz) und kann sich ganz auf ihre Aufgaben konzentrieren. Damit verstößt sie nicht länger gegen das Prinzip der einzigen Verantwortung.

Globaler Zustand und Tests

Beim Schreiben von Tests gehen wir im Idealfall davon aus, dass jeder Test eine isolierte Einheit ist, in die kein Zustand von außen hinein- und aus der keiner hinausgelangt. Nachdem ein Test durchgelaufen ist, sollte jeder mit ihm verbundene Zustand automatisch vom Garbage Collector aufgeräumt werden. Das macht die Tests isoliert. Deshalb können wir sie in beliebiger Reihenfolge ausführen.

Sobald aber globaler Zustand oder Singletons im Spiel sind, zerfallen diese nützlichen Annahmen. Zustand kann in Tests hinein- und aus ihnen hinausdringen. Plötzlich kann die Reihenfolge der Tests eine Rolle spielen.

Um Code mit Singletons überhaupt testen zu können, müssen Entwickler oft Abstriche an deren Unversehrtheit machen und zum Beispiel erlauben, die Instanz des Singletons zu ersetzen. Solche Lösungen sind bestenfalls Hacks, die zu Code führen, der schwer zu warten und zu verstehen ist. Jeder Test (oder seine Methode tearDown()), der globalen Zustand verändert, muss diese Änderungen sorgfältig zurücknehmen.

Globaler Zustand ist das größte Kopfzerbrechen beim Unit-Testing!

Wie bringt man das in Ordnung? Ganz einfach. Schreiben Sie keinen Code, der Singletons verwendet; übergeben Sie Abhängigkeiten lieber ausdrücklich. Machen Sie sich Dependency Injection zu eigen.

Globale Konstanten

Globaler Zustand beschränkt sich nicht auf Singletons und statische Variablen, er kann auch globale Konstanten betreffen.

Konstanten, deren Werte universelle Wahrheiten darstellen (M_PI) oder in sich abgeschlossene Informationen liefern (PREG_BACKTRACK_LIMIT_ERROR), sind in der Regel unbedenklich. Umgekehrt sind Konstanten, die als Weg dienen, Informationen drahtlos in den Code zu schleusen, faktisch versteckte Abhängigkeiten. So wie LOG_FILE im folgenden Beispiel. Die Verwendung der Konstanten FILE_APPEND ist dagegen völlig richtig.

const LOG_FILE = '...';

class Foo
{
	public function doSomething()
	{
		// ...
		file_put_contents(LOG_FILE, $message . "\n", FILE_APPEND);
		// ...
	}
}

Stattdessen sollten wir den Pfad zur Logdatei als Parameter im Konstruktor der Klasse Foo deklarieren und ihn damit zu einem ausdrücklichen Teil ihrer API machen:

class Foo
{
	public function __construct(
		private string $logFile,
	) {
	}

	public function doSomething()
	{
		// ...
		file_put_contents($this->logFile, $message . "\n", FILE_APPEND);
		// ...
	}
}

Jetzt übergeben wir den Pfad zur Logdatei ausdrücklich. Wir können ihn nach Bedarf leicht ändern, was das Testen und die Wartung des Codes vereinfacht.

Globale Funktionen und statische Methoden

Wir möchten betonen, dass die Verwendung statischer Methoden und globaler Funktionen an sich kein Problem ist. Wir haben die Schwierigkeiten mit Methoden wie DB::insert() erklärt, aber das eigentliche Problem war immer der dahinterliegende globale Zustand, üblicherweise in einer statischen Variablen. Die Methode DB::insert() verlässt sich auf eine statische Variable, die die Datenbankverbindung hält. Ohne diese Variable ließe sich die Methode gar nicht umsetzen.

Deterministische statische Methoden und Funktionen wie Closure::fromCallable(), strlen() und viele andere zu verwenden, ist mit Dependency Injection vollkommen vereinbar. Diese Funktionen sind vorhersehbar, weil sie für dieselben Eingabeparameter immer dasselbe Ergebnis liefern. Sie verwenden keinerlei globalen Zustand.

In PHP gibt es allerdings Funktionen, die nicht deterministisch sind. Dazu zählt zum Beispiel die Funktion htmlspecialchars(). Ihr dritter Parameter $encoding nimmt, wenn er weggelassen wird, standardmäßig den Wert der Konfigurationsoption default_charset an (ini_get('default_charset')). Es empfiehlt sich deshalb, diesen Parameter immer anzugeben, um mögliches unvorhersehbares Verhalten zu vermeiden. Nette macht das durchgängig.

Manche Funktionen wie strtolower() und strtoupper() verhielten sich in der jüngeren Vergangenheit nicht deterministisch, sondern abhängig von der Einstellung des Locale (setlocale()). Das führte zu vielen Komplikationen, am häufigsten bei der Arbeit mit der türkischen Sprache. Das Türkische unterscheidet nämlich sowohl in Klein- als auch in Großbuchstaben zwischen dem ‚I‘ mit und ohne Punkt. Folglich gab strtolower('I') den Wert ı zurück (kleines i ohne Punkt) und strtoupper('i') den Wert İ (großes I mit Punkt), was zu zahlreichen rätselhaften Fehlern in Anwendungen führte. Dieses Problem wurde in PHP 8.2 behoben, die Funktionen hängen nicht mehr vom Locale ab.

Das ist ein gutes Beispiel dafür, wie globaler Zustand (die Einstellung des Locale) Tausende Entwickler weltweit geplagt hat. Die endgültige Lösung bestand darin, die Funktionen unabhängig vom Locale zu machen und damit die versteckte Abhängigkeit zu entfernen.

Wann lässt sich globaler Zustand verwenden?

Es gibt bestimmte, eng begrenzte Situationen, in denen die Verwendung von globalem Zustand vertretbar ist. Zum Beispiel beim Debuggen, wenn Sie den Wert einer Variablen ausgeben oder die Laufzeit eines bestimmten Codeabschnitts messen müssen. In diesen Fällen, in denen es um vorübergehende Eingriffe geht, die später wieder aus dem Code verschwinden, kann ein global zugänglicher Dumper oder Timer legitim sein. Diese Werkzeuge sind nicht Teil des eigentlichen Entwurfs der Anwendung.

Ein weiteres Beispiel sind die Funktionen von PHP für reguläre Ausdrücke (preg_*), die kompilierte reguläre Ausdrücke intern in einem statischen Speicher zwischenspeichern. Wenn Sie die Funktionen an mehreren Stellen Ihres Codes mit demselben regulären Ausdruck aufrufen, wird der Ausdruck nur einmal kompiliert. Dieses Caching verbessert die Leistung und ist für den Nutzer vollständig unsichtbar, weshalb diese Verwendung von internem statischem Zustand allgemein akzeptabel ist.

Zusammenfassung

Wir haben besprochen, warum es sinnvoll ist:

  1. alle veränderlichen statischen Properties (globalen Zustand) aus Ihrem Code zu entfernen
  2. Abhängigkeiten ausdrücklich zu deklarieren
  3. und Dependency Injection zu nutzen

Denken Sie beim Entwurf Ihres Codes daran, dass jedes veränderliche static $foo eine mögliche Quelle von Problemen ist. Um eine DI-freundliche Umgebung zu schaffen, ist es entscheidend, globalen Zustand vollständig zu beseitigen und durch Dependency Injection zu ersetzen.

Dabei stellen Sie vielleicht fest, dass Sie Klassen mit mehreren Verantwortlichkeiten aufteilen müssen. Zögern Sie nicht; streben Sie das Single Responsibility Principle an.

Ich möchte Miško Hevery danken, dessen Artikel wie Flaw: Brittle Global State & Singletons die Grundlage dieses Kapitels bilden.