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.
Ein gutes Beispiel für späte Initialisierung findest Du hier.
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);
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;
}
}
Wir haben hier zwei var_dump()
-Statements eingebaut damit, wir nachher gleich sehen können, was passiert.
Die Klasse ist übrigens final, weil wir sie weder extenden noch mocken werden. Stattdessen würden wir das Interface Connection
mocken:
<?php declare(strict_types=1);
interface Connection
{
public function prepare(string $statement): SQLite3Stmt;
public function exec(string $statement): bool;
public function query(string $query): SQLite3Result;
}
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 und das Problem, dass wir eine an anderer Stelle aufgebaute Verbindung anders konfigurieren, fällt damit weg.
Probieren wir es 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);
require __DIR__ . '/autoload.php';
$db = new SqliteConnection(':memory:');
$db->exec('SELECT * FROM sqlite_schema');
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:20: string(47) "Executing statement SELECT * FROM sqlite_schema" SqliteConnection.php:34: string(26) "Connecting to the database"
Wir sehen, dass zuerst die Methode exec()
aufgerufen wird und danach connect()
. So soll es sein.
Die Methode connect()
hat ähnlichen Code wie wir ihn 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);
require __DIR__ . '/autoload.php';
$db = new SqliteConnection(':memory:');
$db->exec('SELECT * FROM sqlite_schema');
$db->exec('SELECT * FROM sqlite_schema');
Funktioniert das?
SqliteConnection.php:20: string(47) "Executing statement SELECT * FROM sqlite_schema" SqliteConnection.php:34: string(26) "Connecting to the database" SqliteConnection.php:20: string(47) "Executing statement SELECT * FROM sqlite_schema"
Tatsächlich, es funktioniert. Die Verbindung wurde nur einmal aufgebaut und bei der zweiten Abfrage wiederverwendet. Das Ganze könntest Du natürlich genauso für andere Datenbankschnittstellen wie PDO
oder mysqli
bauen.
Du wunderst Dich, dass wir hier eine Leaky Abstraction haben, weil die Rückgabewerte der Methoden die originalen in PHP eingebauten Klassen sind? Es ist gut, wenn Du dies erkannt hast. Aber in diesem Fall geht es nicht darum, uns von der eigentlichen Datenbankschnittstelle zu abstrahieren, sondern das Lazy Initialization-Entwurfsmuster zu implementieren. Die Leaky Abstraction ist dabei beabsichtigt beziehungsweise nicht schädlich.