Observer Pattern
← Tilbage til patterns

Observer Pattern

Definerer en one-to-many afhængighed mellem objekter, så når ét objekt ændrer tilstand, notificeres alle afhængige objekter automatisk.

BehavioralMellem

Foto: Lukas / Unsplash

Om Patterned

Observer Pattern er et fundamentalt behavioral design pattern introduceret af Gang of Four i 1994, men konceptet stammer fra Model-View-Controller (MVC) arkitekturen udviklet i Smalltalk i 1970'erne. Patterned etablerer en subscription mekanisme hvor observere kan registrere sig til at modtage notifikationer fra et subject når dets tilstand ændres, hvilket fremmer loose coupling mellem komponenter. Dette er essentielt i event-driven arkitekturer, reactive programming og GUI frameworks hvor multiple views skal opdateres når underliggende data ændres. PHP inkluderer built-in SplObserver og SplSubject interfaces i Standard PHP Library, men mange moderne implementationer bruger event dispatcher systemer som i Symfony eller Laravel. Observer Pattern er grundlaget for mange moderne programmeringsparadigmer inkluderende reactive extensions og pub/sub systemer.

Key Points

  • +Etablerer one-to-many relationship mellem subject og observers
  • +Observers registrerer sig (subscribe) hos subject
  • +Subject notificerer automatisk alle observers ved state changes
  • +Fremmer loose coupling - subject kender ikke observernes detaljer
  • +PHP's SPL tilbyder SplObserver og SplSubject interfaces
  • +Også kendt som Publish-Subscribe pattern
  • +Basis for event-driven programming
  • +Observers kan tilføjes eller fjernes dynamisk runtime
  • +Subject sender typisk sig selv eller event data til observers
  • +Kan føre til memory leaks hvis observers ikke unsubscribes
  • +Performance overhead ved mange observers
  • +Notifikationsordre er typisk ikke garanteret

Kode Eksempel

<?php

declare(strict_types=1);

namespace App\Order;

use SplObserver;
use SplSubject;
use SplObjectStorage;

enum OrderStatus: string
{
    case PENDING = 'pending';
    case PAID = 'paid';
    case SHIPPED = 'shipped';
    case DELIVERED = 'delivered';
    case CANCELLED = 'cancelled';
}

/**
 * Subject - Order der kan observeres
 */
final class Order implements SplSubject
{
    private SplObjectStorage $observers;
    private OrderStatus $status;
    
    public function __construct(
        private readonly string $orderId,
        private readonly float $total,
        private readonly string $customerEmail
    ) {
        $this->observers = new SplObjectStorage();
        $this->status = OrderStatus::PENDING;
    }
    
    public function attach(SplObserver $observer): void
    {
        $this->observers->attach($observer);
    }
    
    public function detach(SplObserver $observer): void
    {
        $this->observers->detach($observer);
    }
    
    public function notify(): void
    {
        foreach ($this->observers as $observer) {
            $observer->update($this);
        }
    }
    
    public function updateStatus(OrderStatus $newStatus): void
    {
        $this->status = $newStatus;
        echo "[Order {$this->orderId}] Status changed to: {$newStatus->value}\n";
        $this->notify();
    }
    
    public function getOrderId(): string
    {
        return $this->orderId;
    }
    
    public function getStatus(): OrderStatus
    {
        return $this->status;
    }
    
    public function getTotal(): float
    {
        return $this->total;
    }
    
    public function getCustomerEmail(): string
    {
        return $this->customerEmail;
    }
}

/**
 * Observer - Email notifikationer
 */
final readonly class EmailNotificationObserver implements SplObserver
{
    public function update(SplSubject $subject): void
    {
        if (!$subject instanceof Order) {
            return;
        }
        
        $email = $subject->getCustomerEmail();
        $status = $subject->getStatus()->value;
        
        echo "  [Email] Sending notification to {$email} about status: {$status}\n";
    }
}

/**
 * Observer - Inventory opdatering
 */
final readonly class InventoryObserver implements SplObserver
{
    public function update(SplSubject $subject): void
    {
        if (!$subject instanceof Order) {
            return;
        }
        
        if ($subject->getStatus() === OrderStatus::PAID) {
            echo "  [Inventory] Reserving items for order {$subject->getOrderId()}\n";
        } elseif ($subject->getStatus() === OrderStatus::CANCELLED) {
            echo "  [Inventory] Releasing reserved items for order {$subject->getOrderId()}\n";
        }
    }
}

/**
 * Observer - Analytics tracking
 */
final readonly class AnalyticsObserver implements SplObserver
{
    public function update(SplSubject $subject): void
    {
        if (!$subject instanceof Order) {
            return;
        }
        
        $orderId = $subject->getOrderId();
        $status = $subject->getStatus()->value;
        $total = $subject->getTotal();
        
        echo "  [Analytics] Tracking: Order {$orderId} -> {$status} (DKK {$total})\n";
    }
}

/**
 * Observer - Shipping service
 */
final readonly class ShippingObserver implements SplObserver
{
    public function update(SplSubject $subject): void
    {
        if (!$subject instanceof Order) {
            return;
        }
        
        if ($subject->getStatus() === OrderStatus::PAID) {
            echo "  [Shipping] Creating shipping label for order {$subject->getOrderId()}\n";
        }
    }
}

// Brug af Observer Pattern
$order = new Order('ORD-12345', 599.00, 'customer@example.com');

// Attach observers
$order->attach(new EmailNotificationObserver());
$order->attach(new InventoryObserver());
$order->attach(new AnalyticsObserver());
$order->attach(new ShippingObserver());

echo "\n=== Order Status Updates ===\n\n";

// Når status ændres, notificeres alle observers automatisk
$order->updateStatus(OrderStatus::PAID);
echo "\n";

$order->updateStatus(OrderStatus::SHIPPED);
echo "\n";

$order->updateStatus(OrderStatus::DELIVERED);

Fordele

  • +Loose coupling mellem subject og observers
  • +Open/Closed Principle - nye observers kan tilføjes uden at ændre subject
  • +Dynamic relationships - observers kan tilføjes/fjernes runtime
  • +Broadcast kommunikation til multiple objekter simultant
  • +Fremmer Single Responsibility - hver observer har én specifik opgave
  • +Grundlag for event-driven og reactive programming

Ulemper

  • -Observers notificeres i arbitrær rækkefølge (ikke deterministisk)
  • -Memory leaks hvis observers ikke detaches korrekt
  • -Performance overhead ved mange observers
  • -Debugging kan være svært da flow ikke er eksplicit

Hvornår bruges det?

  • Event systems - notificer multiple komponenter om system events
  • Order processing - opdater inventory, shipping, email ved order changes
  • User notifications - send notifikationer via multiple kanaler (email, SMS, push)
  • Cache invalidation - invalider caches når data opdateres
  • Logging systems - log events til multiple destinations
  • Real-time dashboards - opdater UI komponenter ved data changes

Best Practices

  • +Brug PHP's SPL interfaces (SplObserver, SplSubject) for standardisering
  • +Ensure observers unsubscribe (detach) når de ikke længere skal notificeres
  • +Overvej event objects i stedet for at sende hele subject
  • +Implementer error handling i observers så én fejl ikke stopper alle
  • +Dokumenter hvilke events subject kan udsende
  • +Brug weak references hvis observers holder subject references
  • +Overvej async notification for performance-kritiske systemer
  • +Test observers isoleret fra subject

Quick Info

Kategori
Behavioral
Sværhedsgrad
Mellem
Relaterede Patterns
  • Mediator Pattern - centraliserer kommunikation mellem objekter
  • Event Dispatcher - modernere approach til observer pattern
  • Publish-Subscribe - variant med message broker mellem publisher og subscribers