策略模式 定義一系列演算法,將每個演算法封裝起來,並使它們可互換。本文以「快取儲存策略」為例,示範 File、Redis、APCu、Database 多種儲存後端的無縫切換。
<?php
// 1. 策略介面
interface CacheStrategy {
public function get(string $key): mixed;
public function set(string $key, mixed $value, int $ttl = 3600): bool;
public function delete(string $key): bool;
public function clear(): bool;
public function has(string $key): bool;
}
// 2. 檔案快取策略
class FileCacheStrategy implements CacheStrategy {
private string $cacheDir;
public function __construct(string $cacheDir = '/tmp/cache') {
$this->cacheDir = $cacheDir;
if (!is_dir($cacheDir)) mkdir($cacheDir, 0755, true);
}
private function filePath(string $key): string {
return $this->cacheDir . '/' . md5($key) . '.cache';
}
public function get(string $key): mixed {
$file = $this->filePath($key);
if (!file_exists($file)) return null;
$data = unserialize(file_get_contents($file));
if ($data['expire'] < time()) {
unlink($file);
return null;
}
return $data['value'];
}
public function set(string $key, mixed $value, int $ttl = 3600): bool {
$data = ['value' => $value, 'expire' => time() + $ttl];
return file_put_contents($this->filePath($key), serialize($data)) !== false;
}
public function delete(string $key): bool {
$file = $this->filePath($key);
return file_exists($file) ? unlink($file) : true;
}
public function clear(): bool {
$files = glob($this->cacheDir . '/*.cache');
foreach ($files as $file) unlink($file);
return true;
}
public function has(string $key): bool {
return $this->get($key) !== null;
}
}
// 3. Redis 快取策略
class RedisCacheStrategy implements CacheStrategy {
private Redis $redis;
private string $prefix;
public function __construct(Redis $redis, string $prefix = 'cache:') {
$this->redis = $redis;
$this->prefix = $prefix;
}
public function get(string $key): mixed {
$data = $this->redis->get($this->prefix . $key);
return $data ? unserialize($data) : null;
}
public function set(string $key, mixed $value, int $ttl = 3600): bool {
return $this->redis->setex($this->prefix . $key, $ttl, serialize($value));
}
public function delete(string $key): bool {
return $this->redis->del($this->prefix . $key) > 0;
}
public function clear(): bool {
$keys = $this->redis->keys($this->prefix . '*');
return $keys ? $this->redis->del(...$keys) > 0 : true;
}
public function has(string $key): bool {
return $this->redis->exists($this->prefix . $key);
}
}
// 4. APCu 快取策略 (單機高效能)
class ApcuCacheStrategy implements CacheStrategy {
private string $prefix;
public function __construct(string $prefix = 'cache:') {
if (!extension_loaded('apcu')) throw new RuntimeException('APCu extension required');
$this->prefix = $prefix;
}
public function get(string $key): mixed {
return apcu_fetch($this->prefix . $key);
}
public function set(string $key, mixed $value, int $ttl = 3600): bool {
return apcu_store($this->prefix . $key, $value, $ttl);
}
public function delete(string $key): bool {
return apcu_delete($this->prefix . $key);
}
public function clear(): bool {
// APCu 無前綴清除,需遍歷 (效能較差)
$iterator = new APCUIterator('/^' . preg_quote($this->prefix, '/') . '/');
foreach ($iterator as $key => $val) apcu_delete($key);
return true;
}
public function has(string $key): bool {
return apcu_exists($this->prefix . $key);
}
}
?>
<?php
class CacheManager {
private CacheStrategy $strategy;
public function __construct(CacheStrategy $strategy) {
$this->strategy = $strategy;
}
public function setStrategy(CacheStrategy $strategy): void {
$this->strategy = $strategy;
}
// 代理方法
public function get(string $key): mixed { return $this->strategy->get($key); }
public function set(string $key, mixed $value, int $ttl = 3600): bool { return $this->strategy->set($key, $value, $ttl); }
public function delete(string $key): bool { return $this->strategy->delete($key); }
public function clear(): bool { return $this->strategy->clear(); }
public function has(string $key): bool { return $this->strategy->has($key); }
// 進階:記憶體快取 + 持久化快取雙層
public function getWithFallback(string $key, CacheStrategy $fallback): mixed {
$value = $this->strategy->get($key);
if ($value === null) {
$value = $fallback->get($key);
if ($value !== null) {
$this->strategy->set($key, $value); // 回填熱點
}
}
return $value;
}
}
?>
<?php
class CacheStrategyFactory {
public static function create(string $driver, array $config = []): CacheStrategy {
return match($driver) {
'file' => new FileCacheStrategy($config['path'] ?? '/tmp/cache'),
'redis' => new RedisCacheStrategy(
(fn() => { $r = new Redis(); $r->connect($config['host']??'127.0.0.1', $config['port']??6379); return $r; })(),
$config['prefix'] ?? 'cache:'
),
'apcu' => new ApcuCacheStrategy($config['prefix'] ?? 'cache:'),
'database' => new DatabaseCacheStrategy($config['pdo']), // 需自行實作
'array' => new ArrayCacheStrategy(), // 單請求生命週期
default => throw new InvalidArgumentException("Unknown cache driver: $driver"),
};
}
}
// 配置檔案 config/cache.php
return [
'default' => env('CACHE_DRIVER', 'file'),
'stores' => [
'file' => ['driver' => 'file', 'path' => storage_path('framework/cache')],
'redis' => ['driver' => 'redis', 'host' => '127.0.0.1', 'port' => 6379],
'apcu' => ['driver' => 'apcu'],
],
];
// 應用啟動時
$config = require 'config/cache.php';
$cache = new CacheManager(CacheStrategyFactory::create($config['default'], $config['stores'][$config['default']] ?? []));
?>
<?php
// 1. 一般使用
$cache->set('user:1', ['name' => 'Alice', 'email' => 'alice@example.com'], 1800);
$user = $cache->get('user:1');
// 2. 運行時切換 (如 Redis 維護期間降級到檔案)
if ($redisDown) {
$cache->setStrategy(new FileCacheStrategy('/var/cache/fallback'));
}
// 3. 雙層快取:APCu (L1) + Redis (L2)
$l1 = new ApcuCacheStrategy('l1:');
$l2 = new RedisCacheStrategy($redis, 'l2:');
$value = $cache->getWithFallback('hot_key', $l2); // 先查 L1,無則查 L2 並回填 L1
// 4. 多策略組合:標籤式快取清除
interface TaggableCacheStrategy extends CacheStrategy {
public function setWithTags(string $key, mixed $value, array $tags, int $ttl = 3600): bool;
public function invalidateByTag(string $tag): bool;
}
// 實作需維護 tag->keys 映射
?>
<?php
// Laravel 契約
interface Store extends CacheStrategy { /* ... */ }
// 服務提供者註冊
class CacheServiceProvider extends ServiceProvider {
public function register(): void {
$this->app->singleton('cache', function($app) {
return new CacheManager($app);
});
}
}
// CacheManager 內建策略切換
$cache = app('cache');
$cache->store('redis')->put('key', 'value', 600);
$cache->store('file')->put('key', 'value', 600);
// 動態切換
$cache->extend('custom', fn($app) => new CustomCacheStrategy(...));
$cache->store('custom')->put('key', 'value');
?>
| 優點 | 缺點 |
|---|---|
| 演算法封裝隔離,易於單元測試 | 策略類別增多 (每個演算法一類) |
| 運行時動態切換,無需修改客戶端 | 客戶端需知道策略差異 (如 TTL 支援度) |
| 符合開放封閉原則:新增策略不修改既有代碼 | 簡單情況下引入過度設計 |
| 避免條件判斷 (if/else/switch) 散落業務邏輯 | 策略間共享狀態需額外機制 |