代理模式——结构型模式(7)

前言

今天我们将介绍的最后一种结构型模式——代理模式,在介绍它之前,让我们先回顾下上一篇博文对享元模式的学习。享元模式主要是通过运用共享技术有效支持大量细粒度的对象,其本质在于两方面:分离和共享。简单地说,分离的是对象状态中变与不变的部分,其中不变的部分设置为对象的内部状态,而随应用场景随之发生变化的部分为对象的外部状态;而共享指的就是对对象状态中不变部分的共享,因为内部状态不会随外部状态的改变而改变,从而也就具备了共享的条件,否则也就失去了共享的意义呢。再者,就是需要通过享元工厂来管理可共享的对象,也是各种具体的享元对象,享元工厂完全可以设计成单例,实现全局唯一性。之前,我们说过,结构型模式大部分都遵循Favor Composition Over Inheritance原则,而在享元模式里并没有太多体现。复习就到此为此吧,让我们步入今天的主题——代理模式的学习。

动机

在实际的软件系统开发中,我们经常面临着对一个对象进行访问控制问题。有些对象由于跨越网络或者安全方面等原因,不能够直接或者不需要直接被访问,直接访问的代价会给系统带来不必要的复杂性,比如可能得考虑访问延迟、运行效率低下等问题。如何在客户端程序与目标对象之间增加一层中间层,通过它来代替目标对象来完成客户程序对目标对象的各种操作要求,如此,客户程序就可以透明地与目标对象进行交互呢。这便是代理模式能解决的类似场景的擅长之处呢。是时候,该代理模式粉墨登场呢!

意图

为其他对象提供一种代理以控制对这个对象的访问。

结构图

image

  1. 代理(Proxy)角色:代理对象,实现与具体目标对象一致的接口,这样就可以使用代理来代替具体的目标对象。同时保存一个指向具体目标对象的引用,可以在需要的时候调用具体的目标对象。此外,亦可以控制对目标对象的访问,并通过代理对象来负责对目标对象的创建和销毁。
  2. 目标接口(Subject)角色:目标接口,定义代理和具体目标对象的接口,这样就可以在任何使用具体目标对象的地方使用代理对象。
  3. 具体目标对象(RealSubject)角色:具体的目标对象,真正实现目标接口功能。
  4. 客户(Client)角色:通过代理中间层与具体目标对象进行交互。

代码示例

 1:  public abstract class Subject{
 2:      public abstract void Request();
 3:  }
 4:   
 5:  public class RealSubject extends Subject{
 6:      public void Request(){
 7:          //执行具体的功能处理
 8:          System.out.println("Request of RealSubject is done!");
 9:      }
10:  }
11:   
12:  public class Proxy extends Subject{
13:      private RealSubject realSubject=null;
14:   
15:      //通过参数完成对具体目标对象的创建,不过一般不推荐,因为客户端通常也无法直接构造具体目标对象
16:      public Proxy(RealSubject realSubject){
17:          this.realSubject=realSubject;
18:      }
19:   
20:      //直接通过默认构造器来完成对具体目标对象的创建
21:      public Proxy(){
22:          this.realSubject=new RealSubject();
23:      }
24:   
25:      public void Request(){
26:          //在转调具体的目标对象之前,可以执行一些预处理
27:          
28:          realSubject.Request();
29:   
30:          //在转调具体的目标对象之后中,可以执行一些后处理
31:      }
32:  }
33:   
34:  public class Client{
35:      public static void main(String[] args){
36:          Subject subject=new Proxy();
37:          subject.Request();
38:      }
39:  }    

从上述示例代码中,我们可以大概明了代理模式的基本实现。目标接口Subject定义了代理和具体目标对象都应该实现的接口,这样客户端使用的时候只需要统一和目标接口打交道即可。目标对象RealSubject真正实现了目标接口功能,而代理Proxy虽然也实现了目标接口,但是它的实现主要还是将功能委派给具体的目标对象来完成,所以代理必须保存一个具体目标对象的引用,在需要的时候能够调用它。而在客户端程序里,我们看到,虽然完成功能的是具体的目标对象,但是其只与代理打交道,因为代理会把客户端的操作请求委派给具体的目标对象的,之所以只与代理打交道,这也就回到了代理模式的终极目的呢:代理模式在客户与具体目标对象之间,引入一定程度的间接性,客户直接使用代理,让代理来与具体的目标对象进行交互,这正因为这层间接性,我们可以完成各种各样的功能,也就可以实现不同类型的代理方式呢,下面对此会有更细致的介绍。从实现上来说,代理模式主要使用了对象的组合和委托,这点从示例代码中已经清楚地表现出来呢,主要体现在代理Proxy类结构中。下面,让我们来看看比较贴切真实的场景例子吧!

现实场景

在现实的生活场景中,我们也可以随处可见代理模式的具体运用。比如,我们通常通过支票或者银行存单来完成对金钱的操作,换句话来说,就是它们在市场交易中作为现金的“代理人”。我们只需要通过它们就能完成对现金的兑现、存款等操作。这样,我们就能省去身上携带大笔现金的不便和不安全性呢,着实给我们日常生活带来一定程度的便利。如果通过UML图来表示,大概就是下面的样子:

image

看到这个图是不是瞬间对上述场景有了更进一步的理解呢。显然,不管是真实的现金还是具有支付功能的支票或者在意都应该有其固定的等价金额功能,这样才能在银行中完成对金钱的各种操作。但是支票和真正的现金还是有很多不一样呢,毕竟它不是真正意义上的现金,所以完成真正的金钱操作还是必须依赖于真正的现金对象,也就是说银行最终打交道的还是现金流对象,而不是支票。之所以需要支票,主要还是为了大众对大额现金的存取方便。这点,在我们日常生活中也是深有体会。抽象成代理模式,支票其实就是代理对象,也就是真实现金的代理者,一切对现金的操作都交给支票来间接完成,从而省去了用户对现金的直接操作,如果非得说为什么不直接通过现金进行操作,可能安全性和便利便是银行使用支票的最重要原因呢。记住,银行里真正处理的对象还是“真金白银”,只是支票在这里作为其等价物,间接地完成了现金的所有操作而已。

接下来,让我们通过一个已经被反复多次作为代理模式示例的demo程序吧——数学计算程序问题,实现简单的加减乘除操作。直接看代码吧!

 1:  public abstract class IMath{
 2:      public abstract double Add(double x,double y);
 3:      public abstract double Sub(double x,double y);
 4:      public abstract double Mul(double x,double y);
 5:      public abstract double Dev(double x,double y);    
 6:  }
 7:   
 8:  public class Math extends IMath{
 9:      public  double Add(double x,double y){
10:          return x+y;
11:      }
12:      public  double Sub(double x,double y){
13:          return x-y;
14:      }
15:      public  double Mul(double x,double y){
16:          return x*y;
17:      }
18:      public  double Dev(double x,double y){
19:          return x/y;//注这里就不考虑分母为零的情况呢,一切从简:)
20:      }
21:  }
22:   
23:  public class MathProxy extends IMath{
24:      private Math math=null;
25:      public MathProxy(){
26:          this.math=new Math();
27:      }
28:   
29:      public  double Add(double x,double y){
30:          //在真正调用Math之前可能需要完成一些网络连接等初始化操作等        
31:          return math.Add(x, y);        
32:          //完成调用Math后,同样需要完成清理工作等
33:      }
34:      public  double Sub(double x,double y){
35:          //在真正调用Math之前可能需要完成一些网络连接等初始化操作等
36:          return math.Sub(x, y);
37:          //完成调用Math后,同样需要完成清理工作等
38:          
39:      }
40:      public  double Mul(double x,double y){
41:          //在真正调用Math之前可能需要完成一些网络连接等初始化操作等
42:          return math.Mul(x, y);
43:          //完成调用Math后,同样需要完成清理工作等
44:      }
45:      public  double Dev(double x,double y){
46:          //在真正调用Math之前可能需要完成一些网络连接等初始化操作等
47:          return  math.Dev(x, y);
48:          //完成调用Math后,同样需要完成清理工作等
49:      }
50:   
51:  }
52:  public class Client{
53:      public static void main(String[] args){
54:          //这里,对MathProxy对象的创建工作完全可以通过创建型模式来完成,
55:          //或者通过java配置和反射功能透明地生成相应的代理对象
56:          IMath math=new MathProxy();
57:          math.Add(1, 2);
58:          math.Sub(4, 2);
59:          math.Mul(3, 4);
60:          math.Dev(6, 3);
61:      }
62:  }    

现在我们对上述代码进行一个简单的讲解下。对这样一个数学计算程序,看似简单,但是当该计算程序位于远程服务器上时,如果我们直接调用真实Math对象,那么每次调用相应的操作就不会像是本地调用那样简单直接呢,因为调用远程对象涉及跨网络问题,我们不得不考虑各种网络相关的复杂操作,比如对发送的消息或者接收到的结果进行装包或者解包等一系列操作问题。可以想像,这将是让人头痛的处理过程。而通过代理模式,我们将这部分工作交给本地端的代理对象MathProxy来完成,同时代理对象也会将我们的请求委派给远程Math对象,间接地完成我们的请求。所以,现在我们只需要与本地端的代理对象打交道,而无需考虑复杂的网络相关问题,大大简化了客户端对真实目标对象Math的调用过程。此外,由于代理对象也实现了相应的目标接口,所以客户端可以像处理具体的目标对象Math一样透明地调用代理对象来完成相应请求操作呢。

最后,让我们来谈谈java中的代理吧。java对代理模式提供了内建的支持,在java.lang.reflect包下面,提供了一个Proxy的类和一个InvocationHandler的接口。上文我们实现的代理模式可以称为java静态代理。不过这种实现方式有一个较大的不足,当目标接口Subject发生改变时,代理类和目标对象的实现都要发生改变,并不是很灵活。而使用java内建的对代理模式支持的功能来实现则可以避免这个问题。通常将使用java内建的对代理模式支持的功能来实现的代理称为java动态代理。与java静态代理的明显区别在于:在静态代理实现的过程中,代理类要实现目标接口所定义的所有方法,而动态代理实现的过程中,即使目标接口中定义了多个方法,但是动态代理类始终只有一个invoke方法。这样,当目标接口发生变化时,动态代理的接口也就不需要发生改变呢。需要注意的是,java的动态代理目前还只支持对接口的代理,基本的实现是依靠java的反射和动态生成class的技术,来动态生成被代理的接口的实现对象。当然,我们亦可以通过cglib库来完成对实现类的代理,cglib是一个开源的Code Generation Library。接下来,就让我们通过动态代理的方式来实现上述数学计算程序吧。

 1:  public interface IMath {
 2:      public  double Add(double x,double y);
 3:      public  double Sub(double x,double y);
 4:      public  double Mul(double x,double y);
 5:      public  double Dev(double x,double y);    
 6:  }
 7:   
 8:  public class Math implements IMath {
 9:      @Override
10:      public double Add(double x, double y) {
11:          // TODO Auto-generated method stub
12:          return x+y;
13:      }
14:   
15:      @Override
16:      public double Dev(double x, double y) {
17:          // TODO Auto-generated method stub
18:          return x-y;
19:      }
20:   
21:      @Override
22:      public double Mul(double x, double y) {
23:          // TODO Auto-generated method stub
24:          return x*y;
25:      }
26:   
27:      @Override
28:      public double Sub(double x, double y) {
29:          // TODO Auto-generated method stub
30:          return x/y;
31:      }
32:  }
33:   
34:  public class MathProxy implements InvocationHandler {
35:      private IMath math=null;
36:   
37:      public IMath getProxyInterface(){
38:          this.math=new Math();
39:          IMath  proxyMath=(IMath)Proxy.newProxyInstance(math.getClass().getClassLoader(),
40:                      math.getClass().getInterfaces(),this);
41:          return proxyMath;
42:      }
43:   
44:      @Override
45:      public Object invoke(Object proxy, Method method, Object[] args)
46:              throws Throwable {
47:          // 直接将请求委派给具体的Math对象
48:          return method.invoke(math, args);
49:      }
50:  }
51:  public class Client{
52:      public static void main(String[] args){
53:          MathProxy mathProxy=new MathProxy();
54:          IMath math=mathProxy.getProxyInterface();
55:   
56:          math.Add(1, 2);
57:          math.Sub(4, 2);
58:          math.Mul(3, 4);
59:          math.Dev(6, 3);
60:      }
61:  }    

运行的结果与我们之前手动实现的静态代理模式是一样的。其实,java的动态代理还是AOP的一个重要手段,有兴趣的同学可以查阅相关资料进行深入学习,在这里,我们对动态代理讲解就说这么多呢,网上对java动态代理的细致解说的博文也有很多,比如这里这里,还有这里等等。好呢,对代理模式的应用举例就说这么多呢。就此打住吧:)

实现要点

  1. 代理类最好也实现与具体目标对象实现的相同接口,这样客户端就可以透明地操作代理对象,因为它与目标对象的接口完全一致。
  2. 代理类内部保存具体目标对象引用,以便需要的时间能够调用之,同时,也应该考虑如何才能更好地创建目标对象,是通过外部传递方式还是直接在构造器中完成对目标对象的创建,两种方案需要根据具体应用场景来取舍。
  3. 如果能够遵循第一点,那么代理类可以不总是需要知道具体目标对象类型,也就是说无须为每一个具体目标对象都生成一个代理类,代理类可以统一地处理所有的目标对象,这点很重要。

运用效果

代理模式在访问对象时引入了一定程度的间接性,这样根据代理的类型,附加的间接性也有会带来不同的用途和效果,具体请参考适用性一节。

适用性

在如下的情况中建议考虑使用代理模式:

  1. 需要为一个对象在不同的地址空间提供局部代表的时候,可以使用远程代理。它隐藏了一个对象存在于不同的地址空间的事实,即客户通过远程代理来访问一个对象,根本就不用关心这个对象在哪里,以及如何通过网络来访问到这个对象,这些工作完全由代理帮我们完成。
  2. 需要按照需要创建开销很大的对象的时候,可以使用虚代理。它可以根据需要来创建“大”对象,只有到必须创建的时候,虚代理才会创建对象,从而大大加快程序运行速度,节省资源。此外,通过虚代理还可以对系统进行优化。
  3. 需要控制对原始对象的访问的时候,可以使用保护代理。它可以在访问一个对象的前后,执行很多附加操作,除了进行权限控制之外,还可以进行很多跟业务相关的处理,而不需要修改被代理的对象。换句话来说就是通过代理来给目标对象增加额外的功能。
  4. 需要在访问对象执行一些附加操作的时候,可以使用智能指引代理。其和保护代理类似,也是允许在访问一个对象的前后,执行很多的附加操作,比如引用计数等操作。

上面的四种代理方式各有侧重,对它们具体的应用情况大家可以查阅相关资料进行深入地学习。

相关模式

  1. 代理模式与适配器模式:两者都是为另一个对象提供间接性的访问,而且都是从自身以外的一个接口向这个对象转发请求。从功能上来说,两者是完全不一样的。适配器模式主要用来解决接口间不匹配的问题,为所适配的对象提供一个不同的接口;而代理模式会实现和目标对象相同的接口。
  2. 代理模式与装饰模式:两者结构上很相似,但是彼此的目的和功能是不同的。装饰模式是为了让不生成子类就可以给对象添加额外的职责,即动态地添加功能;而代理模式主要是控制对对象的访问。

总结

代理模式的本质是:控制对象访问。代理模式通过代理对象,在客户端与目标对象之间增加了一层中间层。也正因为这个中间层,给代理对象很多的活动空间。可以在代理对象中调用目标对象前后增加很多操作,从而实现新的功能或扩展目标对象的功能,从抽象的含义来说,也就是完成了对对象的访问控制作用。而且通过java动态代理方式,可以实现对目标对象的完全代理,即完全替代呢。从实现过程上来看,代理模式主要使用对象的组合和委托,这点从我们实现的静态代理方法中体现地比较明了。在这里,强烈推荐大家可以更深入地去学习理解下java或者.net语言中的动态代理实现方式,这样会对代理模式有一个更深一步地认识。到此为止,我们已经将所有的结构型模式都介绍完毕呢,下一篇我们将会对结构型模式作一较为详细的总结,紧接着就是对行为型模式的讲解呢,敬请期待!

参考资料

  1. 程杰著《大话设计模式》一书
  2. 陈臣等著《研磨设计模式》一书
  3. GOF著《设计模式》一书
  4. Terrylee .Net设计模式系列文章
  5. 吕震宇老师 设计模式系列文章
posted @ 2012-10-15 08:52  JackyBing  阅读(2293)  评论(7编辑  收藏  举报