Immutability mit PHP: Warum das so wichtig ist

So manch eine Entwicklung, die PHP als Programmiersprache genommen hat, kann gar nicht laut genug gefeiert werden. Immutability hat sich fast unbemerkt eingeschlichen. Warum wir sie feiern sollten, werde ich Euch in diesem Artikel erklären.

Was ist Immutability in PHP?

Ich selbst hatte das Thema lange nicht auf dem Schirm. Erst bei einem Besuch der PHP User Group in Frankfurt am Main, als Andreas Heigl einen Vortrag über die DateTimeImmutable Klasse und dessen Neuerungen in PHP 8 berichtete, wurde mir klar, wie wichtig dieses Thema ist.

Immutability ist die Unabänderbarkeit eines Zustands eines Objekts nach der Erstellung. Im Detail bedeutet dies:

  • Alle Eigenschaften werden einmalig gesetzt
  • Danach gibt es keine Möglichkeit die Werte dieser Eigenschaften zu ändern
  • Jede Änderung erzeugt ein neues Objekt, statt die Werte des bestehenden Objekts zu ändern

Immutability führt dazu, dass das Risiko von versteckten Seiteneffekten minimiert wird. Wenn wir ehrlich sind, kennen wir diesen Moment, in dem wir uns erstaunt fragen, wo diese Werte eigentlich herkommen, wenn wir mitten im Debugging stecken, doch alle. Mit Immutability können wir diese Situationen einfach vermeiden.

Mit Immutability können Objekte gefahrlos in verschiedenen Threads, Prozessen und Co-Routinen genutzt werden. Gerade dieser Punkt wird zukünftig immer wichtiger werden.

Wie zuvor schon erwähnt, wird das Debugging einfacher werden, weil das Objekt immer genau die Daten beinhaltet, welche zur Initialisierung des Objekts genutzt wurden. Dein Code wird also vorhersagbar.

Immutability ist ein wesentlicher Bestandteil von guter Software Architektur, wie z.B. in Domain Driven Design (DDD), Clean Architecture und anderen Software Paradigmen. Gut strukturierte Software berücksichtigt Immutability aus Prinzip.

Wie geht Immutability mit PHP?

PHP hat kein eingebautes immutable Keyword, wie es in anderen Programmiersprachen vorhanden ist. Seit PHP 8.1 kann das readonly Keyword für einzelne Klasseneigenschaften genutzt werden. Mit der Einführung der Constructor Property Promotion sieht das folgendermaßen aus.

class MyClass
{
    public function __construct(
        public readonly string $name,
        public readonly DateTimeImmutable $birthday,
    ) {}
}

Mit diesem Beispiel sind die Eigenschaften nach der Konstruktion unveränderbar. Wenn man trotzdem versucht einen anderen Wert für die Eigenschaften zu setzen, wird eine entsprechende Error Exception geworfen.

Mit PHP 8.2 wurde das readonly Keyword für Klassen eingeführt, so dass die Redundanz bei der Constructor Property Promotion reduziert werden konnte. Mit dem readonly Keyword für Klassen werde alle Eigenschaften einer Klasse als immutable deklariert, während man auf Constructor Property Promotion Ebene entscheiden kann, welche Eigenschaften mutable und welche immutable sind.

readony class MyClass
{
    public function __construct(
        public string $name,
        public DateTimeImmutable $birthday,
    ) {}
}

In diesem Beispiel ist die komplette Klasse als readonly deklariert. Alle ihre Eigenschaften sind unveränderbar.

Werte mit Immutability ändern?

Immutability erlaubt es per Definition nicht Werte nach Initialisierung eines Objekts zu ändern. Jede Änderung eines Wertes erzeugt ein neues Objekt. Während man zuvor noch mit Setter-Methoden arbeitete, um Objekte mit Werten zu füllen, arbeitet man im Kontext der Unveränderbarkeit mit with-Methoden, die immer ein neues Objekt erzeugen. Man spricht in diesem Zusammenhang auch auf Wither-Pattern.

// Mutable Objekte
$object->setValue('blubb');

// Immutable Objekte
$object->withValue('blubb');

Mit PHP 8.5 gibt es sogar die clone with Syntax, die es erlaubt Objekte mit anderen Werten zu clonen.

final readonly class Developer
{
    public function __construct(
        public string $name,
    ) {}

    public function withName(string $name): self
    {
        return clone($this, [
            'name' => $name,
        ]);
    }
}

$developerA = new Developer('Marcel');
$developerB = $developerA->withName('John Doe');

Gut, das Beispiel ist nicht perfekt. Es zeigt aber auf eine sehr einfache Art und Weise, wie das Wither-Pattern funktioniert. Es wird immer ein neues Objekt erzeugt. Dies sogar sehr performant, weil Du als Programmierer das Objekt nicht mit allen neuen Werten initialisieren musst.

Wo wird Immutability bereits eingesetzt?

Immutability ist, wie eingangs schon erwähnt, gar nicht mehr so neu. Schon jetzt nutzen einige von uns es unbemerkt fast täglich.

Value Objects

Value Objects sind ein sehr prominentes Beispiel. Ich selbst nutze sie immer dann, wenn es darum geht Werte aus HTTP Requests nach durchgeführter, bestandener Validierung zu speichern. Sie bieten Type-Safety und stehen immer für ein festes Dataset. Die zuvor gezeigte Klasse ist ein sehr gutes Beispiel hierfür.

final readonly class Developer
{
    public function __construct(
        public string $name,
        public DateTimeImmutable $birthday,
    ) {}
}

$developer = new Developer('Marcel', new DateTimeImmutable('1979-12-19'));
echo $developer->name; // Marcel
echo $developer->birthday->format('Y-m-d'); // 1979-12-19

$developer->name = 'John Doe'; // Error Exception

Value Objekte sind per Design immer immutable. Zumindest sollten sie es sein. Zum Beispiel ändern sich empfangene Werte aus einem Request nach erfolgreicher Validierung nicht mehr. Es werden keine Setter benötigt. Es gibt keine unvorhersehbaren Seiteneffekte. Value Objects werden durch Immutability einfach vorhersehbar.

Das PHP DateTimeImmutable Objekt

Das wohl prominenteste, oft ungesehene Beispiel existiert seit Jahren in PHP selbst. Das DateTimeImmutable Objekt. Mit der Einführung des DateTime Objekts wurde vieles einfacher. Gleichzeitig wurden aber auch neue Probleme geschaffen (die es zuvor bereits mit den unterschiedlichen datumsbezogenen Funktionen in PHP gab).

$birthday = new DateTime('1979-12-19');
$birthday->modify('+1 day');

Soweit sieht das harmlos aus. Bis es dann irgendwann folgende Methoden in irgendwelchen Klassen gibt.

public function hasBirthday(DateTimeInterface $birthday): bool
{
    return $this->birthday == $birthday;
}

Durch die Modifikation des DateTime Objekts irgendwo im Code wird der Rückgabewert der Methode verändert. Solche Fehler haben mich als Entwickler mehrfach Stunden gekostet. Ihr kennt das sicherlich.

Mit dem DateTimeImmutable Objekt gibt es diese Problematik einfach nicht mehr. Ganz im Sinne der Unveränderbarkeit liefert die modify Methode hier ein neues Objekt.

$birthday = new DateTimeImmutable('1979-12-19');
$anotherBirthday = $birthday->modify('+1 day');

echo $birthday->format('Y-m-d); // immernoch 1979-12-19
echo $anotherBirthday->format('Y-m-d'); // 1979-12-20

Wunderhübsch. Vorhersehbar. Sicher. Das DateTime Objekt hat sich bei mir persönlich fast vollständig aus der Architektur meiner Entwicklungen verabschiedet. Dies ist nur ein einfaches Beispiel. Man stelle sich nur den Impact vor, wenn es um den Ablauf eines Security Tokens während eines Requests geht.

PSR-7 HTTP Messages

Die PHP Framework Interoperability Group hat einen sehr sinnvollen Standard für HTTP Messaging definiert. Dieser Standard macht Gebrauch von dem Wither-Pattern. Mir persönlich ist das nie aufgefallen, weil ich es rein intuitiv genutzt habe.

$request = $request->withHeader('Content-Type', 'application/json');

Durch das Setzen eines neuen Headers, wird ein neues Request Objekt erzeugt. Warum? Requests repräsentieren einfach Fakten, wie z.B. das State eines Requests. Diese Fakten sollten sich während eines Requests niemals ändern. Mit dieser Entscheidung den Standard so zu beschreiben konnten z.B. viele Fehler im Zusammenhang mit dem State eines Requests in prominenten Middleware Pipelines vermieden werden.

Fazit

Natürlich ergibt Immutability nicht überall Sinn. Wenn die Konstruktion von Objekten besonders aufwendig ist, oder ein Objekt veränderbar sein muss, sollte man das Immutability Konzept nicht berücksichtigen. Immutability ist kein Allheilmittel. Dennoch kann die Architektur Deiner Software erheblich verbessert werden, wenn Du sie nutzt.

Immutability kann z.B. nicht in jedem Fall Lazy Objects genutzt werden. Die Erklärung dafür ist denkbar einfach. Unveränderbarkeit wird mit dem Konstruktur definiert. Lazy Ghosts können nur mit Objekten funktionieren, deren Zustand zu einem späteren Zeitpunkt definiert wird. Lazy Proxies hingegen funktionieren mit Immutability hervorragend.

Allerdings hat Immutability einen sehr großen Impact auf stabilen, gut strukturierten Code. Immutability wird auf jeden Fall die Architektur verbessern und weniger fehleranfällig machen.

Nutzt Du Immutability bereits in Deinen Entwicklungen? Lass es mich mit einem Kommentar wissen.

Kommentar verfassen

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre, wie deine Kommentardaten verarbeitet werden.