概述
代理模式(Proxy Pattern)是一种结构型设计模式,其核心思想是为目标对象提供一个代理对象,通过代理对象控制对目标对象的访问。代理模式在不改变目标对象接口和核心逻辑的前提下,可在访问前后添加额外操作(如权限校验、日志记录、缓存处理、延迟加载),实现对目标对象的增强和控制。
该模式遵循“单一职责原则”,将目标对象的核心业务逻辑与附加功能(如监控、控制)分离,代理对象专注于访问控制,目标对象专注于业务实现,同时支持动态切换代理类型,提升系统的灵活性和可维护性。
核心价值
| 核心要点 | 详细说明 |
|---|---|
| 访问控制与增强 | 代理对象可在调用目标对象前后添加额外逻辑(如权限校验、日志记录、缓存拦截),无需修改目标对象代码 |
| 延迟加载优化 | 对于创建成本高的目标对象(如大文件、数据库连接、远程服务),可通过代理实现懒加载,仅在真正需要时创建目标对象 |
| 解耦核心与附加逻辑 | 目标对象仅关注核心业务,附加功能(如监控、统计)封装在代理中,职责边界清晰,便于维护和扩展 |
| 保护目标对象 | 代理可屏蔽目标对象的直接访问,防止外部直接操作目标对象导致的风险(如敏感数据泄露、非法调用) |
| 灵活扩展 | 可通过新增代理类扩展不同的附加功能(如日志代理、缓存代理、权限代理),无需修改目标对象和客户端代码 |
适用场景
| 场景类型 | 具体说明 |
|---|---|
| 延迟加载场景 | 如大文件读取、远程服务调用、数据库连接创建等,目标对象创建耗时或消耗资源大,需按需加载 |
| 访问控制场景 | 如接口权限校验、用户身份验证、敏感操作拦截等,需在调用目标对象前验证访问合法性 |
| 附加功能增强场景 | 如操作日志记录、性能监控统计、数据缓存处理、事务管理等,需在不修改目标对象的前提下添加附加功能 |
| 远程服务代理场景 | 如调用第三方API、分布式服务通信等,代理可封装网络请求、参数序列化、异常处理等细节,客户端透明调用 |
| 保护目标对象场景 | 如核心业务组件、敏感数据操作类等,需防止外部直接访问目标对象,通过代理控制访问方式 |
注意事项
| 注意点 | 具体说明 |
|---|---|
| 确保接口一致性 | 代理类与目标对象必须实现同一接口,否则客户端无法透明使用代理对象,破坏模式核心特性 |
| 避免过度代理 | 若目标对象简单且无需附加功能,直接调用目标对象更高效,无需引入代理模式增加系统复杂度 |
| 控制代理层级 | 避免多层代理嵌套(如日志代理→缓存代理→权限代理),否则会增加调用链路长度,影响系统性能 |
| 不篡改核心逻辑 | 代理仅负责访问控制和附加功能,不得修改目标对象的核心业务逻辑和返回结果,确保业务一致性 |
| 区分代理类型 | 根据场景选择合适的代理类型(如静态代理、动态代理),静态代理适用于简单场景,动态代理(如PHP的__call魔术方法、反射)适用于复杂场景 |
完整代码
以“商品数据查询”为业务场景,目标对象为数据库查询服务,通过代理实现“缓存代理”(查询结果缓存)和“日志代理”(操作日志记录),控制对目标对象的访问并增强功能。
<?php
declare(strict_types = 1);
/**
* 目标接口:定义商品查询的统一规范
*/
interface ProductServiceInterface
{
/**
* 根据商品ID查询商品信息
* @param int $productId 商品ID
* @return array 商品信息
*/
public function getProductById(int $productId): array;
/**
* 根据商品分类查询商品列表
* @param string $category 商品分类
* @return array 商品列表
*/
public function getProductsByCategory(string $category): array;
}
/**
* 目标对象:商品数据库查询服务(核心业务逻辑)
*/
class ProductDatabaseService implements ProductServiceInterface
{
/**
* 模拟数据库查询,创建成本高(如建立数据库连接、复杂查询)
*/
public function getProductById(int $productId): array
{
// 模拟数据库查询耗时操作
sleep(1);
echo "【数据库查询】查询商品ID:{$productId}的信息" . PHP_EOL;
return [
'id' => $productId,
'name' => "商品{$productId}(数据库查询)",
'price' => rand(100, 1000),
'category' => '电子设备'
];
}
public function getProductsByCategory(string $category): array
{
// 模拟数据库查询耗时操作
sleep(1);
echo "【数据库查询】查询分类:{$category}的商品列表" . PHP_EOL;
return [
['id' => 101, 'name' => "商品101({$category})", 'price' => 599],
['id' => 102, 'name' => "商品102({$category})", 'price' => 799],
['id' => 103, 'name' => "商品103({$category})", 'price' => 999]
];
}
}
/**
* 代理类1:缓存代理(附加缓存功能,减少数据库查询)
*/
class ProductCacheProxy implements ProductServiceInterface
{
/**
* 持有目标对象引用
* @var ProductServiceInterface
*/
private ProductServiceInterface $target;
/**
* 缓存存储(内存缓存,实际可使用Redis等)
* @var array
*/
private array $cache = [];
/**
* 缓存过期时间(秒)
* @var int
*/
private int $cacheExpire = 300;
public function __construct(ProductServiceInterface $target)
{
$this->target = $target;
}
public function getProductById(int $productId): array
{
$cacheKey = "product:id:{$productId}";
// 1. 先查询缓存
if (isset($this->cache[$cacheKey]) && time() - $this->cache[$cacheKey]['time'] < $this->cacheExpire) {
echo "【缓存命中】从缓存中获取商品ID:{$productId}的信息" . PHP_EOL;
return $this->cache[$cacheKey]['data'];
}
// 2. 缓存未命中,调用目标对象查询
$data = $this->target->getProductById($productId);
// 3. 存入缓存
$this->cache[$cacheKey] = [
'data' => $data,
'time' => time()
];
echo "【缓存存储】将商品ID:{$productId}的信息存入缓存" . PHP_EOL;
return $data;
}
public function getProductsByCategory(string $category): array
{
$cacheKey = "product:category:{$category}";
// 1. 先查询缓存
if (isset($this->cache[$cacheKey]) && time() - $this->cache[$cacheKey]['time'] < $this->cacheExpire) {
echo "【缓存命中】从缓存中获取分类:{$category}的商品列表" . PHP_EOL;
return $this->cache[$cacheKey]['data'];
}
// 2. 缓存未命中,调用目标对象查询
$data = $this->target->getProductsByCategory($category);
// 3. 存入缓存
$this->cache[$cacheKey] = [
'data' => $data,
'time' => time()
];
echo "【缓存存储】将分类:{$category}的商品列表存入缓存" . PHP_EOL;
return $data;
}
}
/**
* 代理类2:日志代理(附加日志记录功能,监控操作)
*/
class ProductLogProxy implements ProductServiceInterface
{
/**
* 持有目标对象引用(可嵌套其他代理)
* @var ProductServiceInterface
*/
private ProductServiceInterface $target;
public function __construct(ProductServiceInterface $target)
{
$this->target = $target;
}
public function getProductById(int $productId): array
{
// 1. 调用前记录日志(请求信息)
$this->log("开始查询商品ID:{$productId}的信息", 'info');
try {
// 2. 调用目标对象(或嵌套的其他代理)
$data = $this->target->getProductById($productId);
// 3. 调用后记录日志(响应信息)
$this->log("商品ID:{$productId}查询成功,结果:" . json_encode($data, JSON_UNESCAPED_UNICODE), 'info');
return $data;
} catch (\Exception $e) {
// 4. 异常时记录错误日志
$this->log("商品ID:{$productId}查询失败,错误:" . $e->getMessage(), 'error');
throw $e;
}
}
public function getProductsByCategory(string $category): array
{
// 1. 调用前记录日志
$this->log("开始查询分类:{$category}的商品列表", 'info');
try {
// 2. 调用目标对象
$data = $this->target->getProductsByCategory($category);
// 3. 调用后记录日志
$this->log("分类:{$category}查询成功,商品数量:" . count($data), 'info');
return $data;
} catch (\Exception $e) {
// 4. 异常日志
$this->log("分类:{$category}查询失败,错误:" . $e->getMessage(), 'error');
throw $e;
}
}
/**
* 模拟日志记录(实际可写入文件、数据库或日志系统)
* @param string $message 日志内容
* @param string $level 日志级别
*/
private function log(string $message, string $level = 'info'): void
{
$time = date('Y-m-d H:i:s');
echo "【{$time}】【{$level}】{$message}" . PHP_EOL;
}
}
// 测试代码
try {
echo "=== 测试1:直接调用目标对象(无代理) ===" . PHP_EOL;
$productService = new ProductDatabaseService();
$productService->getProductById(1001); // 耗时1秒,无缓存和日志
echo PHP_EOL;
echo "=== 测试2:缓存代理 + 日志代理(嵌套代理) ===" . PHP_EOL;
// 嵌套代理:日志代理包裹缓存代理,缓存代理包裹目标对象
$target = new ProductDatabaseService();
$cacheProxy = new ProductCacheProxy($target);
$logProxy = new ProductLogProxy($cacheProxy);
// 第一次查询:缓存未命中,走数据库查询,记录日志并缓存
$logProxy->getProductById(1001);
echo PHP_EOL;
// 第二次查询:缓存命中,直接从缓存获取,记录日志
$logProxy->getProductById(1001);
echo PHP_EOL;
// 测试分类查询
$logProxy->getProductsByCategory('电子设备');
} catch (\Exception $e) {
echo "错误:" . $e->getMessage();
}
核心结构拆解
代理模式的核心由“目标接口”“目标对象”和“代理对象”三部分组成,结构清晰且职责明确:
| 结构组成 | 详细说明 |
|---|---|
| 目标接口(ProductServiceInterface) | 定义目标对象和代理对象的统一行为规范(如 getProductById getProductsByCategory),确保客户端可透明调用 |
| 目标对象(ProductDatabaseService) | 实现目标接口,封装核心业务逻辑(如数据库查询),是代理对象的代理目标,不包含任何附加功能 |
| 代理对象(ProductCacheProxy、ProductLogProxy 等) | 实现目标接口,持有目标对象的引用,在调用目标对象方法前后添加附加逻辑(如缓存、日志),并转发调用给目标对象 |
关键优化点
| 优化方向 | 详细说明 |
|---|---|
| 接口统一约束 | 目标接口严格定义方法签名(参数类型、返回值类型),确保代理对象与目标对象的方法一致性,客户端无需区分调用对象 |
| 支持代理嵌套 | 代理对象可嵌套其他代理(如日志代理包裹缓存代理),实现多种附加功能的组合,且不影响客户端调用逻辑 |
| 懒加载优化 | 缓存代理中实现延迟查询,仅在缓存未命中时调用目标对象,减少不必要的资源消耗(如数据库连接、网络请求) |
| 无侵入增强 | 目标对象无需任何修改即可被代理增强,附加功能(缓存、日志)封装在代理中,完全遵循“开放-封闭原则” |
| 职责单一明确 | 每个代理仅负责一项附加功能(缓存代理专注缓存,日志代理专注日志),符合“单一职责原则”,便于维护和扩展 |
常见错误规避
| 错误类型 | 问题描述及解决方案 |
|---|---|
| 代理类未实现目标接口 | 问题:代理类未遵循统一接口,客户端无法透明使用代理对象,导致类型错误或调用异常。 解决:所有代理类必须实现目标接口,确保方法签名与目标对象完全一致 |
| 代理直接修改目标对象逻辑 | 问题:代理类在转发调用时篡改目标对象的参数或返回结果,导致业务逻辑异常。 解决:代理仅添加附加功能,不修改目标对象的输入参数和核心返回结果,确保业务一致性 |
| 客户端直接依赖代理实现 | 问题:客户端代码直接引用具体代理类(如 new ProductCacheProxy()),导致切换代理类型时需修改客户端代码。解决:客户端应依赖目标接口而非具体代理类,可通过工厂模式创建代理实例,降低耦合 |
| 多层代理导致性能损耗 | 问题:过多代理嵌套(如日志→缓存→权限→事务)导致调用链路过长,系统响应变慢。 解决:合理控制代理层级(建议不超过2层),将关联紧密的附加功能合并为一个代理,减少调用开销 |
| 静态代理扩展困难 | 问题:静态代理需为每个目标接口创建对应代理类,若目标接口方法过多,代理类代码冗余。 解决:复杂场景可使用动态代理(如通过PHP的 __call魔术方法、反射机制),自动适配目标接口,减少重复编码 |
扩展场景示例(新增代理无需修改原有代码)
若业务新增“权限代理”(验证用户是否有权限查询商品),仅需新增代理类,无需修改目标对象、已有代理和客户端代码:
// 新增代理类:权限代理(验证用户权限)
class ProductAuthProxy implements ProductServiceInterface
{
private ProductServiceInterface $target;
private array $allowedRoles = ['admin', 'editor', 'viewer']; // 允许访问的角色
public function __construct(ProductServiceInterface $target)
{
$this->target = $target;
}
public function getProductById(int $productId): array
{
// 1. 权限校验(模拟当前用户角色)
$currentRole = $this->getCurrentUserRole();
if (!in_array($currentRole, $this->allowedRoles, true)) {
throw new \RuntimeException("用户角色【{$currentRole}】无权限查询商品ID:{$productId}");
}
// 2. 权限通过,转发调用
echo "【权限校验通过】用户角色:{$currentRole} 允许查询商品ID:{$productId}" . PHP_EOL;
return $this->target->getProductById($productId);
}
public function getProductsByCategory(string $category): array
{
// 1. 权限校验
$currentRole = $this->getCurrentUserRole();
if (!in_array($currentRole, $this->allowedRoles, true)) {
throw new \RuntimeException("用户角色【{$currentRole}】无权限查询分类:{$category}的商品");
}
// 2. 转发调用
echo "【权限校验通过】用户角色:{$currentRole} 允许查询分类:{$category}" . PHP_EOL;
return $this->target->getProductsByCategory($category);
}
/**
* 模拟获取当前用户角色(实际可从会话、Token中获取)
* @return string
*/
private function getCurrentUserRole(): string
{
// 模拟不同用户角色,可切换测试
return 'editor'; // 可选:'admin'/'editor'/'viewer'/'guest'
}
}
// 测试新增权限代理(嵌套多层代理)
echo "=== 测试3:权限代理 + 日志代理 + 缓存代理 ===" . PHP_EOL;
$target = new ProductDatabaseService();
$cacheProxy = new ProductCacheProxy($target);
$logProxy = new ProductLogProxy($cacheProxy);
$authProxy = new ProductAuthProxy($logProxy);
$authProxy->getProductById(1001); // 权限校验→日志→缓存→数据库(缓存命中)
echo PHP_EOL;
// 测试无权限场景(修改getCurrentUserRole返回'guest')
// $authProxy->getProductsByCategory('电子设备'); // 抛出无权限异常
实际应用场景
| 场景分类 | 具体说明 |
|---|---|
| 缓存代理应用 | 如API接口缓存、数据库查询结果缓存、静态资源缓存等,代理拦截重复请求,直接返回缓存结果,提升系统性能 |
| 日志代理应用 | 如接口访问日志、操作审计日志、性能监控日志等,代理记录请求参数、响应结果、耗时等信息,便于问题排查和系统优化 |
| 权限代理应用 | 如后台管理系统接口、敏感数据查询接口等,代理验证用户身份、角色权限,防止非法访问 |
| 远程代理应用 | 如调用第三方支付API、天气接口、地图服务等,代理封装网络请求、参数序列化、异常重试等细节,客户端透明调用 |
| 延迟加载应用 | 如电商平台的商品详情页、大文件下载、视频播放等,代理仅在用户需要时加载目标资源,减少初始加载时间和资源消耗 |
代理模式与其他模式对比
| 对比维度 | 代理模式 | 适配器模式 | 装饰器模式 |
|---|---|---|---|
| 核心目标 | 控制目标对象访问,添加附加功能 | 转换接口,解决兼容性问题 | 动态添加扩展功能,组合多个职责 |
| 对象关系 | 代理持有目标对象引用(代理关系) | 适配器组合原始类(适配关系) | 装饰器嵌套包装组件(包装关系) |
| 执行逻辑 | 代理转发调用,添加附加逻辑(不改变核心功能) | 适配器转换接口格式,转发调用 | 多层装饰器依次执行,叠加功能 |
| 接口一致性 | 代理与目标对象实现同一接口 | 适配器与目标接口一致,与原始类接口不同 | 装饰器与被装饰对象实现同一接口 |
| 适用场景 | 访问控制、延迟加载、附加功能增强 | 接口不兼容、第三方组件集成 | 功能动态扩展、多职责组合 |
| 核心差异 | 侧重“访问控制”,不改变目标对象功能 | 侧重“接口转换”,解决兼容问题 | 侧重“功能叠加”,增强目标对象功能 |


