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.
Weitere Informationen
Texterstellung mit KI-Unterstützung