Arbeit im Konstruktor

Der Konstruktor sollte ein Objekt in einen arbeitsfähigen Zustand versetzen. Falls das nicht möglich ist, sollte der Konstruktor eine Exception werfen. Das verhindert, dass ein Objekt in einem ungültigen Zustand existiert.

<?php declare(strict_types=1);

final class SomethingPositive
{
    private int $positiveValue;

    public function __construct(int $mustBePositive)
    {
        if ($mustBePositive < 0) {
            throw new InvalidArgumentException('The greatest teacher, failure is.');
        }

        $this->positiveValue = $mustBePositive;
    }

    // ...
}
SomethingPositive.php

In diesem Beispiel wissen wir, dass der im Attribut positiveValue gespeicherte Wert positiv ist, falls das Objekt SomethingPositive existiert. Das Objekt ist in einem garantiert gültigen Zustand oder wir konnten es gar nicht erst erzeugen:

createObject.php:6:
class SomethingPositive#2 (1) {
  private int $positiveValue =>
  int(1)
}
Ausgabe von createObject.php

Solange der Konstruktorparameter positiv ist, klappt das. Aber wehe, wenn wir versuchen, unserer Software einen negativen Wert als positiv zu verkaufen:

PHP Fatal error:  Uncaught InvalidArgumentException: The greatest teacher, failure is. in SomethingPositive.php:10
Stack trace:
#0 createObject-will-fail.php(5): SomethingPositive->__construct()
#1 {main}
  thrown in SomethingPositive.php on line 10
Ausgabe von createObject-will-fail.php

Ein Konstruktor sollte allerdings ein Objekt nur initialisieren und keine "echte Arbeit" verrichten. "Echte Arbeit" wären beispielsweise teure Berechnungen, weil diese die Objekterzeugung aufwändig und damit langsam machen.

Keine echte Arbeit im Konstruktor, schon gar keinen I/O.

Teure Initialisierung

Falls Du der Meinung bist, dass Du ein Objekt hast, das teuer initialisiert werden muss, dann solltest Du stattdessen auf Lazy Initialization setzen. Hier fällt der Aufwand erst dann an, wenn ein Objekt auch tatsächlich benutzt wird und wir führen nicht spekulativ Code aus, dessen Ergebnisse gar nicht gebraucht werden.

Im nachfolgenden Beispiel erzeugen wir im Konstruktor Zufallszahlen, die dann nacheinander aus dem Objekt abgerufen werden können. Das Ganze ist zugegebenermaßen ein wenig sinnlos und nicht wirklich teuer, aber ich wollte ein nicht so ganz triviales Beispiel haben, daher die position. Und wir tun jetzt einfach so, als ob die Erzeugung von 1000 Zufallszahlen super teuer wäre:

<?php declare(strict_types=1);

final class ExpensiveConstructor
{
    private int   $position     = 0;
    private array $randomValues = [];

    public function __construct()
    {
        for ($i = 0; $i < 1000; $i++) {
            $this->randomValues[] = random_int(1, 1000);
        }
    }

    public function getRandomValue(): int
    {
        $position = $this->position;
        $this->position++;

        return $this->randomValues[$position];
    }
}
ExpensiveConstructor.php

Probieren wir's aus:

<?php declare(strict_types=1);

require __DIR__ . '/autoload.php';

$randomValues = new ExpensiveConstructor;

for ($i = 0; $i < 3; $i++) {
    var_dump($randomValues->getRandomValue());
}
expensiveRandomValues.php
expensiveRandomValues.php:8:
int(13)
expensiveRandomValues.php:8:
int(240)
expensiveRandomValues.php:8:
int(109)
Ausgabe von expensiveRandomValues.php

Funktioniert, aber war halt teuer. Wir haben hier 997 Zufallszahlen umsonst erzeugt.

Wir können das so umbauen, dass die Initialisierung des Arrays erst dann erfolgt, wenn wir auch tatsächlich darauf zugreifen:

<?php declare(strict_types=1);

final class LazyInitialization
{
    private int    $position     = 0;
    private ?array $randomValues = null;

    public function getRandomValue(): int
    {
        $this->initialize();

        $position = $this->position;
        $this->position++;

        return $this->randomValues[$position];
    }

    private function initialize(): void
    {
        if ($this->randomValues !== null) {
            return;
        }

        for ($i = 0; $i < 1000; $i++) {
            $this->randomValues[] = random_int(1, 1000);
        }
    }
}
LazyInitialization.php

Im Wesentlichen haben wir hier nur das, was zuvor im Konstruktor stand, in die Methode initialize() verschoben. Ein sehr gutes Beispiel dafür, dass wir oft zwar den richtigen Code schreiben, dies aber an einer falschen Stelle tun.

Natürlich kommen da diesmal andere Zufallszahlen raus, was aber der Tatsache keinen Abbruch tut, dass das Ganze wie erwartet funktioniert:

<?php declare(strict_types=1);

require __DIR__ . '/autoload.php';

$randomValues = new LazyInitialization;

for ($i = 0; $i < 3; $i++) {
    var_dump($randomValues->getRandomValue());
}
lazyRandomValues.php
lazyRandomValues.php:8:
int(763)
lazyRandomValues.php:8:
int(845)
lazyRandomValues.php:8:
int(901)
Ausgabe von lazyRandomValues.php

Wenn wir uns LazyInitialization mit Verstand ansehen, wird deutlich, dass es überhaupt nichts bringt, vorab 1000 Zufallszahlen zu erzeugen. Wir können diese einfach einzeln on demand erzeugen:

<?php declare(strict_types=1);

final class AdHoc
{
    public function getRandomValue(): int
    {
        return random_int(1, 1000);
    }
}
AdHoc.php

Auch das sollte funktionieren:

<?php declare(strict_types=1);

require __DIR__ . '/autoload.php';

$randomValues = new AdHoc;

for ($i = 0; $i < 3; $i++) {
    var_dump($randomValues->getRandomValue());
}
adHocRandomValues.php
adHocRandomValues.php:8:
int(247)
adHocRandomValues.php:8:
int(772)
adHocRandomValues.php:8:
int(47)
Ausgabe von adHocRandomValues.php

Jawohl. Schön, wenn es so deutlich wird, dass das ursprüngliche Beispiel nicht nur blöd, sondern auch unnötig kompliziert war. Aber es ist natürlich ein schönes Plädoyer dafür, Objekte eben nicht vorab aufwändig zu initialisieren, sondern Berechnungen möglichst on demand auszuführen.

Auf gar keinen Fall sollte der Konstruktor I/O machen, beispielsweise indem wir eine Datenbankverbindung aufbauen, mit einem Remote-Dienst kommunizieren oder auf das Dateisystem zugreifen. Das wäre nicht nur besonders langsam, sondern würde auch die Wiederverwendung des Objekts erschweren, weil wir vor der Objektzeugung externe Abhängigkeiten erfüllen müssten.

Hier findest Du ein Beispiel für das Lazy Initialization-Entwurfsmuster in Verbindung mit Datenbankverbindungen.