Observer i Strategy to dwa z najczęściej używanych wzorców behawioralnych. Observer buduje luźno powiązany system zdarzeń – jeden obiekt zmienia stan, wiele innych reaguje. Strategy wymienia algorytmy jak klocki – ta sama operacja, różne sposoby wykonania. Oba wzorce realizują zasadę Open/Closed z SOLID.
Observer
Observer definiuje relację jeden-do-wielu: gdy jeden obiekt (Subject) zmienia stan, wszyscy jego obserwatorzy są automatycznie powiadamiani. Subject nie zna konkretnych klas obserwatorów – tylko ich wspólny interfejs.
<?php
declare(strict_types=1);
// Interfejsy wzorca
interface ObserverInterface
{
public function update(string $event, mixed $data): void;
}
interface SubjectInterface
{
public function subscribe(string $event, ObserverInterface $observer): void;
public function unsubscribe(string $event, ObserverInterface $observer): void;
public function notify(string $event, mixed $data = null): void;
}
// Subject - obiekt obserwowany
class EventEmitter implements SubjectInterface
{
/** @var array */
private array $listeners = [];
public function subscribe(string $event, ObserverInterface $observer): void
{
$this->listeners[$event][] = $observer;
}
public function unsubscribe(string $event, ObserverInterface $observer): void
{
$this->listeners[$event] = array_filter(
$this->listeners[$event] ?? [],
fn($o) => $o !== $observer
);
}
public function notify(string $event, mixed $data = null): void
{
foreach ($this->listeners[$event] ?? [] as $observer) {
$observer->update($event, $data);
}
}
}
<?php
declare(strict_types=1);
// Konkretny Subject - koszyk zakupowy
class ShoppingCart extends EventEmitter
{
private array $items = [];
private float $total = 0.0;
public function addItem(string $sku, float $price, int $qty = 1): void
{
$this->items[] = compact('sku', 'price', 'qty');
$this->total += $price * $qty;
$this->notify('item.added', ['sku' => $sku, 'price' => $price, 'qty' => $qty]);
}
public function checkout(string $customerEmail): void
{
$this->notify('cart.checkout', [
'email' => $customerEmail,
'items' => $this->items,
'total' => $this->total,
]);
}
public function getTotal(): float { return $this->total; }
}
// Konkretni obserwatorzy - każdy reaguje inaczej
class InventoryObserver implements ObserverInterface
{
public function update(string $event, mixed $data): void
{
if ($event === 'item.added') {
echo "Rezerwacja: {$data['qty']}x {$data['sku']} w magazynie\n";
}
}
}
class AnalyticsObserver implements ObserverInterface
{
private array $events = [];
public function update(string $event, mixed $data): void
{
$this->events[] = ['event' => $event, 'data' => $data, 'ts' => time()];
echo "Analytics: zdarzenie '{$event}' zapisane\n";
}
public function getEvents(): array { return $this->events; }
}
class EmailObserver implements ObserverInterface
{
public function update(string $event, mixed $data): void
{
if ($event === 'cart.checkout') {
echo "Email do {$data['email']}: potwierdzenie zamówienia (total: {$data['total']} PLN)\n";
}
}
}
// Użycie
$cart = new ShoppingCart();
$inventory = new InventoryObserver();
$analytics = new AnalyticsObserver();
$email = new EmailObserver();
$cart->subscribe('item.added', $inventory);
$cart->subscribe('item.added', $analytics);
$cart->subscribe('cart.checkout', $analytics);
$cart->subscribe('cart.checkout', $email);
$cart->addItem('SKU-001', 29.99, 2);
$cart->addItem('SKU-002', 9.99);
$cart->checkout('jan@example.com');
PHP ma wbudowane interfejsy SplObserver i SplSubject które implementują ten sam wzorzec:
<?php
// Wbudowany Observer w PHP SPL
class StockPrice implements \SplSubject
{
private \SplObjectStorage $observers;
private float $price;
public function __construct(float $initialPrice)
{
$this->observers = new \SplObjectStorage();
$this->price = $initialPrice;
}
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 setPrice(float $price): void
{
$this->price = $price;
$this->notify();
}
public function getPrice(): float { return $this->price; }
}
Strategy
Strategy definiuje rodzinę wymiennych algorytmów, enkapsuluje każdy z nich i sprawia że są wymienne. Klient może zmieniać algorytm w runtime.
<?php
declare(strict_types=1);
// Interfejs strategii - jeden kontrakt dla wszystkich algorytmów
interface SortStrategyInterface
{
/**
* @param array<int|float|string> $data
* @return array<int|float|string>
*/
public function sort(array $data): array;
public function getName(): string;
}
// Konkretne strategie - każda implementuje inny algorytm
class BubbleSortStrategy implements SortStrategyInterface
{
public function sort(array $data): array
{
$n = count($data);
for ($i = 0; $i < $n - 1; $i++) {
for ($j = 0; $j < $n - $i - 1; $j++) {
if ($data[$j] > $data[$j + 1]) {
[$data[$j], $data[$j + 1]] = [$data[$j + 1], $data[$j]];
}
}
}
return $data;
}
public function getName(): string { return 'Bubble Sort O(n²)'; }
}
class QuickSortStrategy implements SortStrategyInterface
{
public function sort(array $data): array
{
if (count($data) <= 1) {
return $data;
}
$pivot = $data[0];
$left = array_filter(array_slice($data, 1), fn($x) => $x <= $pivot);
$right = array_filter(array_slice($data, 1), fn($x) => $x > $pivot);
return [...$this->sort(array_values($left)), $pivot, ...$this->sort(array_values($right))];
}
public function getName(): string { return 'Quick Sort O(n log n)'; }
}
class NativeSortStrategy implements SortStrategyInterface
{
public function sort(array $data): array
{
sort($data);
return $data;
}
public function getName(): string { return 'PHP sort() - Timsort'; }
}
// Kontekst - korzysta ze strategii nie wiedząc co to za algorytm
class DataSorter
{
private SortStrategyInterface $strategy;
public function __construct(SortStrategyInterface $strategy)
{
$this->strategy = $strategy;
}
public function setStrategy(SortStrategyInterface $strategy): void
{
$this->strategy = $strategy;
}
public function sort(array $data): array
{
echo "Sortuję " . count($data) . " elementów używając: {$this->strategy->getName()}\n";
return $this->strategy->sort($data);
}
}
// Podmiana strategii w runtime
$sorter = new DataSorter(new NativeSortStrategy());
$data = [64, 34, 25, 12, 22, 11, 90];
$result = $sorter->sort($data);
// Zmień strategię bez modyfikacji DataSorter
$sorter->setStrategy(new BubbleSortStrategy());
$result = $sorter->sort($data);
Praktyczny przykład – strategia cenowa w sklepie:
<?php
declare(strict_types=1);
interface PricingStrategyInterface
{
public function calculatePrice(float $basePrice, int $quantity): float;
}
class RegularPricingStrategy implements PricingStrategyInterface
{
public function calculatePrice(float $basePrice, int $quantity): float
{
return $basePrice * $quantity;
}
}
class BulkDiscountStrategy implements PricingStrategyInterface
{
public function __construct(
private int $minQuantity,
private float $discountPercent
) {}
public function calculatePrice(float $basePrice, int $quantity): float
{
$total = $basePrice * $quantity;
if ($quantity >= $this->minQuantity) {
$total *= (1 - $this->discountPercent / 100);
}
return $total;
}
}
class SeasonalSaleStrategy implements PricingStrategyInterface
{
public function __construct(
private float $salePercent
) {}
public function calculatePrice(float $basePrice, int $quantity): float
{
return $basePrice * (1 - $this->salePercent / 100) * $quantity;
}
}
// Wybór strategii dynamicznie - np. na podstawie konfiguracji w bazie
function getPricingStrategy(string $type): PricingStrategyInterface
{
return match($type) {
'bulk' => new BulkDiscountStrategy(minQuantity: 10, discountPercent: 15),
'seasonal' => new SeasonalSaleStrategy(salePercent: 20),
default => new RegularPricingStrategy(),
};
}
$strategy = getPricingStrategy('bulk');
$price = $strategy->calculatePrice(basePrice: 10.0, quantity: 15);
echo "Cena: {$price} PLN\n"; // 127.50 PLN (15% rabat)
Observer vs Strategy – kiedy który?
| Aspekt | Observer | Strategy |
|---|---|---|
| Relacja | Jeden Subject, wiele Observerów | Jeden Context, jedna aktywna Strategy |
| Komunikacja | Subject powiadamia Observerów o zdarzeniu | Context deleguje algorytm do Strategy |
| Typowy problem | Reagowanie na zdarzenia bez twardego sprzężenia | Wymiana algorytmu bez zmiany kodu klienta |
| Liczba „graczy” | Jeden-do-wielu | Jeden-do-jednego (aktywna strategia) |
Podsumowanie
Observer i Strategy to wzorce które naturalnie wchodzą do kodu gdy zaczynacie myśleć w kategoriach SOLID. Observer eliminuje twarde sprzężenia przy obsłudze zdarzeń – zamiast wołać bezpośrednio EmailService z CartService, emitujesz zdarzenie i każdy kto chce może zareagować. Strategy eliminuje rosnące if-else przy wymiennych algorytmach – nowa strategia cenowa to nowa klasa, nie modyfikacja istniejącej logiki.
