Singleton Pattern Verwendung
Singleton Pattern: Richtig einsetzen
Das Singleton Pattern stellt sicher, dass eine Klasse nur eine Instanz hat. Lernen Sie wann es sinnvoll ist und welche Alternativen es gibt.
Das Pattern
class Singleton {
private static ?self $instance = null;
// Private Constructor verhindert new Singleton()
private function __construct() {}
// Verhindert Klonen
private function __clone() {}
// Verhindert Unserialisierung
public function __wakeup() {
throw new Exception("Cannot unserialize singleton");
}
// Einziger Zugriffspunkt
public static function getInstance(): self {
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
}
// Verwendung
$instance1 = Singleton::getInstance();
$instance2 = Singleton::getInstance();
var_dump($instance1 === $instance2); // true - Gleiche Instanz
Praktisches Beispiel: Logger
class Logger {
private static ?self $instance = null;
private array $logs = [];
private $fileHandle;
private function __construct() {
$this->fileHandle = fopen('app.log', 'a');
}
public function __destruct() {
if ($this->fileHandle) {
fclose($this->fileHandle);
}
}
public static function getInstance(): self {
return self::$instance ??= new self();
}
public function log(string $level, string $message): void {
$entry = sprintf(
"[%s] %s: %s\n",
date('Y-m-d H:i:s'),
strtoupper($level),
$message
);
$this->logs[] = $entry;
fwrite($this->fileHandle, $entry);
}
public function info(string $message): void {
$this->log('info', $message);
}
public function error(string $message): void {
$this->log('error', $message);
}
public function getLogs(): array {
return $this->logs;
}
}
// Überall in der Anwendung
Logger::getInstance()->info('User logged in');
Logger::getInstance()->error('Database connection failed');
JavaScript Singleton
// ES6 Module sind bereits Singletons!
// logger.js
class Logger {
constructor() {
this.logs = [];
}
log(level, message) {
const entry = `[${new Date().toISOString()}] ${level}: ${message}`;
this.logs.push(entry);
console.log(entry);
}
info(message) { this.log('INFO', message); }
error(message) { this.log('ERROR', message); }
}
// Eine einzige Instanz exportieren
export const logger = new Logger();
// Verwendung überall gleich
import { logger } from './logger.js';
logger.info('Application started');
// Klassischer Singleton in JavaScript
const Singleton = (function() {
let instance;
function createInstance() {
return {
timestamp: Date.now(),
data: {},
setData(key, value) {
this.data[key] = value;
},
getData(key) {
return this.data[key];
}
};
}
return {
getInstance: function() {
if (!instance) {
instance = createInstance();
}
return instance;
}
};
})();
// Verwendung
const s1 = Singleton.getInstance();
const s2 = Singleton.getInstance();
console.log(s1 === s2); // true
Probleme mit Singleton
// ❌ Problem 1: Versteckte Abhängigkeiten
class UserService {
public function createUser(string $name): void {
// Woher kommt der Logger? Nicht ersichtlich!
Logger::getInstance()->info("Creating user: {$name}");
// Woher kommt die DB? Versteckt!
Database::getInstance()->insert('users', ['name' => $name]);
}
}
// ❌ Problem 2: Schwer testbar
class UserServiceTest extends TestCase {
public function testCreateUser(): void {
$service = new UserService();
$service->createUser('Test');
// Wie mocken wir Logger und Database?
// Singleton macht das sehr schwierig!
}
}
// ❌ Problem 3: Globaler State
// Änderungen wirken sich überall aus
Logger::getInstance()->setLevel('debug');
// Jetzt loggt ALLES in der Anwendung auf Debug-Level
// ❌ Problem 4: Thread-Safety (in multithreaded Umgebungen)
// Race Condition möglich bei getInstance()
Bessere Alternative: Dependency Injection
// ✅ Klare Abhängigkeiten
class UserService {
public function __construct(
private LoggerInterface $logger,
private DatabaseInterface $db
) {}
public function createUser(string $name): void {
$this->logger->info("Creating user: {$name}");
$this->db->insert('users', ['name' => $name]);
}
}
// Einfach testbar
class UserServiceTest extends TestCase {
public function testCreateUser(): void {
$mockLogger = $this->createMock(LoggerInterface::class);
$mockDb = $this->createMock(DatabaseInterface::class);
$mockLogger->expects($this->once())
->method('info')
->with($this->stringContains('Test'));
$mockDb->expects($this->once())
->method('insert');
$service = new UserService($mockLogger, $mockDb);
$service->createUser('Test');
}
}
// DI Container kümmert sich um Instanzen
// Container kann intern Singletons verwalten
$container->singleton(LoggerInterface::class, fn() => new FileLogger());
Wann Singleton SINNVOLL ist
// ✅ 1. Ressourcen-Manager (Datenbankverbindungen)
class ConnectionPool {
private static ?self $instance = null;
private array $connections = [];
public static function getInstance(): self {
return self::$instance ??= new self();
}
public function getConnection(): PDO {
// Verbindungen wiederverwenden statt neu erstellen
}
}
// ✅ 2. Konfiguration (Read-only)
class Config {
private static ?self $instance = null;
private array $config;
private function __construct() {
$this->config = require 'config.php';
}
public static function getInstance(): self {
return self::$instance ??= new self();
}
public function get(string $key, mixed $default = null): mixed {
return $this->config[$key] ?? $default;
}
}
// ✅ 3. Caching
class Cache {
private static ?self $instance = null;
private array $store = [];
public static function getInstance(): self {
return self::$instance ??= new self();
}
public function get(string $key): mixed {
return $this->store[$key] ?? null;
}
public function set(string $key, mixed $value): void {
$this->store[$key] = $value;
}
}
Singleton + Dependency Injection
// Moderne Lösung: Container verwaltet Singletons
// Container-Konfiguration
$container = new Container();
// Als Singleton registrieren
$container->singleton(LoggerInterface::class, function($c) {
return new FileLogger('/var/log/app.log');
});
// Normal (neue Instanz jedes Mal)
$container->bind(UserService::class, function($c) {
return new UserService(
$c->get(LoggerInterface::class), // Immer gleiche Instanz
$c->get(DatabaseInterface::class)
);
});
// Verwendung
$logger1 = $container->get(LoggerInterface::class);
$logger2 = $container->get(LoggerInterface::class);
// $logger1 === $logger2 (Singleton durch Container)
// Aber: Klassen haben keine statischen Aufrufe
// Abhängigkeiten sind klar sichtbar
// Einfach testbar
Thread-Safe Singleton (PHP 8.1+)
// Mit Enums (PHP 8.1+) - automatisch Singleton
enum AppLogger {
case Instance;
private array $logs;
public function log(string $message): void {
// Enum-Cases sind Singletons
}
}
// Verwendung
AppLogger::Instance->log('Message');
⚠️ Empfehlung:
Vermeiden Sie klassische Singletons mit statischen Methoden. Nutzen Sie stattdessen Dependency Injection Container, die Instanzen als Singletons verwalten können. So behalten Sie Testbarkeit und klare Abhängigkeiten.