【Design Pattern】Structural - Proxy/Wrapper

Posted by 西维蜀黍 on 2019-04-05, Last Modified on 2022-12-10

代理模式(Proxy Pattern)

In computer programming, the proxy pattern is a software design pattern.

Proxy is a structural design pattern that lets you provide a substitute or placeholder for another object. A proxy controls access to the original object, allowing you to perform something either before or after the request gets through to the original object.

A proxy, in its most general form, is a class functioning as an interface to something else. The proxy could interface to anything: a network connection, a large object in memory, a file, or some other resource that is expensive or impossible to duplicate.

In short, a proxy is a wrapper or agent object that is being called by the client to access the real serving object behind the scenes. Use of the proxy can simply be forwarding to the real object, or can provide additional logic.

In the proxy, extra functionality can be provided, for example caching when operations on the real object are resource intensive, or checking preconditions before operations on the real object are invoked. For the client, usage of a proxy object is similar to using the real object, because both implement the same interface.

What problems can the Proxy design pattern solve?

  • The access to an object should be controlled.
  • Additional functionality should be provided when accessing an object.

When accessing sensitive objects, for example, it should be possible to check that clients have the needed access rights.

What solution does the Proxy design pattern describe?

Define a separate Proxy object that

  • can be used as substitute for another object (Subject) and
  • implements additional functionality to control the access to this subject.

This makes it possible to work through a Proxy object to perform additional functionality when accessing a subject. For example, to check the access rights of clients accessing a sensitive object.

To act as substitute for a subject, a proxy must implement the Subject interface. Clients can’t tell whether they work with a subject or its proxy.

在需要科学上网时,我们通常会搭建一个代理服务器,并且通过这个代理服务器来访问Google或Youtube。至于具体是哪个Google的服务器来服务我们,我们是不知情,同时也不需要了解的(只有代理服务器才需要关注这个事情,因而选择哪一个具体的Google服务器就是代理的functionality了)。

在软件设计中,使用代理模式的意图也很多,比如因为安全原因需要屏蔽客户端直接访问真实对象,或者在远程调用中需要使用代理类处理远程方法调用的技术细节(如 RMI),也可能为了提升系统性能,对真实对象进行封装,从而达到延迟加载的目的。

UML

  • 主题接口(Subject):一个借口,定义了代理类(Proxy)和真实主题(RealSubject)的公共对外方法;
  • 真实主题(RealSubject):真正实现业务逻辑的类;
  • 代理类(Proxy):用来代理和封装真实主题;
  • 客户端:调用代理类和主题接口来完成一些工作。

例子

小成希望买一台最新的顶配Mac电脑。但是,由于在国内还没上市,只有美国才有。因此,小成将通过代购进行购买。

主体接口(Subject)

public interface Subject {  
		public void buyMac();
}

真实对象类(RealSubject)

public class RealSubject implement Subject{
    @Override
    public void buyMac() {  
        System.out.println(买一台Mac);  
    }  
}

代理类(Proxy)

public class Proxy implement Subject{
    private Subject subject;
  	public Proxy(Subject subject){
   	 		this.subject = subject;
  	}
  
    // Add extra functionality here 
    public void beforeBuyMac() {
        //..., e.g., calculate agency fee
    }
    // Add extra functionality here   
    public void afterBuyMac() {
       // ..., e.g., let the client pay agency fee
    }  
    @Override
    public void buyMac() {  
      	this.subject.buyMac();
        System.out.println(通过指定的代购买了一台Mac);  
    }  
}

客户端调用

public class ProxyPattern {
    public static void main(String[] args){
    		Subject proxy = new Proxy(new Subject())
    		proxy.buyMac()
    }
}

应用代理模式的例子

Python 装饰器(Wrapper)

from functools import wraps
def logged(func):
    @wraps(func)
    def with_logging(*args, **kwargs):
        print func.__name__      # 输出 'f'
        print func.__doc__       # 输出 'does some math'
        return func(*args, **kwargs)
    return with_logging

@logged
def f(x):
   """does some math"""
   return x + x * x

In the proxy, proxy pattern (wrapper pattern) provides extra functionality,

In this case, logging functionality is provided.

Circuit Breaker

一个典型的使用了代理模式的例子就是 Circuit Breaker。

Basically, you wrap a protected function call in a circuit breaker object, which monitors for failures. Once the failures reach a certain threshold, the circuit breaker trips, and all further calls to the circuit breaker return with an error, without the protected call being made at all. Usually you’ll also want some kind of monitor alert if the circuit breaker trips.

![img](assets/assets 2021-06-26 11-50-52.png)

Note that Proxy Pattern is not only used by function call level, but also at higher level, for example service level.

Actually Circuit Breaker Pattern (under microservice architecture) is another typical example that applied the idea of Proxy/Wrapper Pattern.

AOP(Aspect Oriented Programming),即面向切面编程

AOP 同样也是应用了代理模式的思想。

代理模式的应用场景

根据代理模式的使用目的,常见的代理模式有以下几种类型:

  • 远程代理(Remote Proxy):为一个位于不同的地址空间的对象提供一个本地的代理对象,这个不同的地址空间可以是在同一台主机中,也可是在另一台主机中,远程代理又叫做大使(Ambassador)。
  • 虚拟代理(Virtual Proxy):如果需要创建一个资源消耗较大的对象,先创建一个消耗相对较小的对象来表示,真实对象只在需要时才会被真正创建。
  • Copy-on-Write代理:它是虚拟代理的一种,把复制(克隆)操作延迟到只有在客户端真正需要时才执行。一般来说,对象的深克隆(deep copy)是一个开销较大的操作,Copy-on-Write代理可以让这个操作延迟,只有对象被用到的时候才被克隆。
  • 保护代理(Protect or Access Proxy):控制对一个对象的访问,可以给不同的用户提供不同级别的使用权限。
  • 缓冲代理(Cache Proxy):为某一个目标操作的结果提供临时的存储空间,以便多个客户端可以共享这些结果。
  • 防火墙代理(Firewall Proxy):保护目标不让恶意用户接近。
  • 同步化代理(Synchronization Proxy):使几个用户能够同时使用一个对象而没有冲突。
  • 智能引用代理(Smart Reference Proxy):当一个对象被引用时,提供一些额外的操作,如将此对象被调用的次数记录下来等。

延迟加载

以一个简单的示例来阐述使用代理模式实现延迟加载的方法及其意义。假设某客户端软件有根据用户请求去数据库查询数据的功能。在查询数据前,需要获得数据库连接,软件开启时初始化系统的所有类,此时尝试获得数据库连接。当系统有大量的类似操作存在时 (比如 XML 解析等),所有这些初始化操作的叠加会使得系统的启动速度变得非常缓慢。为此,使用代理模式的代理类封装对数据库查询中的初始化操作,当系统启动时,初始化这个代理类,而非真实的数据库查询类,而代理类什么都没有做。因此,它的构造是相当迅速的。

在系统启动时,将消耗资源最多的方法都使用代理模式分离,可以加快系统的启动速度,减少用户的等待时间。而在用户真正做查询操作时再由代理类单独去加载真实的数据库查询类,完成用户的请求。这个过程就是使用代理模式实现了延迟加载。

延迟加载的核心思想是:如果当前并没有使用这个组件,则不需要真正地初始化它,使用一个代理对象替代它的原有的位置,只要在真正需要的时候才对它进行加载。使用代理模式的延迟加载是非常有意义的,首先,它可以在时间轴上分散系统压力,尤其在系统启动时,不必完成所有的初始化工作,从而加速启动时间;其次,对很多真实主题而言,在软件启动直到被关闭的整个过程中,可能根本不会被调用,初始化这些数据无疑是一种资源浪费。

动态代理

动态代理是指在运行时动态生成代理类。即,代理类的字节码将在运行时生成并载入当前代理的 ClassLoader。与静态处理类相比,动态类有诸多好处。首先,不需要为真实主题写一个形式上完全一样的封装类,假如主题接口中的方法很多,为每一个接口写一个代理方法也很麻烦。如果接口有变动,则真实主题和代理类都要修改,不利于系统维护;其次,使用一些动态代理的生成方法甚至可以在运行时制定代理类的执行逻辑,从而大大提升系统的灵活性。

动态代理类使用字节码动态生成加载技术,在运行时生成加载类。生成动态代理类的方法很多,如,JDK 自带的动态处理、CGLIB、Javassist 或者 ASM 库。JDK 的动态代理使用简单,它内置在 JDK 中,因此不需要引入第三方 Jar 包,但相对功能比较弱。CGLIB 和 Javassist 都是高级的字节码生成库,总体性能比 JDK 自带的动态代理好,而且功能十分强大。ASM 是低级的字节码生成工具,使用 ASM 已经近乎于在使用 Java bytecode 编程,对开发人员要求最高,当然,也是性能最好的一种动态代理生成工具。但 ASM 的使用很繁琐,而且性能也没有数量级的提升,与 CGLIB 等高级字节码生成工具相比,ASM 程序的维护性较差,如果不是在对性能有苛刻要求的场合,还是推荐 CGLIB 或者 Javassist。

优缺点

优点

  • 代理模式能够协调调用者和被调用者,在一定程度上降低了系统的耦合度。
  • 远程代理使得客户端可以访问在远程机器上的对象,远程机器可能具有更好的计算性能与处理速度,可以快速响应并处理客户端请求。
  • 虚拟代理通过使用一个小对象来代表一个大对象,可以减少系统资源的消耗,对系统进行优化并提高运行速度。
  • 保护代理可以控制对真实对象的使用权限。

缺点

  • 由于在客户端和真实主题之间增加了代理对象,因此有些类型的代理模式可能会造成请求的处理速度变慢。
  • 实现代理模式需要额外的工作,有些代理模式的实现非常复杂。

Reference