Challenge - Design patterns
Build a flexible notification system with Factory, Strategy and Observer.
Notification system: three patterns
In this lesson you write a flexible notification system. The Factory pattern centralizes object creation in NotificationFactory::make($type); the client requests a channel without knowing the concrete class. The Strategy pattern exposes every channel through the same NotificationChannel interface; EmailChannel and SmsChannel are interchangeable.
The Observer pattern uses the NotificationObserver interface and an attach() method for event delivery. NotificationService::notify() builds a channel, sends the message and informs all observers of the result. Channel switching and logging are then handled from one place.
Observer pattern
The Observer pattern has a subject and listeners that subscribe to it. NotificationService is the subject; LogObserver is a listener. attach(NotificationObserver $o) adds a listener to an array. When notify() runs, each listener's onSent(string $log) method fires.
So when a notification is sent you can add side effects like logging, analytics or email without changing NotificationService. You just write a new listener and attach it. The open-closed principle is satisfied: the class is closed for modification, open for extension.
Exercise Write NotificationFactory and NotificationService
The starting code has the interfaces and channel classes (EmailChannel, SmsChannel, LogObserver) ready. You write the NotificationFactory and NotificationService classes.
The static NotificationFactory::make(string $type): NotificationChannel method uses match to return EmailChannel for "email" and SmsChannel for "sms"; the default arm throws InvalidArgumentException. The NotificationService class holds a private array $observers = []; attach(NotificationObserver $o): void adds a listener; notify(string $type, string $message): string first builds a channel via NotificationFactory::make, calls send(), then runs each observer's onSent($result) and returns $result.
Show example solution
NotificationChannel { return match($type) { "email" => new EmailChannel(), "sms" => new SmsChannel(), default => throw new InvalidArgumentException(...) }; } }. Write class NotificationService { private array $observers = []; public function attach(NotificationObserver $o): void { ... } public function notify(string $type, string $message): string { $channel = NotificationFactory::make($type); $result = $channel->send($message); foreach ($this->observers as $o) { $o->onSent($result); } return $result; } }. Then run the demo.
Example solution:
<?php
// Product API + security + notifications: write NotificationFactory and NotificationService
class ProductRepository {
private PDO $pdo;
public function __construct(PDO $pdo) {
$this->pdo = $pdo;
$this->pdo->exec("CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT, price INTEGER)");
$stmt = $this->pdo->prepare("INSERT INTO products (name, price) VALUES (?, ?)");
$stmt->execute(["Kale", 5]);
$stmt->execute(["Defter", 15]);
}
public function all(): array {
$rows = $this->pdo->query("SELECT id, name, price FROM products ORDER BY id")->fetchAll(PDO::FETCH_ASSOC);
return array_map(fn($r) => [
"id" => (int)$r["id"],
"name" => $r["name"],
"price" => (int)$r["price"]
], $rows);
}
}
function handleGet(PDO $pdo): string {
$repo = new ProductRepository($pdo);
return json_encode(["products" => $repo->all()], JSON_UNESCAPED_UNICODE);
}
class AuthService {
private array $users = [];
private string $csrf = "";
public function register(string $user, string $pass): void {
$this->users[$user] = password_hash($pass, PASSWORD_DEFAULT);
}
public function login(string $user, string $pass): bool {
if (!isset($this->users[$user])) return false;
return password_verify($pass, $this->users[$user]);
}
public function csrfToken(): string {
if ($this->csrf === "") {
$this->csrf = bin2hex(random_bytes(8));
}
return $this->csrf;
}
public function applyHeaders(): void {
header("X-Frame-Options: DENY");
header("X-Content-Type-Options: nosniff");
}
}
interface NotificationChannel {
public function send(string $message): string;
}
class EmailChannel implements NotificationChannel {
public function send(string $message): string {
return "Email: " . $message;
}
}
class SmsChannel implements NotificationChannel {
public function send(string $message): string {
return "SMS: " . $message;
}
}
interface NotificationObserver {
public function onSent(string $log): void;
}
class LogObserver implements NotificationObserver {
public array $logs = [];
public function onSent(string $log): void {
$this->logs[] = $log;
}
class NotificationFactory {
public static function make(string $type): NotificationChannel {
return match ($type) {
"email" => new EmailChannel(),
"sms" => new SmsChannel(),
default => throw new InvalidArgumentException("Bilinmeyen kanal"),
};
}
}
class NotificationService {
private array $observers = [];
public function attach(NotificationObserver $o): void {
$this->observers[] = $o;
}
public function notify(string $type, string $message): string {
$channel = NotificationFactory::make($type);
$result = $channel->send($message);
foreach ($this->observers as $o) {
$o->onSent($result);
}
return $result;
}
}
// Demo:
$svc = new NotificationService();
$logger = new LogObserver();
$svc->attach($logger);
echo $svc->notify("email", "Order received") . "\n";
echo $svc->notify("sms", "Shipment dispatched") . "\n";
echo "Log count: " . count($logger->logs) . "\n";
Test yourself
-
Which design pattern centralizes object creation?
- Observer
- Strategy
- Singleton
- Factory
Show answer
Factory — Correct. The Factory pattern centralizes object creation; the client does not know which class is instantiated.
-
Complete the NotificationChannel interface
interface NotificationChannel { public function ____(string $message): string; }Show answer
interface NotificationChannel { public function send(string $message): string; } -
Match design patterns
- Factory
- Centralizes object creation
- Strategy
- Selects behavior at runtime
- Observer
- Notifies event subscribers
- Interface
- Defines a contract