Lazy Initialization

Lazy Initialization, auch späte Initialisierung genannt, bedeutet, dass ein Objekt nicht bereits im Konstruktor vollständig initialisiert wird, sondern erst on demand, das heisst, wenn wir das Objekt tatsächlich verwenden. Hier ein gutes Beispiel.

Lazy Initialization ist besonders hilfreich bei Datenbankverbindungen. Anstelle ein in PHP eingebautes Objekt wie PDO oder mysqli direkt zu verwenden und dadurch eine Datenbankverbindung schon dann aufzubauen, wenn das Objekt instantiiert wird, können wir einen Wrapper erstellen, der automatisch genau dann eine Verbindung aufbaut, wenn wir das erste Mal tatsächlich mit der Datenbank sprechen.

Hier ein Beispiel für SQLite, weil das so schön ohne externe Abhängigkeiten in der PHP-Installation funktioniert:

<?php declare(strict_types=1);

namespace spriebsch\tfd\designPatterns\lazyInitialization;

use SQLite3;
use SQLite3Result;
use SQLite3Stmt;

final class SqliteConnection implements Connection
{
    private ?SQLite3 $connection = null;
    private string   $database;

    public function __construct(string $database)
    {
        $this->database = $database;
    }

    public function prepare(string $statement): SQLite3Stmt
    {
        return $this->connection()->prepare($statement);
    }

    public function exec(string $statement): bool
    {
        var_dump('Executing statement ' . $statement);

        return $this->connection()->exec($statement);
    }

    public function query(string $query): SQLite3Result
    {
        return $this->connection()->query($query);
    }

    private function connection(): Sqlite3
    {
        if ($this->connection === null) {

            var_dump('Connecting to the database');

            $this->connection = new SQLite3($this->database);
            $this->connection->enableExceptions(true);
            $this->connection->exec('PRAGMA journal_mode=WAL');
        }

        return $this->connection;
    }
}
SqliteConnection.php

Wir haben hier zwei var_dump()-Statements eingebaut, damit wir nachher gleich sehen können, was passiert.

Die Klasse ist übrigens final, weil wir weder von ihr ableiten wollen noch sie mocken werden. Stattdessen würden wir das Interface Connection mocken:

<?php declare(strict_types=1);

namespace spriebsch\tfd\designPatterns\lazyInitialization;

use SQLite3Result;
use SQLite3Stmt;

interface Connection
{
    public function prepare(string $statement): SQLite3Stmt;

    public function exec(string $statement): bool;

    public function query(string $query): SQLite3Result;

    //change
}
Connection.php

Es ist viel besser, ein Interface zu mocken anstelle einer konkreten Klasse, weil ein Interface keinen Konstruktor hat. Nun ja, zumindest sollte ein Interface keine Konstruktor-Methode enthalten. Aber das nur am Rande.

Unser SqliteConnection ist toll, weil wir in der Methode connect() die Datenbankverbindung auch gleich passend konfigurieren. Endlich mal haben wir eine zentrale Stelle im Code, wo wir das tun können. Oben legen wir beispielsweise fest, dass die Datenbank bei Fehlern Exceptions werfen soll.

Probieren wir das Ganze einmal aus. Wir verwenden eine In-Memory-Datenbank, weil wir ja gesagt hatten, dass wir ohne externe Abhängigkeiten arbeiten wollen:

<?php declare(strict_types=1);

namespace spriebsch\tfd\designPatterns\lazyInitialization;

require __DIR__ . '/autoload.php';

$db = new SqliteConnection(':memory:');
$db->exec('SELECT * FROM sqlite_schema');
connect.php

Die verwendete Datenbank ist leer, beziehungsweise sie hat kein Schema. Das stört uns aber nicht, weil wir wollen ja nur eine Verbindung aufbauen und nicht wirklich produktive Abfragen ausführen.

Legen wir also los:

SqliteConnection.php:26:
string(47) "Executing statement SELECT * FROM sqlite_schema"
SqliteConnection.php:40:
string(26) "Connecting to the database"
Ausgabe von connect.php
connect.php ausführen

Wir sehen, dass zuerst die Methode exec() aufgerufen wird und danach implizit beziehungsweise automatisch die Methode connect(). So soll es sein.

Die Methode connect() sieht ziemlich genau so aus wie Code, den wir in einem Singleton finden würden. Damit stellen wir sicher, dass wir nur einmal eine Datenbankverbindung aufbauen und nicht bei jeder Interaktion mit der Datenbank erneut.

Um auszuprobieren, ob das wirklich so funktioniert, senden wir zwei Abfragen hintereinander ab:

<?php declare(strict_types=1);

namespace spriebsch\tfd\designPatterns\lazyInitialization;

require __DIR__ . '/autoload.php';

$db = new SqliteConnection(':memory:');
$db->exec('SELECT * FROM sqlite_schema');
$db->exec('SELECT * FROM sqlite_schema');
twoStatements.php

Funktioniert das?

SqliteConnection.php:26:
string(47) "Executing statement SELECT * FROM sqlite_schema"
SqliteConnection.php:40:
string(26) "Connecting to the database"
SqliteConnection.php:26:
string(47) "Executing statement SELECT * FROM sqlite_schema"
Ausgabe von twoStatements.php
twoStatements.php ausführen

Tatsächlich, es funktioniert. Die Verbindung wurde nur einmal aufgebaut und bei der zweiten Abfrage wiederverwendet. Das Ganze könnten wir natürlich genauso für andere Datenbankschnittstellen wie PDO oder mysqli bauen.

Fall Du Dich jetzt wunderst, dass wir hier eine Leaky Abstraction haben, weil die Rückgabewerte der Methoden die originalen in PHP eingebauten Klassen sind? Gut erkannt! In diesem Fall geht es uns allerdings nicht darum, uns von der eigentlichen Datenbankschnittstelle zu abstrahieren, sondern das Lazy Initialization-Entwurfsmuster zu implementieren. Die "leakiness" der Abstraktion ist dabei beabsichtigt beziehungsweise nicht schädlich.