Observer Pattern Events
Observer Pattern und Event-Systeme
Das Observer Pattern ermöglicht lose Kopplung durch ein Publish-Subscribe-Modell. Lernen Sie die Implementierung und moderne Event-Systeme.
Das Pattern verstehen
┌─────────────────────────────────────────────────────────────┐ │ OBSERVER PATTERN │ ├─────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────┐ notify() ┌─────────────┐ │ │ │ Subject │ ───────────────────────►│ Observer A │ │ │ │ (Publisher)│ └─────────────┘ │ │ │ │ notify() ┌─────────────┐ │ │ │ - observers│ ───────────────────────►│ Observer B │ │ │ │ - state │ └─────────────┘ │ │ │ │ notify() ┌─────────────┐ │ │ │ attach() │ ───────────────────────►│ Observer C │ │ │ │ detach() │ └─────────────┘ │ │ │ notify() │ │ │ └─────────────┘ │ │ │ └─────────────────────────────────────────────────────────────┘
Klassische Implementierung (PHP)
// Observer Interface
interface Observer {
public function update(Subject $subject): void;
}
// Subject Interface
interface Subject {
public function attach(Observer $observer): void;
public function detach(Observer $observer): void;
public function notify(): void;
}
// Konkretes Subject
class User implements Subject {
private array $observers = [];
private string $email;
private string $status = 'pending';
public function attach(Observer $observer): void {
$this->observers[] = $observer;
}
public function detach(Observer $observer): void {
$this->observers = array_filter(
$this->observers,
fn($o) => $o !== $observer
);
}
public function notify(): void {
foreach ($this->observers as $observer) {
$observer->update($this);
}
}
public function setStatus(string $status): void {
$this->status = $status;
$this->notify(); // Alle Observer benachrichtigen
}
public function getStatus(): string {
return $this->status;
}
public function getEmail(): string {
return $this->email;
}
}
// Konkrete Observer
class EmailNotificationObserver implements Observer {
public function update(Subject $subject): void {
if ($subject instanceof User && $subject->getStatus() === 'active') {
$this->sendWelcomeEmail($subject->getEmail());
}
}
private function sendWelcomeEmail(string $email): void {
echo "Sending welcome email to {$email}\n";
}
}
class LoggingObserver implements Observer {
public function update(Subject $subject): void {
if ($subject instanceof User) {
echo "User status changed to: {$subject->getStatus()}\n";
}
}
}
class AnalyticsObserver implements Observer {
public function update(Subject $subject): void {
if ($subject instanceof User) {
echo "Tracking user status change in analytics\n";
}
}
}
// Verwendung
$user = new User();
$user->attach(new EmailNotificationObserver());
$user->attach(new LoggingObserver());
$user->attach(new AnalyticsObserver());
$user->setStatus('active'); // Alle Observer werden benachrichtigt
Modernes Event-System (PHP)
// Event Klasse
class Event {
private bool $propagationStopped = false;
public function stopPropagation(): void {
$this->propagationStopped = true;
}
public function isPropagationStopped(): bool {
return $this->propagationStopped;
}
}
// Konkrete Events
class UserRegisteredEvent extends Event {
public function __construct(
public readonly User $user,
public readonly DateTime $registeredAt
) {}
}
class OrderPlacedEvent extends Event {
public function __construct(
public readonly Order $order,
public readonly User $user
) {}
}
// Event Dispatcher
class EventDispatcher {
private array $listeners = [];
public function addListener(string $eventClass, callable $listener, int $priority = 0): void {
$this->listeners[$eventClass][$priority][] = $listener;
}
public function dispatch(Event $event): Event {
$eventClass = get_class($event);
if (!isset($this->listeners[$eventClass])) {
return $event;
}
// Nach Priorität sortieren (höher = früher)
krsort($this->listeners[$eventClass]);
foreach ($this->listeners[$eventClass] as $priority => $listeners) {
foreach ($listeners as $listener) {
if ($event->isPropagationStopped()) {
return $event;
}
$listener($event);
}
}
return $event;
}
}
// Event Subscriber (gruppiert mehrere Listener)
interface EventSubscriberInterface {
public static function getSubscribedEvents(): array;
}
class UserEventSubscriber implements EventSubscriberInterface {
public static function getSubscribedEvents(): array {
return [
UserRegisteredEvent::class => [
['onUserRegistered', 10],
['sendWelcomeEmail', 0],
],
];
}
public function onUserRegistered(UserRegisteredEvent $event): void {
echo "User {$event->user->getName()} registered\n";
}
public function sendWelcomeEmail(UserRegisteredEvent $event): void {
echo "Sending welcome email to {$event->user->getEmail()}\n";
}
}
// Verwendung
$dispatcher = new EventDispatcher();
// Einzelne Listener
$dispatcher->addListener(
UserRegisteredEvent::class,
fn($e) => echo "Analytics: New user\n",
5
);
// Subscriber registrieren
$subscriber = new UserEventSubscriber();
foreach ($subscriber::getSubscribedEvents() as $eventClass => $listeners) {
foreach ($listeners as [$method, $priority]) {
$dispatcher->addListener($eventClass, [$subscriber, $method], $priority);
}
}
// Event auslösen
$event = new UserRegisteredEvent($user, new DateTime());
$dispatcher->dispatch($event);
JavaScript Event System
// Einfacher Event Emitter
class EventEmitter {
constructor() {
this.listeners = new Map();
}
on(event, callback) {
if (!this.listeners.has(event)) {
this.listeners.set(event, []);
}
this.listeners.get(event).push(callback);
return this; // Für Chaining
}
off(event, callback) {
if (!this.listeners.has(event)) return this;
const callbacks = this.listeners.get(event);
const index = callbacks.indexOf(callback);
if (index > -1) {
callbacks.splice(index, 1);
}
return this;
}
once(event, callback) {
const wrapper = (...args) => {
callback(...args);
this.off(event, wrapper);
};
return this.on(event, wrapper);
}
emit(event, ...args) {
if (!this.listeners.has(event)) return false;
this.listeners.get(event).forEach(callback => {
callback(...args);
});
return true;
}
}
// Verwendung
const emitter = new EventEmitter();
emitter.on('user:created', (user) => {
console.log(`User created: ${user.name}`);
});
emitter.on('user:created', (user) => {
sendWelcomeEmail(user.email);
});
emitter.once('user:created', (user) => {
console.log('This only runs once');
});
// Event auslösen
emitter.emit('user:created', { name: 'Max', email: 'max@test.de' });
// Typisierter Event Emitter (TypeScript-Style mit JSDoc)
/**
* @template {Record} Events
*/
class TypedEventEmitter {
/** @type {Map} */
#listeners = new Map();
/**
* @template {keyof Events} K
* @param {K} event
* @param {(...args: Events[K]) => void} callback
*/
on(event, callback) {
if (!this.#listeners.has(event)) {
this.#listeners.set(event, []);
}
this.#listeners.get(event).push(callback);
return this;
}
/**
* @template {keyof Events} K
* @param {K} event
* @param {Events[K]} args
*/
emit(event, ...args) {
this.#listeners.get(event)?.forEach(cb => cb(...args));
}
}
// Verwendung mit definiertem Event-Schema
/** @typedef {{ 'user:login': [User], 'user:logout': [string], 'error': [Error] }} AppEvents */
/** @type {TypedEventEmitter} */
const events = new TypedEventEmitter();
events.on('user:login', (user) => {
// user ist typisiert
});
events.emit('user:login', currentUser);
DOM Events (Browser)
// Custom Events im Browser
class UserComponent extends HTMLElement {
connectedCallback() {
this.innerHTML = ``;
this.querySelector('button').addEventListener('click', () => {
this.registerUser();
});
}
registerUser() {
// ... registration logic ...
// Custom Event dispatchen
const event = new CustomEvent('user:registered', {
bubbles: true, // Event steigt im DOM auf
composed: true, // Durchdringt Shadow DOM
detail: {
user: { name: 'Max', email: 'max@test.de' }
}
});
this.dispatchEvent(event);
}
}
customElements.define('user-component', UserComponent);
// Event global abfangen
document.addEventListener('user:registered', (e) => {
console.log('User registered:', e.detail.user);
});
Praktische Anwendungsfälle
// 1. Entkopplung von Modulen
class OrderService {
constructor(private eventDispatcher: EventDispatcher) {}
placeOrder(order: Order): void {
// Kernlogik
order.status = 'placed';
this.orderRepository.save(order);
// Andere Module werden via Event benachrichtigt
this.eventDispatcher.dispatch(new OrderPlacedEvent(order));
// Inventory, Shipping, Email, Analytics reagieren unabhängig
}
}
// 2. Audit Logging
dispatcher.addListener('*', (event) => {
auditLogger.log({
event: event.constructor.name,
timestamp: new Date(),
data: event
});
});
// 3. Caching invalidieren
dispatcher.addListener(ProductUpdatedEvent::class, (event) => {
cache.invalidate(`product:${event.product.id}`);
cache.invalidate('product:list');
});
// 4. Real-time Updates (WebSockets)
dispatcher.addListener(MessageSentEvent::class, (event) => {
websocket.broadcast('chat:' + event.roomId, {
type: 'new_message',
message: event.message
});
});
💡 Best Practices:
1. Events sollten immutable sein (readonly properties)
2. Vermeiden Sie zu viele Events (Performance)
3. Dokumentieren Sie alle Events
4. Nutzen Sie typisierte Events wo möglich
5. Events für asynchrone/optionale Aktionen, direkte Aufrufe für Pflichtaktionen
2. Vermeiden Sie zu viele Events (Performance)
3. Dokumentieren Sie alle Events
4. Nutzen Sie typisierte Events wo möglich
5. Events für asynchrone/optionale Aktionen, direkte Aufrufe für Pflichtaktionen