Globale Bedingungen. Dieser Satz verursacht Angst und Schmerz im Herzen jedes Entwicklers, der das Unglück hatte, sich diesem Phänomen zu stellen. Sind Sie bereits auf unerwartetes Anwendungsverhalten gestoßen, ohne die Gründe dafür zu verstehen, wie ein unglücklicher Ritter, der versucht, Hydra mit vielen Köpfen zu töten? Sie befinden sich in einem endlosen Zyklus von Versuch und Irrtum. 90% der Zeit fragen Sie sich, was passiert?
All dies kann ärgerliche Folgen von Globals sein: versteckte Variablen, die ihren Status an unbekannten Orten ändern, aus Gründen, die Sie noch nicht herausgefunden haben.
Wandern Sie gerne im Dunkeln, während Sie versuchen, die Anwendung zu ändern? Natürlich mag ich es nicht. Zum Glück habe ich Kerzen für Sie:
- Zunächst werde ich beschreiben, was wir am häufigsten als globale Staaten bezeichnen. Dieser Begriff wird nicht immer genau verwendet und muss daher geklärt werden.
- Als nächstes werden wir herausfinden, warum Globals für unsere Codebasis schädlich sind.
- Dann werde ich erklären, wie man den Umfang der Globals trimmt, um sie in lokale Variablen umzuwandeln.
- Abschließend werde ich über die Kapselung sprechen und warum der Kreuzzug gegen globale Variablen nur ein Teil des großen Problems ist.
Ich hoffe, dieser Artikel erklärt alles, was Sie über globale Staaten wissen müssen. Wenn du denkst, dass ich viel vermisst habe und du mich dafür hasst und mich nicht mehr sehen willst, schreibe darüber in den Kommentaren. Es wird mir, meinen Lesern und allen, die plötzlich auf dieser Seite erschienen sind, angenehm sein.
Bist du bereit, lieber Leser, auf ein Pferd zu springen und deinen Feind zu kennen? Finde diese Globalen und lass sie den Stahl unserer Schwerter schmecken!
Was ist eine Bedingung?

Beginnen wir mit den Grundlagen, damit Sie Entwickler sich verstehen.
Ein Zustand ist eine Definition eines Systems oder einer Entität. Zustände sind im wirklichen Leben zu finden:
- Wenn der Computer ausgeschaltet ist, wird sein Status ausgeschaltet.
- Wenn eine Tasse Tee heiß ist, ist ihr Zustand heiß.
In der Softwareentwicklung können einige Konstrukte (z. B. Variablen) Zustände haben. Angenommen, die Zeichenfolge "Hallo" oder die Zahl 11 werden nicht als Zustände betrachtet, sondern als Werte. Sie werden zum Status, wenn sie an eine Variable angehängt und im Speicher abgelegt werden.
<?php echo "hello";
Es können zwei Arten von Zuständen unterschieden werden:
Variable Zustände: Nach ihrer Initialisierung können sie sich jederzeit während der Ausführung Ihrer Anwendung ändern.
<?php $lala = "hello";
Unveränderliche Zustände: können sich während der Ausführung nicht ändern. Sie weisen Ihrer Variablen den ersten Status zu, dessen Wert sich anschließend nicht ändert. "Konstanten" im Alltag werden Beispiele für unveränderliche Zustände genannt:
<?php define("GREETING", "hello");
Hören wir uns jetzt ein hypothetisches Gespräch zwischen Denis und Vasily an, Ihren Mitentwicklern:
- Dan! Sie haben überall globale Variablen erstellt! Sie können nicht geändert werden, ohne dass alles kaputt geht! Ich werde dich töten!
- Nifiga, Vasek! Mein globales Schicksal ist großartig! Ich lege meine Seele in sie, das sind Meisterwerke! Ich verehre meine Globalen!
Am häufigsten nennen Entwickler globale Zustände, globale Variablen oder globale Zustände, was sie als globale veränderbare Zustände bezeichnen sollten. Das heißt, Zustände, die in dem größten der Ihnen zur Verfügung stehenden
Bereiche geändert werden können: in der gesamten Anwendung.
Wenn eine Variable nicht die gesamte Anwendung als Gültigkeitsbereich hat, handelt es sich um lokale Variablen oder Gebietsschemas. Sie existieren in bestimmten Sichtbarkeitsbereichen, weniger als im Bereich der gesamten Anwendung.
<?php namespace App\Ecommerce; $global = "I'm a mutable global variable!";
Sie könnten denken: Wie bequem ist es, Variablen zu haben, auf die Sie von überall aus zugreifen und sie ändern können! Ich kann Zustände von einem Teil der Anwendung auf einen anderen übertragen! Sie müssen sie nicht durch Funktionen führen und so viel Code schreiben! Sei gegrüßt, global veränderlicher Staat!
Wenn Sie das wirklich gedacht haben, empfehle ich dringend, weiterzulesen.
Globale Staaten schlimmer als Pest und Cholera?
Das größte Linkdiagramm
Fakt: Es ist für Sie einfacher, ein genaues Anwendungsverbindungsdiagramm zu erstellen, wenn es nur Gebietsschemas mit kleinen und definierten Bereichen ohne globale Elemente enthält.
Warum?
Angenommen, Sie haben eine große Anwendung mit globalen Variablen. Jedes Mal, wenn Sie etwas ändern müssen, müssen Sie:
- Denken Sie daran, dass diese veränderlichen globalen Zustände existieren.
- Schätzen Sie, ob sie sich auf den Umfang auswirken, den Sie ändern werden.
Normalerweise müssen Sie nicht über lokale Variablen nachdenken, die sich in anderen Bereichen befinden. Was auch immer Sie tun, Sie müssen in Ihrem müden Gehirn immer einen Ort für globale veränderbare Zustände behalten, da diese alle Bereiche betreffen können.
Darüber hinaus können sich Ihre globalen veränderlichen Zustände überall in der Anwendung ändern. Man muss sich normalerweise fragen, wie ihr aktueller Zustand ist. Dies bedeutet, dass Sie gezwungen sind, in der gesamten Anwendung zu suchen und zu versuchen, die Werte der Globals im veränderbaren Bereich zu berechnen.
Das ist noch nicht alles. Wenn Sie den Status der Globals ändern müssen, können Sie sich nicht vorstellen, welchen Umfang dies hat. Wird dies zu unerwartetem Verhalten einer anderen Klasse, Methode oder Funktion führen? Erfolg bei der Suche.
Kurz gesagt, Sie kombinieren alle Klassen, Methoden und Funktionen, die denselben globalen Status verwenden. Vergessen Sie nicht:
Abhängigkeiten erhöhen die Komplexität erheblich . Erschreckt es dich? Es sollte sein. Kleine spezifische Sichtbarkeitsbereiche sind sehr nützlich: Sie müssen nicht die gesamte Anwendung im Auge behalten, es reicht aus, sich nur an die Bereiche zu erinnern, mit denen Sie arbeiten.
Menschen verfolgen nicht sofort eine große Menge an Informationen. Wenn wir dies versuchen, erschöpfen wir schnell das Angebot an kognitiven Fähigkeiten, es wird schwierig für uns, uns zu konzentrieren, und wir beginnen, Fehler und dumme Dinge zu schaffen. Aus diesem Grund ist es so unangenehm, im globalen Bereich Ihrer Anwendung zu agieren.
Globale Namenskollisionen
Es gibt Schwierigkeiten bei der Verwendung von Bibliotheken von Drittanbietern. Stellen Sie sich vor, Sie möchten diese supercoole Bibliothek verwenden, die jedes Zeichen zufällig mit einem Flickereffekt färbt. Der Traum eines jeden Entwicklers! Wenn diese Bibliothek auch Globals verwendet, die dieselben Namen wie Ihre eigenen haben, werden Sie Kollisionen von Namen genießen. Ihre Anwendung wird abstürzen und Sie werden die Gründe wahrscheinlich für eine lange Zeit erraten:
- Zunächst müssen Sie herausfinden, dass Ihre Bibliothek globale Variablen verwendet.
- Zweitens müssen Sie berechnen, welche Variable während der Ausführung verwendet wurde - Ihre oder Bibliotheken? Es ist nicht so einfach, die Namen sind die gleichen!
- Drittens müssen Sie Ihre globalen veränderlichen Variablen umbenennen, da Sie die Bibliothek nicht selbst ändern können. Wenn Sie während der gesamten Anwendung verwendet werden, werden Sie weinen.
In jedem Stadium werden Sie Ihre Haare vor Wut und Verzweiflung reißen. Bald brauchen Sie keinen Kamm mehr. Dieses Szenario wird Sie wahrscheinlich nicht verführen. Vielleicht wird sich jemand daran erinnern, dass die JavaScript-Bibliotheken Mootools, Underscore und jQuery immer miteinander kollidierten, wenn sie nicht in kleineren Bereichen platziert wurden. Oh, und das berühmte globale
$
-Objekt in jQuery!
Testen wird zum Albtraum
Wenn ich Sie noch nicht überzeugt habe, betrachten wir die Situation unter dem Gesichtspunkt des Unit-Tests: Wie schreiben Sie Tests bei Vorhandensein globaler Variablen? Da Tests die globalen Werte ändern können, wissen Sie nicht, in welchem Test sich der Status befand. Sie müssen die Tests voneinander isolieren, und globale Zustände binden sie zusammen.
Haben Sie es jemals gehabt, damit Isolationstests gut funktionieren, und wenn Sie das gesamte Paket ausführen, werden sie fehlschlagen? Nein? Und ich hatte es. Jedes Mal, wenn ich mich daran erinnere, leide ich.
Parallelitätsprobleme
Variable globale Zustände können viele Probleme verursachen, wenn Sie Parallelität benötigen. Wenn Sie den Status von Globals in mehreren Hinrichtungsfäden ändern, dann gehen Sie Hals über Kopf in einem mächtigen
Zustand des Rennens .
Wenn Sie ein PHP-Entwickler sind, stört Sie dies nicht, es sei denn, Sie verwenden Bibliotheken, mit denen Sie Parallelität erstellen können. Wenn Sie jedoch
eine neue Sprache lernen, in der Parallelität leicht zu implementieren ist, hoffe ich, dass Sie sich an meine Prosa erinnern.
Vermeidung global veränderlicher Zustände

Während globale veränderbare Zustände eine Menge Probleme verursachen können, sind sie manchmal schwer zu vermeiden.
Nehmen Sie die REST-API: Endpunkte empfangen HTTP-Anforderungen mit Parametern und senden Antworten. Diese an den Server gesendeten HTTP-Parameter können auf vielen Ebenen Ihrer Anwendung angefordert werden. Es ist sehr verlockend, diese Parameter beim Empfang einer HTTP-Anfrage global zu machen und sie vor dem Senden einer Antwort zu ändern. Fügen Sie zu jeder Anforderung Parallelität hinzu, und das Katastrophenrezept ist fertig.
Globale veränderbare Zustände können auch direkt in Sprachimplementierungen unterstützt werden. Zum Beispiel gibt es in PHP
Superglobale .
Wenn Globale von irgendwoher kommen, wie soll man dann mit ihnen umgehen? Wie kann man die Anwendung von Denis umgestalten, Ihrem Entwicklerkollegen, der, wo immer möglich, Globals erstellt hat, weil er in den letzten 20 Jahren nichts über die Entwicklung gelesen hat?
Funktionsargumente
Der einfachste Weg, Globale zu vermeiden, besteht darin, Variablen mithilfe von Funktionsargumenten zu übergeben. Nehmen Sie ein einfaches Beispiel:
<?php namespace App; use Router\HttpRequest; use App\Product\ProductData; use App\Exceptions; class ProductController { public function createAction(HttpRequest $httpReq) { $productData = $httpReq->get("productData"); if (!$this->productModel->validateProduct($productData)) { return ValidationException(sprintf("The product %d is not valid", $productData["id"])); } $product = $this->productModel->createProduct($productData); } } class Product { public function createProduct(array $productData): Product { $productData["name"] = "SuperProduct".$productData["name"];
Wie Sie sehen können,
$productData
das
$productData
vom Controller über eine HTTP-Anforderung verschiedene Ebenen:
- Der Controller hat eine HTTP-Anfrage erhalten.
- An das Modell übergebene Parameter.
- Parameter an das DAO übergeben .
- Parameter werden in der Anwendungsdatenbank gespeichert.
Wir könnten dieses Parameterarray global machen, wenn wir es von der HTTP-Anforderung abrufen. Es scheint so einfach zu sein: Es müssen keine Daten an 4 verschiedene Funktionen übertragen werden. Übergeben von Parametern als Argumente an Funktionen:
- Es wird offensichtlich zeigen, dass diese Funktionen das
$productData
.
- Es wird offensichtlich gezeigt, welche Funktionen welche Parameter verwenden. Es ist ersichtlich, dass für
ProductDao::find
aus dem $productData
nur $id
benötigt wird und nicht alles.
Globale machen den Code weniger verständlich und verknüpfen Methoden miteinander, was ein sehr hoher Preis für das fast vollständige Fehlen von Vorteilen ist.
Sie hören bereits Denis Protest: „Und wenn eine Funktion drei oder mehr Argumente hat? Wenn Sie noch mehr hinzufügen müssen, erhöht sich die Komplexität der Funktion! Und was ist mit den Variablen, Objekten und anderen Konstrukten, die überall benötigt werden? Übergeben Sie sie an jede Funktion in der Anwendung? “
Fragen sind fair, lieber Leser. Als
guter Entwickler sollten Sie Denis anhand Ihrer Kommunikationsfähigkeiten Folgendes erklären:
„Denis, wenn Ihre Funktionen zu viele Argumente haben, können die Funktionen selbst ein Problem sein. Sie tun wahrscheinlich zu viel, sind für zu viele Dinge verantwortlich. Du hast nicht daran gedacht, sie in kleinere Funktionen zu unterteilen? " .
Sie fühlen sich wie ein Redner auf der Akropolis von Athen und fahren fort:
„Wenn Sie Variablen in vielen Bereichen der Sichtbarkeit benötigen, ist dies ein Problem, und wir werden bald darüber sprechen. Aber wenn Sie sie wirklich brauchen, was ist dann falsch daran, sie durch Funktionsargumente zu führen? Ja, Sie müssen sie auf der Tastatur eingeben, aber wir sind Entwickler. Es ist unsere Aufgabe, Code zu schreiben. “Es mag komplizierter erscheinen, wenn Sie mehr Argumente haben (vielleicht ist dies so), aber ich wiederhole, die Vorteile überwiegen die Nachteile: Es ist besser, wenn der Code so klar wie möglich ist und keine versteckten globalen veränderlichen Zustände verwendet.
Kontextobjekte
Kontextobjekte sind solche, die Daten enthalten, die durch einen bestimmten Kontext definiert sind. In der Regel werden diese Daten als Schlüsselpaarkonstrukt gespeichert, z. B. als assoziatives Array in PHP. Ein solches Objekt hat kein Verhalten, nur Daten, ähnlich
einem Wertobjekt .
Ein Kontextobjekt kann jeden globalen veränderlichen Zustand ersetzen. Kehren Sie zum vorherigen Codebeispiel zurück. Anstatt Daten aus der Anforderung durch die Ebenen zu leiten, können wir ein Objekt verwenden, das diese Daten kapselt.
Der Kontext ist die Abfrage selbst: eine andere Abfrage - ein anderer Kontext - ein anderer Datensatz. Anschließend wird das Kontextobjekt an eine Methode übergeben, die diese Daten benötigt.
Sie sagen: "Es ist großartig und das alles, aber was gibt es?"
- Daten werden in ein Objekt eingekapselt. In den meisten Fällen besteht Ihre Aufgabe darin, die Daten unveränderlich zu machen, dh, dass Sie den Status - den Wert der Daten im Objekt nach der Initialisierung - nicht ändern können.
- Offensichtlich benötigt der Kontext die Daten des Kontextobjekts, da sie an alle Funktionen (oder Methoden) übertragen werden, die diese Daten benötigen.
- Dies löst das Parallelitätsproblem: Wenn jede Anforderung ein eigenes Kontextobjekt hat, können Sie diese sicher in ihre eigenen Ausführungsthreads schreiben oder lesen.
Aber alles in der Entwicklung hat einen Preis. Kontextobjekte können schädlich sein:
- Wenn Sie sich die Argumente der Funktion ansehen, wissen Sie nicht, welche Daten sich im Kontextobjekt befinden.
- Sie können alles in ein Kontextobjekt einfügen. Achten Sie darauf, nicht zu viel zu speichern, z. B. die gesamte Benutzersitzung oder sogar die meisten Ihrer Anwendungsdaten. Und dann kann dies passieren:
$context->getSession()->getUser()->getProfil()->getUsername()
. Brechen Sie das Gesetz von Demeter , und Ihr Fluch wird wahnsinnige Komplexität sein.
- Je größer das Kontextobjekt ist, desto schwieriger ist es herauszufinden, welche Daten und in welchem Umfang sie verwendet werden.
Im Allgemeinen würde ich die Verwendung von Kontextobjekten so weit wie möglich vermeiden. Sie können viele Zweifel hervorrufen. Die Unveränderlichkeit von Daten ist ein großes Plus, aber wir dürfen die Mängel nicht vergessen. Wenn Sie ein Kontextobjekt verwenden, stellen Sie sicher, dass es klein genug ist, und übergeben Sie es an einen kleinen und sorgfältig definierten Bereich.
Wenn Sie vor dem Ausführen eines Programms keine Ahnung haben, wie viele Status an Ihre Funktionen übergeben werden (z. B. Parameter aus einer HTTP-Anforderung), können Kontextobjekte hilfreich sein. Einige von ihnen verwenden sie daher. Denken Sie beispielsweise an das
Request
Objekt in Symfony.
Abhängigkeitsinjektion
Eine weitere gute Alternative zu globalen veränderlichen Zuständen besteht darin, die benötigten Daten direkt beim Erstellen direkt in das Objekt einzubetten. Dies ist die Definition der Abhängigkeitsinjektion: Eine Reihe von Techniken zum Einbetten von Objekten in Ihre Komponenten (Klassen).
Warum genau Abhängigkeitsinjektion?
Ziel ist es, die Verwendung Ihrer Variablen, Objekte oder anderer Konstrukte einzuschränken und sie in einem begrenzten Bereich zu platzieren. Wenn Sie Abhängigkeiten haben, die eingebettet sind und daher nur im Rahmen eines Objekts agieren können, können Sie leichter herausfinden, in welchem Kontext sie verwendet werden und warum. Keine Qual und Qual!
Die Abhängigkeitsinjektion unterteilt den Anwendungslebenszyklus in zwei wichtige Phasen:
- Anwendungsobjekte erstellen und deren Abhängigkeiten implementieren.
- Verwenden Sie Objekte, um Ihre Ziele zu erreichen.
Dieser Ansatz macht den Code klarer: Sie müssen nicht alles an zufälligen Orten instanziieren oder, noch schlimmer, überall globale Objekte verwenden.
Viele Frameworks verwenden die Abhängigkeitsinjektion, manchmal in relativ komplexen Schemata, mit Konfigurationsdateien und einem Dependency Injection Container (DIC). Es ist jedoch keineswegs notwendig, die Dinge zu komplizieren. Sie können einfach Abhängigkeiten auf einer Ebene erstellen und auf einer niedrigeren Ebene implementieren. In der Go-Welt kenne ich beispielsweise niemanden, der DIC verwenden würde. Sie erstellen einfach die Abhängigkeiten in der Hauptdatei mit dem Code (main.go) und übertragen sie dann auf die nächste Ebene. Sie können auch alles in verschiedenen Paketen instanziieren, um klar anzuzeigen, dass die „Phase der Abhängigkeitsinjektion“ nur auf dieser bestimmten Ebene durchgeführt werden sollte. In Go kann der Umfang der Pakete die Dinge einfacher machen als in PHP, in dem DICs in jedem mir bekannten Framework, einschließlich Symfony und Laravel, weit verbreitet sind.
Implementierung über Konstruktor oder Setter
Es gibt zwei Möglichkeiten, Abhängigkeiten einzufügen: über den Konstruktor oder die Setter. Ich rate, wenn möglich, sich an die erste Methode zu halten:
- Wenn Sie wissen möchten, was Klassenabhängigkeiten sind, müssen Sie nur einen Konstruktor finden. Sie müssen nicht nach Methoden suchen, die über die gesamte Klasse verteilt sind.
- Durch das Festlegen von Abhängigkeiten während der Installation können Sie sicher sein, dass das Objekt sicher verwendet wird.
Lassen Sie uns ein wenig über den letzten Punkt sprechen: Dies wird als "Erzwingen der Invariante" bezeichnet. Indem Sie eine Instanz eines Objekts erstellen und seine Abhängigkeiten implementieren, wissen Sie, dass es unabhängig von den Anforderungen Ihres Objekts korrekt konfiguriert ist. Und wenn Sie Setter verwenden, woher wissen Sie, dass Ihre Abhängigkeiten zum Zeitpunkt der Verwendung des Objekts bereits festgelegt sind? Sie können auf den Stapel gehen und versuchen herauszufinden, ob die Setter aufgerufen wurden, aber ich bin sicher, dass Sie dies nicht tun möchten.
Kapselungsverletzung
Schließlich besteht der einzige Unterschied zwischen lokalen und globalen Staaten in ihrem Umfang. Sie sind für lokale Staaten begrenzt, und für globale Staaten ist die gesamte Anwendung verfügbar. Wenn Sie jedoch lokale Zustände verwenden, können Probleme auftreten, die für globale Zustände spezifisch sind. Warum?
Hast du Kapselung gesagt?
Die Verwendung globaler Zustände unterbricht möglicherweise die Kapselung, genauso wie Sie sie mit lokalen Zuständen aufheben können.
Beginnen wir von vorne. Was sagt uns
Wikipedia über die Definition der Kapselung? Der Sprachmechanismus zum Einschränken des direkten Zugriffs auf einige Komponenten eines Objekts. Zugangsbeschränkung? Warum?
Nun, wie wir oben gesehen haben, ist es viel einfacher, im lokalen Bereich zu argumentieren als im globalen. Globale veränderbare Zustände sind per Definition überall verfügbar, und dies ist gegen die Einkapselung! Keine Zugriffsbeschränkungen für Sie.
Wachsender Umfang und Zustandsleckage

Stellen wir uns einen Staat in seinem eigenen kleinen Rahmen vor. Leider wächst die Anwendung im Laufe der Zeit und dieser lokale Status wird als Argument an die Funktion in der gesamten Anwendung übergeben. Jetzt wird Ihr Gebietsschema in vielen Bereichen verwendet, und in allen ist der direkte Zugriff auf das Gebietsschema autorisiert. Jetzt fällt es Ihnen schwer, den genauen Status des Gebietsschemas zu berechnen, ohne alle Sichtbarkeitsbereiche zu untersuchen, in denen es vorhanden ist und in denen es geändert werden kann. All dies haben wir bereits bei global veränderlichen Staaten gesehen.
Nehmen Sie ein Beispiel: Das
anämische Domänenmodell kann den Umfang Ihrer veränderlichen Modelle erhöhen. Tatsächlich unterteilt das anämische Domänenmodell die Daten und das Verhalten Ihrer Domänenobjekte in zwei Gruppen: Modelle (nur Objekte mit Daten) und Dienste (nur Objekte mit Verhalten). Meistens werden diese Modelle in allen Diensten verwendet. Daher ist es wahrscheinlich, dass irgendeine Art von Modell den Umfang ständig vergrößert. Sie werden nicht verstehen, welches Modell in welchem Kontext verwendet wird, ihr Status wird sich ändern und dieselben Probleme werden auf Sie fallen.
Ich möchte eine wichtige Idee vermitteln: Wenn Sie globale veränderbare Zustände vermeiden, bedeutet dies nicht, dass Sie sich entspannen, einen Cocktail in einer Hand halten und mit der anderen die Knöpfe drücken können, um das Leben und Ihren legendären Code zu genießen. -,
, -, , .
, . , .
? - . , , -.
, - , , . , , — . , : , . .
.
.
Product
, , :
class Product { public function createProduct(array $productData): Product { $productData["name"] = "SuperProduct".$productData["name"];
$productData
. , , , .
, . , - ? , . .
, , . .
:
class Product { public function createProduct(array $productData): Product {
, ,
$productData
. , .
$productData
, , HTTP-.
, : «, ».
?

. , .
?
. , .
,
ShipmentDelay
, , , . , -,
ShipmentDelay
, , , . Ist es dumm ,
DRY .
, , . : , , . , , , . , , .
?
, (, ), , , . , , , , .
. :
. , — . , .
, , .
, , . , . , , . , !