诗有可解不可解,若镜花水月勿泥其迹可也
—— 谢榛
文章目录
- 定义
- 图纸
- 一个例子:图片搜索器
- 图片加载
- 搜索器
- 直接在Image添加
- 组合他们
- 各种各样的代理
- 远程代理:镜中月,水中花
- 保护代理:对象也该有隐私
- 引用代理:我什么时候可以动手?
- 虚拟代理:我们真的需要全部信息吗?
定义
为其他对象提供一种代理以控制对这个对象的访问
图纸
一个例子:图片搜索器
某天,你突发奇想,想做一个可以展示指定文件夹内所有图片的桌面应用。这个应用很简单,遍历文件夹,发现图片文件,把图片加载到GUI上的图片列表里,显示图片名和图片,就像这样:
图片加载
为了实现这样的效果,你编写了自己的 Image 类簇,并给予他一个通过 InputStream 来载入图片并获取图片信息的方法,就像这样:
我们可以通过 Image 中的 loadByStream 方法把参数输入流内的图片文件加载到内存中来,并把获取到的信息写到 messageMap 中,根据需要获取里面的内容反馈给 client
至此,不出意外的话我们根据 Image对象 提供的信息绘制出GUI后就可以得到截图类似的效果
搜索器
随着文件夹里面的图片越来越多,在里面找到你需要的变得越来越困难,于是新的想法出现了,你想要加一个搜索框,用于筛选读取到的图片
这个需求很合理,但是实现起来却出现了问题
Image 是通过把图片载入到内存的方式来获取图片信息的,这就意味着我要获取所有图片的文件名用于搜索之前必须加载所有的图片,这是无法接受的
经过我们分析,只要不把文件加载到内存里,只是遍历文件夹获取其中的所有文件的文件名是很快的,通过File
也就是说,对于一个文件来说,他在程序里会同时拥有对应的 Image对象 和 File对象
我当然希望这两个对象可以绑定在一起,那该怎么做呢?
直接在Image添加
直接添加到Image中,就像这样:
看起来很美好,也很必要
可是我要为将来考虑,这个程序里面的 Image 不一定都从硬盘上读取文件,我允许他从任何输入流中加载图片出来
这种时候file对象的存在就显得很尴尬,而且会导致getName方法的异常,我又得给他写 if(file==null)……
这显然是很糟糕的设计
组合他们
既然直接添加不可以,那很显然就只能用另一个类来把他们组合起来
但是怎么组合,有讲究
组合他们的这个类本质上还是Image
,我并不是为了用这个类的对象来复制/删除文件……他的任务是包含了 Image 的任务的
所以我们可以考虑这样做:
我们创建了一个 Image类 的 代理类 ImageFileProxy,这个代理类本质上还是 Image,当你调用他的 loadByStream 或者 getWidth 之类的方法时,他直接会去调用 super 对象的对应方法
而当你访问 getName 的时候,他里面藏着的 File 会处理对应的业务,他才不管你现在有没有把图片加载完。这样一来就可以实现一边加载一边查询了
不要小看这种写法,将来如果你突发奇想想在加载图片的时候增加一个加载进度条,那也只是新增Image的子类而已,对已有的代码不会有什么影响
那你会说,这种实现跟代理的图纸差别太大了,这不就是我平时用的继承吗?
你可以试着把Image的接口抽象出来,并在ImageFileProxy的构造方法中要求传入Image对象,就像这样:
是不是发现他成了一个标准的代理实现?
各种各样的代理
远程代理:镜中月,水中花
远程代理是指在不同的地址空间里提供对相同内容的局部代表
是不是觉得这个定义老复杂了,emmmm,举个例子,比如说数组的复制,就像这样:
数组A里存着 X/Y/Z 三个对象,接着我们复制数组A得到数组B。数组A和B的内存地址当然是不一样的,但B里面存的还是 X/Y/Z。操作B,其实跟操作A没什么区别,其实此时B就能算是A的一个远程代理
真正让远程代理广为人知的是网络相关的开发
比如说现在我有Java写成的 服务器和N个客户机,我希望在服务器上有个按钮,点击后可以直接获取客户机上的硬件信息。
要做成这个效果,根据不同的连接方式,实现方法各不相同。其中一种是利用 RMI
技术,让服务器直接调用客户机上运行的对象里的方法,并获取结果,这时候其实就是在服务器上建立客户机的 远程代理
是的,你没看错,我说的就是在服务器JVM上调用运行在其他JVM上的对象。这不是魔法,是真实可行的技术,同时他也是分布式的基础,也是远程代理大放异彩的舞台
保护代理:对象也该有隐私
当你需要管理N个具有相同根类对象的时候,十有八九会用到 容器,List也好,Set也好,或者数组、Map 这不重要
重要的是这些容器对象拥有对自己所存储的对象的完全掌控权,我的意思是说,client 可以随自己喜好对容器里面的内容增删改查,这完全不受控
不是所有的容器都允许随意往里添加或删除的内容的,这时候你就需要隐藏容器的某些接口
我们会再建一个类把真正的容器类封装起来,接着不提供被隐藏的接口的访问方式(或者根据不同的权限提供不同的行为)。而 client 只能和外部的 代理类
交互,至此实现对容器的保护,这就是保护代理
引用代理:我什么时候可以动手?
如果说工厂方法之类的模式提供了对一个对象的 创建行为的包装
的话,那么引用代理就是 对一个对象提供从创建到销毁全方面的包装
因为 client 只能通过代理类对象来访问被代理的对象,那么所有对被代理的对象的访问都是在代理类对象的监控之下的。只要你想,你可以知道被代理对象现在被多少个地方引用,他有没有进入某个有锁的区域,也可以决定被代理对象什么时候被初始化……
知道这些信息是有用的,打个比方:
- 知道多少个引用:可以做引用计数、可以在丢失所有引用时释放资源
- 了解状态是否被锁:可以控制别的对象不允许修改被代理对象的状态
- 决定何时初始化:是第一次被访问时初始化?还是跟代理类对象一起被初始化?
引用代理可以给你的程序提供很多很偏门的优化手段哒
虚拟代理:我们真的需要全部信息吗?
代理模式可以提供对对象的一种访问控制
这种控制可以是限制对象公开的接口(保护代理);也可以用来管理被代理对象何时释放(引用代理)
他甚至可以做到在某个内容没有被加载进来的情况下展示他的一部分,这就是虚拟代理
试想以下,当我们需要获取一个文件的名字和修改时间,有必要把整个文件都加载到内存里过一遍吗?
答案必然是否定的。要不然你打开【我的电脑】里面的目录一定要很久(因为你打开的时候需要过一遍文件夹里所有的文件)
在Java程序里,我们用 File 来表示一个文件,通过 File 对象的方法我们可以获取到和他所代表的文件有关的各种信息
那么请问,File 在获取硬盘上的文件的信息的时候,真的每次都会把整个文件加载到程序中吗?
肯定没有啊。倒不如说在你用 IO流 让这个文件流入程序之前,硬盘上的那个文件根本没有被载入
也就是说 File对象 和硬盘上的那个文件之间,也存在一种代理关系
File 为我们提供了一系列操作文件和读取文件信息的接口;但是换句话来说, File对象 也控制着我们访问文件的路径和方式,让我们一定是按照 File类 的编写者的想法去跟文件交互,就像 getter&setter 一样。