诗有可解不可解,若镜花水月勿泥其迹可也
—— 谢榛
文章目录
- 定义
- 图纸
- 一个例子:图片搜索器
- 图片加载
- 搜索器
- 直接在Image添加
- 组合他们
- 各种各样的代理
- 远程代理:镜中月,水中花
- 保护代理:对象也该有隐私
- 引用代理:我什么时候可以动手?
- 虚拟代理:我们真的需要全部信息吗?
- 碎碎念
- 代理和装饰器
- 最后一点碎碎念:代理和围墙
定义
为其他对象提供一种代理以控制对这个对象的访问
图纸
一个例子:图片搜索器
某天,你突发奇想,想做一个可以展示指定文件夹内所有图片的桌面应用。这个应用很简单,遍历文件夹,发现图片文件,把图片加载到GUI上的图片列表里,显示图片名和图片,就像这样:
图片加载
为了实现这样的效果,你编写了自己的 Image 类簇,并给予他一个通过 InputStream 来载入图片并获取图片信息的方法,就像这样:
我们可以通过 Image 中的 loadByStream 方法把参数输入流内的图片文件加载到内存中来,并把获取到的信息写到 messageMap 中,根据需要获取里面的内容反馈给 client
至此,不出意外的话我们根据 Image对象 提供的信息绘制出GUI后就可以得到截图类似的效果
搜索器
随着文件夹里面的图片越来越多,在里面找到你需要的变得越来越困难,于是新的想法出现了,你想要加一个搜索框,用于筛选读取到的图片
这个需求很合理,但是实现起来却出现了问题
Image 是通过把图片载入到内存的方式来获取图片信息的,这就意味着我要获取所有图片的文件名用于搜索之前必须加载所有的图片,这是无法接受的
经过我们分析,只要不把文件加载到内存里,只是遍历文件夹获取其中的所有文件的文件名是很快的,通过File
也就是说,对于一个文件来说,他在程序里会同时拥有对应的 Image对象 和 File对象
我当然希望这两个对象可以绑定在一起,那该怎么做呢?
直接在Image添加
直接添加到Image中,就像这样:
看起来很美好,也很必要
可是我要为将来考虑,这个程序里面的 Image 不一定都从硬盘上读取文件,我允许他从任何输入流中加载图片出来
这种时候file对象的存在就显得很尴尬,而且会导致getName方法的异常,我又得给他写 if(file==null)……
这显然是很糟糕的设计
组合他们
既然直接添加不可以,那很显然就只能用另一个类来把他们组合起来
但是怎么组合,有讲究
组合他们的这个类本质上还是Image
,我并不是为了用这个类的对象来复制/删除文件……他的任务是包含了 Image 的任务的
所以我们可以考虑这样做:
public abstract class AbstractImage {public static final String KEY_NAME = "name";public static final String KEY_WIDTH = "width";protected final Map<String,Object> messageMap;public AbstractImage() {messageMap = new HashMap<>();}public abstract void loadByStream(InputStream is);public abstract String getName();public abstract Double getWidth();……
}public class Image extends AbstractImage {private volatile boolean isLoaded = false;//是否加载完毕@Overridepublic void loadByStream(InputStream is) {通过input流载入图片 载入后把isLoaded改为true}@Overridepublic String getName() {return isLoaded ? messageMap.get(KEY_NAME).toString() : null;}@Overridepublic Double getWidth() {Object o = messageMap.get(KEY_WIDTH);return isLoaded ? Double.parseDouble(o.toString()) : null;}
}public class ImageFileProxy extends AbstractImage {private final Image image;private File file;public ImageFileProxy(Image image) {this.image = image;}public void loadByFile(File file) throws FileNotFoundException {this.file = file;loadByStream(new FileInputStream(file));}@Overridepublic void loadByStream(InputStream is) {throw new RuntimeException("ImageFileProxy 中的loadByStream无法直接调用");}@Overridepublic String getName() {return file == null ? null : file.getName();}@Overridepublic Double getWidth() {return image.getWidth();}
}
我们为 AbstractImage 增加了了一个新子类 ImageFileProxy,这个类接收一个 Image 对象作为基底,绝大多数方法中他会直接调用传入的 Image对象 的实现,比如说当你调用 getWidth 的时候,其实和直接调用 Image 对象没有什么区别
当你访问特定的方法的时候(本例中的 getName),ImageFileProxy对象 内藏着的 File对象 会发挥作用,这样就实现了在没有把图片加载到内存的时候就可以获取到图片名称,从而实现边加载边查询
而当你直接访问 ImageFileProxy 中的 loadByStream 方法时,程序会直接报错,不允许你通过这种方式去直接加载图片
好,现在我们知道了上面的结构是怎么运作的,但为什么要这样写呢?
来看看 AbstractImage 的三个方法在 ImageFileProxy 中是怎么被实现的
-
getWidth
不做任何修改,直接调用被代理的 Image对象 的实现
-
getName
直接无视被代理对象的实现,完全改变接口的功能
-
loadByStream
不允许client直接访问,这相当于“封装”了这个方法。我们通过对 Image 中方法的访问进行“监视”的方式,来保护 ImageFileProxy对象状态 的完整性
不要小看这种写法,将来如果你突发奇想想在加载图片的时候增加一个加载进度条,也可以直接新增一个 AbstractImage 的代理子类来实现这样的需求,对已有的代码不会有什么影响。
而这正是一个标准的代理模式实现
各种各样的代理
远程代理:镜中月,水中花
远程代理是指在不同的地址空间里提供对相同内容的局部代表
是不是觉得这个定义老复杂了,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 一样。
碎碎念
代理和装饰器
你可能已经发现了,上例中的 ImageFileProxy 和 Image 的关系不只是组合,他完全包装了后者。client 想要访问 Image 一定会受到 ImageFileProxy 的监视。这一点上装饰器也是这样的,装饰器包装别的对象的接口,为其添加职能
这样看起来代理和装饰器何其相似,但他们的区别也就在刚刚那句话里
请注意我的用词,对于代理,我说的是 监视
;对于装饰器,我说的是 添加职能
这就是两者最大的区别:
- 对代理来说,被代理对象的接口是被控制的,代理类可以决定整个调用过程最终的走向
- 对装饰器来说,被装饰对象的接口是必须被调用的,装饰器只是为了给他添加功能
所以即使他们的实现很相似,但他们依然是不同的两种模式,因为目的截然不同
最后一点碎碎念:代理和围墙
围城里有句经典台词:城里的人想出去,城外的人想进来。
代理模式就像把守这座城池的卫兵一样,所有人出入这座城都要经过代理对象的盘问,以决定是否放行
对于围墙内的部分(被代理的部分)来说,难免会觉得卫兵不近人情,侵犯隐私,和围墙外的人相比毫无自由
而对围墙外的部分(client)来说,却觉得墙里个个是被保护得很好的花朵,虽然自由有了边界,却换回了惬意的生活
你说谁更幸福呢?
其实取决你自己,如果你在里面的时候只能看到围墙里的狼狈不堪;相信我,就算你诞生在外面,也只能看到围墙之外的一地鸡毛
万分感谢您看完这篇文章,如果您喜欢这篇文章,欢迎点赞、收藏。还可以通过专栏,查看更多与【设计模式】有关的内容