初识设计模式——代理模式(Proxy Pattern)

cuixiaogang

代理模式(Proxy Pattern)是一种结构型设计模式,它允许通过代理对象来控制对另一个对象(即目标对象)的访问。代理对象在客户端和目标对象之间起到中介的作用,客户端与目标对象之间的交互都通过代理对象来完成。代理模式可以在不改变目标对象的情况下,对目标对象的功能进行增强或控制访问。

代理模式的结构

  • 抽象主题(Subject):定义了目标对象和代理对象的共同接口,客户端通过这个接口来调用目标对象或代理对象的方法。
  • 真实主题(Real Subject):实现了抽象主题接口,是实际执行具体业务逻辑的对象。
  • 代理(Proxy):也实现了抽象主题接口,持有一个真实主题的引用。代理对象可以在调用真实主题的方法前后进行一些额外的操作,如权限检查、缓存处理等。

代理模式结构图

代理模式的主要应用场景

网络请求与 API 调用

  • 场景描述:在开发网络应用时,客户端与服务器交互频繁,直接进行网络请求可能会面临超时、异常处理等问题。使用代理模式可以封装网络请求细节,增强代码的可维护性和健壮性。
  • 具体示例:在一个电商 APP 里,商品数据的获取依赖服务器的 API。为避免网络请求的复杂性影响业务逻辑,可创建一个代理类。该代理类负责处理网络连接、请求参数封装、错误重试等操作。当 APP 需要获取商品列表时,只需调用代理类的相应方法,而无需关心底层网络请求的具体实现。

性能优化与缓存

  • 场景描述:对于一些计算开销大或者数据获取成本高的操作,频繁执行会严重影响系统性能。通过代理模式引入缓存机制,可显著提升系统性能。
  • 具体示例:在一个数据分析系统中,生成复杂报表需要对大量数据进行计算和处理,过程耗时较长。可以创建一个代理类,在首次请求报表数据时,代理类调用实际的报表生成服务并将结果缓存起来。后续再次请求相同报表时,代理类直接从缓存中获取数据,避免重复计算。

安全控制与权限管理

  • 场景描述:在企业级应用系统中,不同用户角色对系统资源的访问权限不同。使用代理模式可以在访问资源前进行权限验证,确保系统数据的安全性。
  • 具体示例:在一个企业内部的文件管理系统中,对于敏感文件的访问需要严格的权限控制。可以创建一个代理类,在用户尝试访问文件时,代理类先检查用户的权限。只有具有相应权限的用户才能通过代理类访问实际的文件资源,否则将拒绝访问请求。

延迟加载

  • 场景描述:在一些大型应用中,部分对象的创建和初始化过程较为复杂,会消耗大量系统资源。使用代理模式可以实现对象的延迟加载,提高系统的响应速度。
  • 具体示例:在一个图形设计软件中,打开一个包含大量图层和特效的文件时,若一次性加载所有内容会导致软件启动缓慢。可以创建一个代理对象来代表这些复杂的图层和特效。在用户真正需要查看或编辑某个图层时,代理对象才会去加载该图层的实际内容。

日志记录与监控

  • 场景描述:为了便于系统的维护和故障排查,需要对系统中某些关键操作进行日志记录和监控。代理模式可以在不影响原有业务逻辑的前提下,方便地添加日志记录和监控功能。
  • 具体示例:在一个在线支付系统中,用户的支付操作是关键业务流程。可以创建一个代理类,在用户发起支付请求时,代理类除了调用实际的支付服务完成支付操作外,还会记录支付的相关信息,如支付时间、支付金额、支付状态等。同时,代理类还可以对支付操作的执行时间进行监控,当支付操作耗时过长时,及时发出警报。

代码示例

下面以“性能优化与缓存”作为场景,来使用代理模式实现功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
<?php
// 抽象主题:定义计算接口
interface Calculator {
public function calculate($input);
}

// 真实主题:实现具体的计算逻辑
class RealCalculator implements Calculator {
public function calculate($input) {
// 模拟复杂的计算,这里简单地返回输入的平方
echo "正在进行复杂计算...\n";
return $input * $input;
}
}

// 代理类:添加缓存机制
class CacheProxyCalculator implements Calculator {
private $realCalculator;
private $cache = [];

public function __construct() {
$this->realCalculator = new RealCalculator();
}

public function calculate($input) {
if (array_key_exists($input, $this->cache)) {
echo "从缓存中获取结果...\n";
return $this->cache[$input];
}

$result = $this->realCalculator->calculate($input);
$this->cache[$input] = $result;
echo "将结果存入缓存...\n";
return $result;
}
}

// 使用示例
$proxy = new CacheProxyCalculator();

// 第一次计算
$input1 = 5;
$result1 = $proxy->calculate($input1);
echo "输入 $input1 的计算结果是: $result1\n";

// 第二次计算相同的输入
$input2 = 5;
$result2 = $proxy->calculate($input2);
echo "输入 $input2 的计算结果是: $result2\n";

// 计算不同的输入
$input3 = 10;
$result3 = $proxy->calculate($input3);
echo "输入 $input3 的计算结果是: $result3\n";
?>

UML类图

UML类图

代理模式的优缺点

优点

  • 保护目标对象:代理对象可以在访问目标对象之前进行权限检查、验证等操作,从而保护目标对象不被非法访问。
  • 增强功能:可以在代理对象中添加额外的功能,如缓存、日志记录等,而不需要修改目标对象的代码,符合开闭原则。
  • 延迟加载:在某些情况下,代理对象可以实现目标对象的延迟加载,只有在真正需要使用目标对象时才进行创建和初始化,提高系统性能。
  • 解耦客户端和目标对象:客户端只需要与代理对象进行交互,不需要直接与目标对象交互,降低了客户端与目标对象之间的耦合度。

缺点

  • 增加系统复杂度:引入代理对象会增加系统的复杂度,尤其是当代理逻辑较为复杂时,可能会导致代码难以理解和维护。
  • 性能开销:代理对象的存在会增加一定的性能开销,因为每次访问目标对象都需要通过代理对象进行中转。
  • 可能导致请求处理变慢:如果代理对象的处理逻辑过多,会导致请求处理的时间变长,影响系统的响应速度。

代理模式与装饰模式的区别

代理模式和装饰模式在结构上有相似之处,都涉及到包装一个对象并提供额外的功能,但它们的设计目的和应用场景存在明显差异。

设计目的

  • 代理模式:主要目的是控制对对象的访问。它在客户端和目标对象之间起到中介作用,客户端通过代理对象来间接访问目标对象。代理对象可以在访问目标对象前后进行一些额外的操作,如权限验证、延迟加载、缓存等,重点在于对访问的控制和管理。
  • 装饰模式:着重于动态地为对象添加额外的职责。它允许在不改变对象原有结构的情况下,通过组合不同的装饰器来为对象增加新的功能。装饰模式更关注于功能的扩展,而不是访问控制。

结构特点

  • 代理模式:代理对象和目标对象实现相同的接口,客户端通常只知道代理对象,而不知道目标对象的存在。代理对象持有目标对象的引用,并在需要时调用目标对象的方法。
  • 装饰模式:装饰器和被装饰的对象都实现相同的抽象组件接口。装饰器可以嵌套使用,通过层层包装来不断为对象添加新的功能。客户端可以直接操作装饰器链,而不需要关心具体的装饰器层次结构。

应用场景

  • 代理模式
    • 远程代理:在分布式系统中,为远程对象提供本地的代理,方便客户端进行远程访问,如远程方法调用(RMI)。
    • 虚拟代理:用于创建开销大的对象,先创建轻量级的代理对象,在真正需要使用时再创建实际对象,如图片的延迟加载。
    • 保护代理:控制对对象的访问权限,在访问前进行权限检查,如系统的权限管理。
    • 缓存代理:为频繁使用的结果提供缓存,避免重复计算,提高系统性能。
  • 装饰模式
    • 功能扩展:当需要为一个对象动态地添加不同的功能组合时,如在图形界面中为组件添加不同的边框、颜色等效果。
    • 避免子类爆炸:相比于使用继承来扩展功能,装饰模式可以避免创建大量的子类,使代码更加灵活和可维护。

总结

代理模式侧重于对对象访问的控制,而装饰模式侧重于对对象功能的扩展。在实际应用中,需要根据具体的需求来选择合适的设计模式。