您的位置:首页 > 其它

设计模式之代理模式(Proxy Pattern)

2015-04-04 19:33 351 查看
这篇博客,我们要详细讲解的是代理模式(Proxy Pattern),将要讲解的内容有:代理模式的定义,作用,
详细设计分析等方面。


一、Pattern name

为其他对象提供一种代理以控制对这个对象的访问。 ——《设计模式 可复用面向对象软件的基础》

二、Problem

在日常的工作和程序设计中,你是否遇到过这样的问题:

你***了一个网页,里面有你对人生的思考和生活的感悟,而且,你为了整个页面的美观而精心挑选***了一些精美的图片或者插图等,整体看起来非常赞!但是,你却发现由于使用了很多精美的图片,在网络不好时,甚至网络状态良好时,整个页面加载起来也会比较慢,用户体验极差。而你又不愿意舍弃精心挑选的图片,为此久久不能释怀,找不到合适的解决方法。


你在***网站时,发现需要对不同的用户(游客,注册普通用户,会员用户,系统管理员,网站管理员等)进行不同的权限设置,而,如果把他们分别设计为不同的功能类,你又发现不同的用户之间有很多相似甚至相同的功能,整个系统看起来代码重复现象严重,该如何巧妙的设计,既能体现不同用户的不同权限,又能有效地减少代码的重复现象呢?

如果你常常遇到上面描述的问题,而且百思不得其解,那么,你应该和代理模式交个朋友,学会用代理模式解决上面的问题以及其他更多的问题。

三、Solution

首先,我们来看一下,代理模式的UML类图结构:



上图是最基本、最简单情况的代理模式,从图中我们可以看出,Subject是表示一个抽象类或者接口,在这个接口中定义了客户端(Client)需要调用的方法,其中RealSubject类是这个方法的真正实现类,而Proxy虽然也继承了Subject抽象类或者实现了Subject接口,但是在Proxy中并不是真正的实现,而是生成一个对RealSubject对象的引用realSubject,再通过调用realSubject.Request()来实现Subject中要求的方法,即Request()方法。

在这里,可以用一个形象的比喻:代理模式实际上告诉我们,Proxy实际上就是一个接活的,而真正干活的是RealSubject,但是客户(Client)不需要知道谁在真正干活,他只需要通过Subject myProxy = new Proxy();创建一个代理(Proxy)的对象,然后调用myProxy.Request();方法来实现自己的要求,就可以了。至于代理(Proxy)是自己干活还是找人干活,Client并不关心,他只关心自己安排的任务是否完成了。

其实,代理模式变形非常多,设计处理也非常灵活,能够通过适当变形解决很多问题,下面我们来尝试用代理模式解决我们刚刚提出的那两个问题。

1. 页面加载问题:

我们首先来看一下,页面加载问题解决方案的UML类图结构:



从图中我们可以发现,其实整个页面的加载控制程序就相当于(DocumentEditor),他并没有直接new一个Image(图像操作的真正实现类)的对象,而是通过Graphic接口创建一个代理对象(ImageProxy),而这个代理对象里面存储了该图像最基本的信息如:图像的名字(fileName),图像的尺寸大小(extent),这样当页面加载时,需要通过页面内元素大小调整布局的时候,并不需要马上创建一个Image的对象,而只需要创建一个代理对象(ImageProxy),并使用代理对象的extent属性就可以完成界面的布局工作。而当页面已经移动(如下拉界面)到图像位置时,再调用ImageProxy的Draw()方法,此时再由代理对象(ImageProxy)new一个Image对象,并调用Image对象的Draw()方法即可。

也就是说,只有在页面的加载控制程序(DocumentEditor)激活(调用)图像代理(ImageProxy)的Draw()操作以显示这个图像的时候,图像代理(ImageProxy)才会创建真正的图像(Image),然后图像代理(ImageProxy)将Draw()请求转发给这个真正的图像(Image)对象。

这样可以有效地推迟Image对象的创建时间,如果Image对象的创建很耗时的话,会有效减少创建Image对象对整个页面加载速度的影响,也就有效地解决了我们上面提到的加载页面很慢的问题。

2. 网站权限控制问题

对于网站权限控制问题的UML类图接口其实和上面的页面加载问题的类似,我们就不画UML类图了,直接上代码。

抽象类或者接口(Subject)代码:

public interface Subject {
    //对网站进行的某项操作
    public void operation();
}


真正实现操作的实现类(RealSubject)代码:

public class RealSubject implements Subject{
    public void operation() {
        //真正的实现类的操作
        System.out.println("这是真正的实现类的操作。");
    }
}


代理类(Proxy)代码:

public class Proxy implements Subject{
    RealSubject realSubject = new RealSubject();
    //当然这里也可以写成Subject realSubject = new RealSubject();
    public void operation() {
        //调用真正实现操作的对象方法之前,可以做调用前操作处理。
        System.out.println("在这里,你可以进行一些调用前操作处理。");        
        realSubject.operation();        
        //调用真正实现操作的对象方法之后,可以做调用后操作处理。
        System.out.println("在这里,你可以进行一些调用后操作处理。");
    }
}


客户端(Client)代码:

public class Client {

    public static void main(String[] args) {
        Subject myProxy = new Proxy();
        myProxy.operation();
    }

}


上面几个代码,我想都很容易看懂,在这里,主要解释一下Proxy的代码,从代码中可以看到,在调用realSubject.operation();之前,你可以进行一下调用前的处理,比如检查权限,检查环境状况等等,在调用之后你也可以进行相应的处理。当然了,如果你觉得根据用户权限,有的用户无法调用该方法,并在无权限用户调用的时候返回提醒信息如:“请登录后查看”等。你可以重新改造Proxy的operation方法,使用一个判断语句,对用户权限进行判断,再根据判断结果,选择执不执行realSubject.operation()方法,如何执行该方法,是否要返回警告信息等。

到这里呢,我们前面提到的两个问题就都解决了,是不是已经慢慢发现代理模式的奇妙魅力!

四、Consequences

可以说通过增加一个代理,我们对这个对象的控制的灵活性大大增加,我们可以在对对象的访问和操作方面增加我们需要的控制和保护等。

那么我们说过代理模式非常灵活,变形非常多,应用的场景也非常多,那么,它都有哪些变形以及都用在什么场景呢?下面我们来做一下简要的介绍:

(1)远程代理(Remote Proxy) -可以隐藏一个对象存在于不同地址空间的事实。也使得客户端可以访问在远程机器上的对象,远程机器可能具有更好的计算性能与处理速度,可以快速响应并处理客户端请求。


(2)虚拟代理(Virtual Proxy) – 允许内存开销较大的对象在需要的时候创建。只有我们真正需要这个对象的时候才创建(其实这个和我们举得那个页面加载的问题相似)。


(3)写入时复制代理(Copy-On-Write Proxy) – 用来控制对象的复制,方法是延迟对象的复制,直到客户真的需要为止。是虚拟代理的一个变体。


(4)保护代理(Protection (Access)Proxy) – 为不同的客户提供不同级别的目标对象访问权限


(5)缓存代理(Cache Proxy) – 为开销大的运算结果提供暂时存储,它允许多个客户共享结果,以减少计算或网络延迟。


(6)防火墙代理(Firewall Proxy) – 控制网络资源的访问,保护主题免于恶意客户的侵害。


(7)同步代理(SynchronizationProxy) – 在多线程的情况下为主题提供安全的访问。


(8)智能引用代理(Smart ReferenceProxy) - 当一个对象被引用时,提供一些额外的操作,比如将对此对象调用的次数记录下来等。


(9)复杂隐藏代理(Complexity HidingProxy) – 用来隐藏一个类的复杂集合的复杂度,并进行访问控制。有时候也称为外观代理(Façade Proxy),这不难理解。复杂隐藏代理和外观模式是不一样的,因为代理控制访问,而外观模式是不一样的,因为代理控制访问,而外观模式只提供另一组接口。

在这里我们详细解释一下copy-on-write的优化方式:

该优化与根据需要创建对象有关,拷贝一个庞大而复杂的对象是一种开销很大的操作,如果这个拷贝根本没有被修改,那么这些开销就没有必要,用代理延迟这一拷贝过程,我们可以保证只有当这个对象被修改的时候才对它进行拷贝。

在实现copy-on-write时,必须对实体进行引用计数,拷贝代理仅会增加引用计数,只有当用户请求修改该实体时,代理才会真正的拷贝它,在这种情况下,代理还必须减少实体的引用计数,当引用的数目为0时,这个实体将被删除。

五、常见疑问解答及其他

疑问1:在Proxy中创建RealSubject的对象时,是使用RealSubject realSubject = new RealSubject();还是采用Subject realSubject = new RealSubject();?其实,也就是说,Proxy是应该持有一个Subject对象还是应该持有一个RealSubject对象?

我在学习这个模式的时候,也有过这样的疑问,看到好多代码,有的是采用第一种方式,有的是采用第二种方式。那么究竟采用哪种方式比较好呢?其实要视情况确定的。

情况一:如果在Subject(抽象类或接口)中声明的所有方法和RealSubject对外提供的所有方法相同,那么原则上两种声明方式都可以。但是考虑到面向接口编程原则,而不是面向实现编程,声明为Subject realSubject;即持有一个Subject对象,会更好一些。


情况二:如果想要实现一个代理类(Proxy)可以同时代理多个实现类(RealSubject)而不需要修改Proxy的话,那么,Proxy采用Subject realSubject;即持有一个Subject对象,会好很多。而且,我们需要在Proxy的构造函数中指定其所指代的具体是哪个实现类(RealSubject)。这样Client在创建代理类对象的时候,需要传入一个他通过代理想要操作的对象。如:

代理类(Proxy)部分代码:

public class Proxy implements Subject{
    private Subject realSubject;
    public Proxy(Subject realSubject){
            this.realSubject = realSubject;
    }
}


客户端(Client)部分代码:

Subject realSubject1 = new RealSubject1();
Subject realSubject2 = new RealSubject2();
Subject proxy1 = new Proxy(realSubject1);
Subject proxy2 = new Proxy(realSubject2);


看到这里,有的人可能又有疑问,既然Client已经获得了具体实现类(RealSubject)的对象,那么Client为什么不直接调用具体实现类中提供的方法呢?而且,将具体实现类的对象轻易地给Client,岂不是允许Client对具体实现类进行一些非法操作?


其实,你们想的对,这样做确实会出现这样的弊端,但是,我们是可以通过进一步设计消除这个弊端的。我们可以在RealSubject的外面再包一层代理,或者直接在RealSubject中添加一层判断处理,用于判断调用方法的来源是否是Proxy,如果不是Proxy就拒绝访问。也就是,使RealSubject只接受来自Proxy的方法调用。这样问题就解决了,但是貌似有点复杂了。


情况三:如果RealSubject对外提供的方法多于Subject中定义的方法,或者RealSubject中定义的方法和Subject中定义的方法不同,而此时Proxy的主要作用可能是隐藏起RealSubject的复杂操作或者Proxy只有调用RealSubject中存在而Subject中不存在的方法才能实现功能时,此时,声明为RealSubject realSubject = new RealSubject();即,持有一个RealSubject对象,可能会更合适。常见的应用有:包装数据库操作,提供数据库代理等。

疑问2:如何实现一个代理类(Proxy)不变,然后可以给这个代理分配不同的被代理对象(RealSubject),即不同的实现类。

这个问题的解答呢,请看疑问1中的情况二的说明。

疑问3:如何实现一个实现类(RealSubject),对应不同的代理类(Proxy),即实现同一对象,不同代理方式或代理功能等?

至于这个问题,要实现的不同代理方式或代理功能差异不是很大,完全可以考虑在原有代理类中添加控制代码,当然了,如果差异很大,或者为了更好地践行OCP原则,可以添加一个对实现类负责的代理类,或者添加一个对原有代理类负责的新代理类,然后在新添加的代理类中,进行相应的修改和设计。

六、总结

到这里呢,我们的代理模式的讲解,算是告一段落了。正如我前面所说,其实代理类涉及范围很广,绝不仅限于我们上面讨论的这些,除此之外还有很多方面需要学习与运用。

特别要强调一个有待进一步研究的方面,那就是关于动态代理模式的思考与分析,由于篇幅与时间问题,对于动态代理模式的分析,我们将在以后的设计模式系列博客中继续学习,希望大家关注。同样,如果有任何疑问或者好的建议,欢迎你留言,我们一起研究,一起进步。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: