代理模式(Proxy Pattern)允许我们为真实对象创建代理,如果您想与某个对象对话,将通过其代理与其对话,此代理程序接收您的消息并将其转发给目标对象。
它有什么好处?允许我们在将消息发送至目标对象前执行一些感兴趣的任务,例如日志、权限控制、缓存等。
问题提出
假设我们要构建一个电子书阅读程序,它有一个图书库,我们购买的所有的电子书均在此图书库中。
创建Ebook类,它有私有属性fileName,私有方法load用于加载书的内容,在构造函数中调用load方法初始化加载书的内容。show方法用于显示书的内容。getFileName方法用于获取书名。
创建Library类,用于保存电子书。我们使用Map存储,方便检索。add方法用于添加电子书,openEbook方法用于打开某本电子书。
到Main进行测试。创建一个Library对象,加载a、b、c三本电子书。然后打开电子书 a。
如果我们有几百本电子书,我们的程序也会将所有的书从磁盘里读出并存储到内存里,正如这个示例展示的,我们并不会打开每一本书的,我们很可能只会打开阅读其中的一本书,显然加载全部的电子书是不必要的成本开销。
这就是代理模式发挥作用的地方。
利用代理模式,我们可以创建一个模拟真实电子书(Ebook)的对象,但它只在我们需要阅读时才加载到内存。
解决方案
当前我们的结构是这样的。
Library类持有Ebook,即会将所有Ebook加载到内存。我们修改这个结构添加一个代理EbookProxy,使其只在我们需要的时候才加载,这个结构被称为延迟初始化。
这种方法用在很多框架里,比如hibernate框架。
我们来整理其层次结构。
我们引入Ebook接口,它定义了show()方法,实际的电子书RealEbook和代理EbookProxy均实现了该接口。EbookProxy有一个私有属性ebook,代表实际的电子书对象。这个私有属性ebook是个关键点,我们不会一开始就加载电子书内容,而是初始化为null。在EbookProxy的show方法里,我们检查ebook是否是实际的电子书对象,如果不是,就创建它,然后再调用实际电子书的show方法。
利用这种结构,我们仅在我们需要的时候才真正加载电子书。一开始仅列出电子书名称并不加载到内存。在实际需要打开电子书时,才从磁盘读出电子书并加载。
Library类仅依赖Ebook接口,符合开放-闭合原则,我们可以有不同的Ebook接口的代理实现。
目前我们的代理实现了延迟初始化,我们也可以创建其他我们感兴趣的代理,比如授权:我们检查电子书的租赁期是否到期,如果到期的话我们将不允许访问该电子书;比如日志记录:我们记录其每一步操作。
这种结构就被称为代理模式(Proxy Pattern)。
四人帮GOF的经典命名结构是这样的。
我们看到Proxy的request的方法里,我们可以执行一些感兴趣的任务,然后调用实际的RealSubject的request方法。
这种模式的重点是代理Proxy对象看起来跟实际对象是一样的,因为它们都实现了相同的接口,代理可以做任何它感兴趣的事情。
代码实现
首先,我们要抽取出实际电子书和代理电子书一致的接口。
将原Ebook类改名为RealEbook。创建接口Ebook,定义了getFileName和show两个方法。实际电子书RealEbook和电子书代理EbookProxy都需要实现Ebook接口。
以下是Ebook接口和RealEbook类。
创建电子书代理类EbookProxy,实现接口Ebook。它有fileName属性并在构造函数中进行初始化,另外还有一个ebook属性表示实际的电子书对象RealEbook。
这里有个关键点,ebook初始保持为null。在实现的show方法里,判断ebook是否为null,如果是则创建RealEbook对象,然后调用ebook对象的show方法。
到Main类进行测试。我们只需用电子书代理EbookProxy来替换原来的实际电子书即可。
我们看到,我们的图书库里有a、b、c三本书,我们只打开a书,实际也只加载了a书,说明它只加载需要打开的电子书,达到了我们的目的。
代理模式是符合开放闭合原则的。比如要实现日志记录的功能,我们只需创建新的代理类即可,不必修改原有的代码。
创建类LoggingEbookProxy,实现Ebook接口。像EbookProxy一样,我们添加fileName、ebook属性,在show方法里,添加日志功能。
回到Main类测试。我们只需将EbookProxy替换成LoggingEbookProxy,就有日志记录的功能了。
我们通过添加新类而不是修改原有的代码改变了应用程序的行为,这就是开放闭合的原则。
小结
代理模式(Proxy Pattern)允许我们为真实对象创建代理,并在将消息发送至目标对象前执行一些感兴趣的任务。