代理模式(Proxy)
前言
代理模式为其他对象提供一种代理以控制对这个对象的访问。它在不改变原始对象接口的前提下,通过代理对象增强或控制对原对象的访问。本文将详细讲解代理模式的核心原理、多种代理类型及实际应用。
一、核心概念
1.1 定义
为其他对象提供一种代理以控制对这个对象的访问。
1.2 核心角色
| 角色 | 说明 |
|---|---|
| 主题接口(Subject) | 定义真实主题和代理的共同接口 |
| 真实主题(RealSubject) | 实际执行业务逻辑的对象 |
| 代理(Proxy) | 控制对真实主题的访问 |
1.3 代理类型
| 类型 | 说明 | 场景 |
|---|---|---|
| 远程代理 | 为远程对象提供本地代表 | RPC 调用 |
| 虚拟代理 | 延迟创建开销大的对象 | 图片懒加载 |
| 保护代理 | 控制访问权限 | 权限校验 |
| 智能引用 | 在访问时附加操作 | 引用计数、日志 |
| 缓存代理 | 缓存结果 | API 请求缓存 |
二、代码实现
2.1 虚拟代理(延迟加载)
php
<?php
// 主题接口
interface Image
{
public function display(): void;
}
// 真实主题:大图片加载
class RealImage implements Image
{
private string $filename;
public function __construct(string $filename)
{
$this->filename = $filename;
$this->loadFromDisk();
}
private function loadFromDisk(): void
{
echo "Loading {$this->filename} from disk..." . PHP_EOL;
}
public function display(): void
{
echo "Displaying {$this->filename}" . PHP_EOL;
}
}
// 虚拟代理:延迟加载图片
class ProxyImage implements Image
{
private ?RealImage $realImage = null;
private string $filename;
public function __construct(string $filename)
{
$this->filename = $filename;
}
public function display(): void
{
// 只有真正需要显示时才加载
if ($this->realImage === null) {
$this->realImage = new RealImage($this->filename);
}
$this->realImage->display();
}
}
// 使用
$image = new ProxyImage("photo.jpg");
echo "Image created but not loaded yet" . PHP_EOL;
$image->display(); // 此时才加载
$image->display(); // 复用已加载实例2.2 保护代理(权限控制)
php
<?php
interface Document
{
public function read(): string;
public function write(string $content): void;
}
class RealDocument implements Document
{
private string $content;
public function __construct(string $content)
{
$this->content = $content;
}
public function read(): string
{
return $this->content;
}
public function write(string $content): void
{
$this->content = $content;
}
}
class ProtectedDocument implements Document
{
private RealDocument $realDocument;
private string $userRole;
public function __construct(RealDocument $realDocument, string $userRole)
{
$this->realDocument = $realDocument;
$this->userRole = $userRole;
}
public function read(): string
{
echo "[Audit] {$this->userRole} read document" . PHP_EOL;
return $this->realDocument->read();
}
public function write(string $content): void
{
if (!in_array($this->userRole, ['admin', 'editor'], true)) {
throw new RuntimeException("Permission denied: write requires admin or editor role");
}
echo "[Audit] {$this->userRole} wrote to document" . PHP_EOL;
$this->realDocument->write($content);
}
}
// 使用
$doc = new ProtectedDocument(new RealDocument("Hello"), "viewer");
echo $doc->read() . PHP_EOL; // 允许读取
$doc->write("World"); // 报错:Permission denied2.3 缓存代理
php
<?php
interface DataService
{
public function getData(string $key): string;
}
class RemoteDataService implements DataService
{
public function getData(string $key): string
{
echo "Fetching {$key} from remote server..." . PHP_EOL;
return "Data for {$key}";
}
}
class CachingProxy implements DataService
{
private RemoteDataService $realService;
private array $cache = [];
private int $ttl;
public function __construct(RemoteDataService $realService, int $ttl = 60)
{
$this->realService = $realService;
$this->ttl = $ttl;
}
public function getData(string $key): string
{
$now = time();
if (isset($this->cache[$key]) && $this->cache[$key]['expiry'] > $now) {
echo "Cache hit for {$key}" . PHP_EOL;
return $this->cache[$key]['value'];
}
$value = $this->realService->getData($key);
$this->cache[$key] = ['value' => $value, 'expiry' => $now + $this->ttl];
return $value;
}
}
// 使用
$service = new CachingProxy(new RemoteDataService());
$service->getData("user:1"); // 从远程获取
$service->getData("user:1"); // 从缓存获取2.4 智能引用代理
php
<?php
class SmartReferenceProxy implements Image
{
private ?RealImage $realImage = null;
private string $filename;
private int $accessCount = 0;
public function __construct(string $filename)
{
$this->filename = $filename;
}
public function display(): void
{
$this->accessCount++;
echo "[Access count: {$this->accessCount}]" . PHP_EOL;
if ($this->realImage === null) {
$this->realImage = new RealImage($this->filename);
}
$this->realImage->display();
}
}三、适用场景
| 场景 | 代理类型 | 说明 |
|---|---|---|
| 图片懒加载 | 虚拟代理 | 延迟加载大资源 |
| 权限校验 | 保护代理 | 控制访问权限 |
| API 缓存 | 缓存代理 | 减少网络请求 |
| 日志记录 | 智能引用 | 记录访问行为 |
| 远程调用 | 远程代理 | 封装网络通信 |
| 资源管理 | 智能引用 | 引用计数 |
四、优缺点分析
| 优点 | 缺点 |
|---|---|
| 控制对象访问 | 增加代理层 |
| 延迟加载节省资源 | 可能影响性能 |
| 权限控制 | 代码复杂度增加 |
| 附加功能(日志/缓存) | 代理类需要维护 |
| 符合开闭原则 | 响应可能延迟 |
五、常见踩坑与问题排查
5.1 代理与装饰器混淆
php
<?php
// 装饰器:增强功能,关注"添加什么"
// 代理:控制访问,关注"是否允许"
// 区别:代理通常由框架控制,装饰器由客户端组合5.2 代理层过多影响性能
php
<?php
// 问题:多层代理叠加
$service = new LoggingProxy(
new CachingProxy(new ProtectedProxy(new RemoteDataService()))
);
// 解决:合理规划代理层级,合并部分功能六、优化方案与进阶
6.1 动态代理(基于反射)
php
<?php
// 通过反射动态代理对象方法
class DynamicProxy
{
private object $target;
private array $interceptors = [];
public function __construct(object $target)
{
$this->target = $target;
}
public function addInterceptor(string $method, callable $fn): void
{
$this->interceptors[$method] = $fn;
}
public function __call(string $name, array $arguments): mixed
{
if (isset($this->interceptors[$name])) {
echo "[Access] Calling {$name}" . PHP_EOL;
}
return call_user_func_array([$this->target, $name], $arguments);
}
}
class UserService
{
public function getUser(int $id): array
{
return ['id' => $id, 'name' => 'Flynn'];
}
}
$proxy = new DynamicProxy(new UserService());
$proxy->addInterceptor('getUser', fn() => null);
$user = $proxy->getUser(1);
// [Access] Calling getUser6.2 拦截器模式
php
<?php
class ApiClient
{
private string $baseURL;
public function __construct(string $baseURL)
{
$this->baseURL = $baseURL;
}
// 代理 cURL 请求
public function request(string $url, array $options = []): string
{
$fullUrl = "{$this->baseURL}{$url}";
$ch = curl_init($fullUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
// 前置拦截:添加 token
$token = $_SESSION['token'] ?? null;
if ($token) {
$headers = $options['headers'] ?? [];
$headers[] = "Authorization: Bearer {$token}";
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
}
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
// 后置拦截:处理 401
if ($httpCode === 401) {
unset($_SESSION['token']);
header('Location: /login');
exit;
}
return $response;
}
}七、全文总结
代理模式的核心是 通过代理对象控制对原对象的访问,提供额外功能。
核心要点:
- 代理和真实对象实现相同接口
- 五种代理类型:远程、虚拟、保护、智能引用、缓存
- PHP 可通过
__call魔术方法实现动态代理 - 适用于懒加载、权限控制、缓存、日志等场景
- 避免代理层过多,合理规划功能分层
