深入了解代理模式:面向对象设计的强大工具

85 阅读14分钟

一、代理模式简介

代理模式(Proxy Pattern) 是一种结构型设计模式,其核心思想是通过代理对象来间接访问真实对象,从而实现对真实对象的控制和扩展。代理模式通常用于延迟加载、权限控制、日志记录、性能监控等场景。

a3820d52-e5ba-4893-a73d-99f805c11eea

代理模式软件设计模式中的一种,具体属于结构型设计模式

  • 软件设计模式: 针对软件开发过程中常见问题的通用解决方案
  • 结构型设计模式:结构型设计模式关注如何组织和组合类或对象,以便在不改变其功能的情况下提高系统的灵活性或可维护性。它们主要涉及对象之间的关系和如何将这些对象组合起来使用。
  • 代理模式作为结构型模式的一种,它是通过引入一个代理对象来控制对原始对象的访问,从而在不改变原始对象的前提下,增加额外的功能。代理对象和原始对象通常都实现同一个接口,因此它们在结构上是相似的,可以互换。

代理模式是非常常见的模式,在生活中的例子也非常多,例如你不好意思向你关系不太好朋友帮个忙,这时需要找一个和它关系好的应一个朋友帮忙转达,这个中间朋友就是代理对象。例如购买火车票不一定要去火车站买,可以通过12306网站或者去火车票代售点买。

二、发展

代理模式(Proxy Pattern)作为一种设计模式,起源于软件工程中的结构型模式。它通过引入代理对象来控制对某个对象的访问。代理模式的应用在软件开发中经历了几个重要的发展阶段,并随着技术和需求的演变,逐渐形成了多种变体。以下是代理模式在软件开发中的发展历程:

1. 早期的代理模式(20世纪60-80年代)

在计算机科学的早期,代理模式的基本思想最早出现在操作系统和硬件资源管理领域。在资源有限的计算环境中,操作系统常常通过代理来管理硬件设备的访问,避免直接操作硬件资源。例如,通过“虚拟设备驱动程序”来管理硬件设备访问。

随着面向对象编程(OOP)的发展,代理模式开始成为软件设计中的一个重要概念。虽然这个时期的代理模式没有像现代设计模式那样被广泛讨论,但其核心思想开始在一些小规模软件项目中使用,主要是通过引入中间层对象来代理资源访问,控制访问逻辑和延迟初始化。

2. 设计模式的正式引入(1980年代末)

1987年,Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides 发表了《Design Patterns: Elements of Reusable Object-Oriented Software》一书,首次正式将代理模式作为一种标准的设计模式提出。这本书被誉为“设计模式的圣经”,代理模式成为四大经典结构型设计模式之一。

a08c0ed1-5bf9-4017-acfa-d5ef462f2346

代理模式被定义为:通过引入代理对象来控制对真实对象的访问。代理对象可以在客户端和真实对象之间充当中介角色,代理模式根据不同的需求有不同的实现方式:

  • 虚拟代理:延迟实例化和加载目标对象,直到真正需要时才进行创建。
  • 远程代理:用于控制访问远程对象,常见于分布式系统中的客户端-服务器通信。
  • 保护代理:通过控制权限来管理对目标对象的访问,常用于安全性管理。
  • 智能代理:除了正常的代理行为外,还能够提供额外的功能,例如计数、缓存等。

3. 分布式系统中的代理应用(1990年代)

随着计算机网络的普及和分布式计算的兴起,代理模式的应用逐渐扩展到远程通信和网络中。在这个时期,代理模式主要用于:

  • 远程代理:在分布式系统中,代理模式被广泛应用于远程过程调用(RPC)系统中。远程代理在客户端和服务器之间充当中介,帮助客户端与远程对象进行通信,隐藏了网络通信的复杂性。
  • Web代理:随着Web应用的快速发展,Web代理(例如HTTP代理)开始流行。代理模式在网络请求的转发、缓存、负载均衡等方面发挥重要作用。

4. 面向切面编程与动态代理(2000年代)

进入21世纪后,代理模式在现代编程语言中的应用得到了进一步的拓展,尤其是在面向切面编程(AOP)中。AOP框架常常通过动态代理来实现横切关注点的分离,如事务管理、日志记录和安全验证等功能。

  • 动态代理:在Java等语言中,代理模式得到了进一步的应用,尤其是在动态代理方面。Java中的java.lang.reflect.Proxy​类允许在运行时创建代理对象,通过反射机制在代理对象上进行方法调用。动态代理的一个典型应用场景是AOP(面向切面编程),如Spring框架中的动态代理实现。

  • 静态代理与动态代理

    • 静态代理:代理类在编译时就已经确定,代理类和目标类具有相同的接口。
    • 动态代理:代理类在运行时动态生成,可以在不修改目标类的情况下,动态地增强目标对象的行为。

5. 云计算与微服务架构中的代理模式(2010年代至今)

随着云计算、微服务架构和容器技术的迅猛发展,代理模式在现代软件架构中的应用愈发广泛,主要体现在以下几个方面:

  • API网关:在微服务架构中,API网关常常作为反向代理,管理客户端和多个后端微服务之间的通信。API网关负责路由请求、负载均衡、认证、授权等功能,它充当了多个微服务的入口。
  • 服务网格:服务网格(如Istio)利用代理模式来增强微服务之间的通信,提供流量管理、安全性、监控和故障恢复等功能。服务网格中的代理(sidecar代理)部署在每个微服务旁边,拦截并管理进入和离开的流量。
  • 反向代理与负载均衡:反向代理用于将客户端的请求转发到后端服务器,并对请求进行负载均衡,提升系统的可靠性和性能。
  • 缓存代理:在Web应用中,代理模式还被广泛用于缓存,减少对目标服务器的请求,提高系统响应速度。例如,CDN(内容分发网络)就是利用代理缓存来加速全球用户的访问速度。

三、特点

3.1 优点

  • 增加额外功能:代理模式可以在不修改真实对象的情况下为其增加额外的功能,如日志记录、事务处理、安全控制等。
  • 解耦:客户端通过代理对象访问真实对象,使得客户端与真实对象之间的耦合度降低。
  • 延迟加载:通过代理类可以控制目标对象的创建时机,延迟对象的初始化。

3.2 缺点

  • 增加复杂性:代理模式通过增加代理对象,可能会使系统结构更加复杂。
  • 性能开销:使用代理模式时,额外的代理层会带来一定的性能开销,尤其是在方法调用较为频繁时。

四、功能

  • 代理模式通过为真实对象(通常被称为 RealSubject​)创建一个代理对象(Proxy​),来控制对该对象的访问。代理对象通常会做一些附加的操作,例如:权限控制、延迟加载、缓存、日志记录等,最后通过代理对象将请求转发到真实对象。

举个例子:

假设有一个 Subject​ 接口,RealSubject​ 是它的一个实现类,Proxy​ 也是 Subject​ 的一个实现类。

  • RealSubject​:实际的业务逻辑执行者,例如在数据库中查询数据。
  • Proxy​:它代理 RealSubject​,比如可以添加一些附加操作(如检查权限、日志记录等),然后将请求转发给 RealSubject​。

五、分类

  1. 虚拟代理(Virtual Proxy):用于延迟加载对象,只有在真正需要时才实例化目标对象。常用于需要大量资源的对象,比如大图片或视频文件。
  2. 远程代理(Remote Proxy):用于对象位于不同地址空间(通常在不同服务器)时,代理对象代表远程对象进行访问。例如,RMI(Remote Method Invocation)或Web服务中的代理。
  3. 保护代理(Protective Proxy):控制访问权限,保护目标对象。例如,限制某些用户访问敏感操作或数据。
  4. 智能代理(Smart Proxy):增强目标对象的功能,如引用计数、缓存等,或提供其他附加服务。

六、代理模式的结构

代理模式一般包含以下角色:

  • Subject(抽象主题) :定义了真实对象和代理对象的共同接口,声明代理对象和真实对象都应该实现的方法。
  • RealSubject(真实主题) :实现了 Subject​ 接口,是真正执行任务的对象。
  • Proxy(代理对象) :持有一个 RealSubject​ 对象的引用,通过代理对象来间接调用 RealSubject​ 的方法,可以在方法执行前后添加额外的逻辑。
  • Client(客户端) :通过代理对象来调用真实对象的方法。

七、实现方式

在 Java 中,代理模式主要有两种实现方式:

  1. 静态代理(Static Proxy)
  2. 动态代理(Dynamic Proxy)

7.1 静态代理(Static Proxy)

静态代理是通过在编译时生成代理类来实现的。代理类需要实现与目标类相同的接口,代理类内部持有目标类的实例,并通过代理类调用目标类的方法。

静态代理是指代理类和真实类在编译时就已经确定,代理类和真实类实现相同的接口,代理类持有真实对象的引用,并在代理方法中转发对真实对象方法的调用。静态代理的实现通常需要手动编写代理类。

实现步骤:

  1. 创建一个接口 Subject​,目标类 RealSubject​ 实现这个接口。
  2. 创建一个代理类 Proxy​,它也实现 Subject​ 接口,并持有 RealSubject​ 的实例。
  3. 在代理类中,控制对目标对象的访问,可能会进行一些附加操作(如日志、权限控制等)。

示例代码(静态代理):

// 1. 定义接口
public interface Subject {
    void request();
}
​
// 2. 目标类实现接口
public class RealSubject implements Subject {
    @Override
    public void request() {
        System.out.println("Request made to RealSubject.");
    }
}
​
// 3. 代理类实现接口,并持有目标类的引用
public class Proxy implements Subject {
    private RealSubject realSubject;
​
    public Proxy(RealSubject realSubject) {
        this.realSubject = realSubject;
    }
​
    @Override
    public void request() {
        // 可以在此添加附加操作
        System.out.println("Proxy: Before delegating to RealSubject.");
        realSubject.request();
        System.out.println("Proxy: After delegating to RealSubject.");
    }
}
​
// 4. 客户端代码
public class Main {
    public static void main(String[] args) {
        RealSubject realSubject = new RealSubject();
        Proxy proxy = new Proxy(realSubject);
        proxy.request();
    }
}

输出:

Proxy: Before delegating to RealSubject.
Request made to RealSubject.
Proxy: After delegating to RealSubject.

7.2 动态代理(Dynamic Proxy)

动态代理是在运行时创建代理对象,不需要在编译时生成代理类。Java 提供了两种动态代理方式:

  • JDK 动态代理(基于接口的代理)
  • CGLIB 动态代理(基于类的代理)

JDK 动态代理

动态代理是通过 java.lang.reflect.Proxy​ 类和 InvocationHandler​ 接口来动态创建代理对象的。在运行时,可以生成代理类,并将目标对象传递给代理类来实现代理功能。动态代理无需提前定义代理类,提供了更高的灵活性。

实现步骤:
  1. 定义一个接口 Subject​。
  2. 实现目标类 RealSubject​,并实现该接口。
  3. 创建一个 InvocationHandler​,它在目标方法调用前后进行处理。
  4. 使用 Proxy.newProxyInstance()​ 动态创建代理类。

示例代码(动态代理):
import java.lang.reflect.*;
​
// 1. 定义接口
public interface Subject {
    void request();
}
​
// 2. 目标类实现接口
public class RealSubject implements Subject {
    @Override
    public void request() {
        System.out.println("Request made to RealSubject.");
    }
}
​
// 3. 动态代理处理器
class ProxyHandler implements InvocationHandler {
    private Object target;
​
    public ProxyHandler(Object target) {
        this.target = target;
    }
​
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 代理方法前的附加操作
        System.out.println("Proxy: Before invoking method.");
        // 调用目标对象的方法
        Object result = method.invoke(target, args);
        // 代理方法后的附加操作
        System.out.println("Proxy: After invoking method.");
        return result;
    }
}
​
// 4. 客户端代码
public class Main {
    public static void main(String[] args) {
        RealSubject realSubject = new RealSubject();
  
        // 创建动态代理实例
        Subject proxy = (Subject) Proxy.newProxyInstance(
                realSubject.getClass().getClassLoader(), 
                realSubject.getClass().getInterfaces(), 
                new ProxyHandler(realSubject));
​
        proxy.request();  // 调用代理方法
    }
}

输出:

Proxy: Before invoking method.
Request made to RealSubject.
Proxy: After invoking method.

八、应用场景

代理模式可以应用于许多不同的场景,以下是一些常见的应用场景:

1. 远程代理(Remote Proxy)
  • 分布式系统:当客户端和服务器端位于不同的地址空间时,可以使用远程代理来封装网络通信细节,使得客户端就像直接调用本地方法一样简单。
  • RMI(远程方法调用) :Java RMI就是一个典型的远程代理应用案例,它允许一个Java虚拟机上的对象调用另一个Java虚拟机上的对象的方法。
2. 虚拟代理(Virtual Proxy)
  • 延迟加载:用于按需创建开销较大的对象。例如,在图形界面中,如果某个组件只有在用户滚动到特定位置时才可见,那么可以在实际显示之前不创建该组件,而是在需要时通过代理创建并初始化。
  • 图像加载:对于大型图片或从网络加载的图片,可以通过代理模式先展示一个小图标或占位符,然后在后台加载真实的图片,加载完成后替换占位符。
3. 保护代理(Protection Proxy)
  • 权限控制:在某些情况下,可能希望根据用户的身份或角色限制对特定资源的访问。保护代理可以在请求到达目标对象之前检查用户的权限,并决定是否允许执行操作。
4. 智能引用(Smart Reference)
  • 计数器:智能引用代理可以在每次访问对象时增加计数,以跟踪对象被引用的次数,这对于垃圾回收机制特别有用。
  • 日志记录:每当对象被访问时,代理可以记录访问信息,如时间戳、访问者身份等,这有助于审计和调试。
5. 缓存代理(Cache Proxy)
  • 性能优化:缓存代理可以在第一次请求后保存结果,后续相同的请求可以直接返回缓存的数据,而不是每次都重新计算或查询数据库,从而提高响应速度。
6. 防火墙代理(Firewall Proxy)
  • 安全过滤:防火墙代理可以用来防止非法数据进入内部系统,确保只有经过验证的数据才能传递给真实对象。
7. 复杂对象的简化接口
  • API封装:有时候,真实对象提供的接口可能过于复杂或不适合外部使用。此时,可以使用代理来提供一个更简洁、更容易使用的接口给客户端。
8. AOP(面向切面编程)
  • 横切关注点:AOP框架通常使用代理模式来实现对业务逻辑的增强,比如事务管理、日志记录、性能监控等功能,这些功能不需要修改原始业务代码即可实现。

通过这些应用场景可以看出,代理模式是一个非常灵活的设计模式,它可以有效地解耦系统组件之间的依赖关系,同时还能为对象交互提供更多的控制和灵活性。