Unveränderlichkeit

Da wir die Art und Weise, wie PHP intern mit Objekten arbeitet, nicht verändern können, müssen wir Zustandsänderungen von Wertobjekten unmöglich machen. Ein solches Objekt nennt man immutable (unveränderlich). Ein unveränderliches Objekt wird durch den Konstruktor initialisiert und darf dann seinen Zustand nie mehr verändern.

Das können wir in aktuellen PHP-Versionen sehr schön durch readonly-Properties sicherstellen:

<?php declare(strict_types=1);

namespace spriebsch\tfd\designPatterns\valueObject\money\immutability;

class Money
{
    public function __construct(
        public readonly int $amount,
        public readonly Currency $currency
    ) {}
}
Money.php

Die beiden Properties müssen jetzt nicht mehr privat, sondern dürfen öffentlich sein. Dank Unveränderlichkeit können wir Properties großzügig öffentlich lesbar machen, zumal wir es uns damit sparen können, Getter zu schreiben.

Wir könnten übrigens auch die ganze Klasse als readonly deklarieren, dann müssen wir das nicht für jedes einzelne Property tun:

<?php declare(strict_types=1);

namespace spriebsch\tfd\designPatterns\valueObject\money\immutability;

readonly class ReadonlyMoney
{
    public function __construct(
        public int $amount,
        public Currency $currency
    ) {}
}
ReadonlyMoney.php

Lesen darf jetzt jeder

<?php declare(strict_types=1);

namespace spriebsch\tfd\designPatterns\valueObject\money\immutability;

require __DIR__ . '/autoload.php';

$money = new Money(200, Currency::EUR);

var_dump($money->amount, $money->currency);
read.php
read.php:9:
int(200)
read.php:9:
enum spriebsch\tfd\designPatterns\valueObject\money\immutability\Currency::EUR;
Ausgabe von read.php
read.php ausführen

aber schreiben beziehungsweise verändern ist nicht möglich:

<?php declare(strict_types=1);

namespace spriebsch\tfd\designPatterns\valueObject\money\immutability;

require __DIR__ . '/autoload.php';

$money = new ReadonlyMoney(200, Currency::EUR);

$money->amount += 300;
write-will-fail.php
PHP Fatal error:  Uncaught Error: Cannot modify readonly property spriebsch\tfd\designPatterns\valueObject\money\immutability\ReadonlyMoney::$amount in write-will-fail.php:9
Stack trace:
#0 {main}
  thrown in write-will-fail.php on line 9
Ausgabe von write-will-fail.php
write-will-fail.php ausführen

Was machen wir jetzt mit unserer increase()-Methode?

Die lassen wir eine neue Instanz von Money zurückgeben:

<?php declare(strict_types=1);

namespace spriebsch\tfd\designPatterns\valueObject\money\immutability;

class ImmutableMoney
{
    public function __construct(
        public readonly int $amount,
        public readonly Currency $currency
    ) {}

    public function increase(int $amount): ImmutableMoney
    {
        return new ImmutableMoney($this->amount + $amount, $this->currency);
    }
}
ImmutableMoney.php

So bleibt das ursprüngliche Objekt unverändert und wir erhalten ein neues Objekt, das den neuen Betrag repräsentiert:

<?php declare(strict_types=1);

namespace spriebsch\tfd\designPatterns\valueObject\money\immutability;

require __DIR__ . '/autoload.php';

$money = new ImmutableMoney(200, Currency::EUR);

var_dump($money, $money->increase(100));
increase.php
increase.php:9:
class spriebsch\tfd\designPatterns\valueObject\money\immutability\ImmutableMoney#2 (2) {
  public readonly int $amount =>
  int(200)
  public readonly spriebsch\tfd\designPatterns\valueObject\money\immutability\Currency $currency =>
  enum spriebsch\tfd\designPatterns\valueObject\money\immutability\Currency::EUR;
}
increase.php:9:
class spriebsch\tfd\designPatterns\valueObject\money\immutability\ImmutableMoney#4 (2) {
  public readonly int $amount =>
  int(300)
  public readonly spriebsch\tfd\designPatterns\valueObject\money\immutability\Currency $currency =>
  enum spriebsch\tfd\designPatterns\valueObject\money\immutability\Currency::EUR;
}
Ausgabe von increase.php
increase.php ausführen

Jetzt kannst ich meinen 20-Euro-Schein ganz für mich alleine behalten, weil Du Dein eigenes, neues Objekt bekommen hast.

Wertobjekte sind unveränderlich (immutable).

Unsere Wertobjekte sind also im Prinzip Wegwerfobjekte. Wir erzeugen davon so viele Instanzen, wie wir brauchen. Die Garbage Collection räumt die Objekte schon auf, wenn sie nicht mehr referenziert werden. Die Erzeugung eines Money-Objekts ist nicht aufwändig und geht daher sehr schnell.

Jetzt können wir uns um die eigentliche Funktionalität kümmern.