一口一口啃完Java中的24种设计模式---代理模式


代理模式的引入

东汉末年,大将军何进引董卓入京,想借西北王的军队对抗阉党,无奈自己先被阉党做掉,而后造成巨变,导致诸侯并起,最终形成三国鼎立局面。汉献帝即位后,初平三年(公元 192 年),治中从事毛玠向曹操建议“奉天子以令不臣”,曹操采纳了他的建议,迎接汉献帝来到许昌。汉献帝刘协在许都没有实际的权利,曹操不断地诛除公卿大臣,不断地集军政大权于一身。建安元年八月,曹操进驻洛阳,立刻趁张杨、杨奉兵众在外,赶跑了韩暹,接着做了三件事:杀侍中台崇、尚书冯硕等,谓“讨有罪”;封董承、伏完等,谓“赏有功”;追赐射声校尉沮俊,谓“矜死节”。然后在第九天趁他人尚未来得及反应的情况下,迁帝都许,使皇帝摆脱其他势力的控制。此后,他还加紧步伐剪除异己,提高自己的权势。他首先向最有影响力的三公发难,罢免太尉杨彪、司空张喜;其次诛杀议郎赵彦;再次是发兵征讨杨奉,解除近兵之忧;最后是一方面以天子名义谴责袁绍,打击其气焰,另一方面将大将军让予袁绍,稳定大敌。这就是历史上著名的“挟天子以令诸侯”。汉献帝与曹操的关系,是历史上两位伟大的政治家的联手,稳定了东汉政权,最终平稳交接给曹魏政权,也间接映射了“代理模式”

代理模式

代理模式使用代理对象完成用户请求,屏蔽用户对真实对象的访问。现实世界的代理人被授权执行当事人的一些事宜,无需当事人出面,从第三方的角度看,似乎当事人并不存在,因为他只和代理人通信。而事实上代理人是要有当事人的授权,并且在核心问题上还需要请示当事人。
在软件设计中,使用代理模式的意图也很多,比如因为安全原因需要屏蔽客户端直接访问真实对象,或者在远程调用中需要使用代理类处理远程方法调用的技术细节 ,也可能为了提升系统性能,对真实对象进行封装,从而达到延迟加载的目的。

代理模式角色分为 4 种:

  1. 主题接口:定义代理类和真实主题的公共对外方法,也是代理类代理真实主题的方法;

  2. 真实主题:真正实现业务逻辑的类

  3. 代理主题:用于代理和封装真实主题

  4. 客户端:面向主题接口,使用代理主题完成一些工作。

UML 图如下:

但是在实际开发中,代理类的实现比上述 UML 要复杂的多,代理模式根据其目的和实现方式不同可以分为很多种类,下面列举几种常用的代理模式:

(1) 远程代理(Remote Proxy):为一个位于不同的地址空间的对象提供一个本地的代理对象,这个不同的地址空间可以是在同一台主机中,也可是在另一台主机中,远程代理又称为大使(Ambassador)。

(2) 虚拟代理(Virtual Proxy):如果需要创建一个资源消耗较大的对象,先创建一个消耗相对较小的对象来表示,真实对象只在需要时才会被真正创建。

(3) 保护代理(Protect Proxy):控制对一个对象的访问,可以给不同的用户提供不同级别的使用权限。

(4) 缓冲代理(Cache Proxy):为某一个目标操作的结果提供临时的存储空间,以便多个客户端可以共享这些结果。

(5) 智能引用代理(Smart Reference Proxy):当一个对象被引用时,提供一些额外的操作,例如将对象被调用的次数记录下来等。

在这些常用的代理模式中,有些代理类的设计非常复杂,例如远程代理类,它封装了底层网络通信和对远程对象的调用,其实现较为复杂。

应用实例

结构分析

开发一个商务信息查询系统,要求如下:

(1) 在进行商务信息查询之前用户需要通过身份验证,只有合法用户才能够使用该查询系统;

(2) 在进行商务信息查询时系统需要记录查询日志,以便根据查询次数收取查询费用。

假设已经完成了商务信息查询模块的开发任务,现希望能够以一种松耦合的方式向原有系统增加身份验证和日志记录功能,客户端代码可以无区别地对待原始的商务信息查询模块和增加新功能之后的商务信息查询模块,而且可能在将来还要在该信息查询模块中增加一些新的功能。

根据上述 UML 图,对这个系统进行建模:

根据上述需求「已经开发完成的商务信息查询模块」在图中用 RealSearcher 表示,其中用于查询信息的方法为 doSearch ,
按照代理模式的思想,将 doSearch 这个方法给抽象出来,放置到 Search 接口中,然后 RealSearcher 去实现这个接口。

验证身份和记录次数这两个操作很明显一个是位于查询之前,一个是位于查询之后,并且作为一个商务信息查询系统,直接将 RealSearcher 开放给用户使用,明显是不安全的行为,所以此处提供一个 ProxySearcher 对象作为 RealSearcher 的代理,相同的去实现 Searcher 接口,并且在 ProxySearcher 当中可以封装好身份校验和查询次数记录的逻辑。

RealSeacher 和 ProxySeacher 都实现了 Searcher 接口,对外暴露的方法都是相同的,所以对于 Client 来说, 调用 RealSearcher 和 ProxySearcher 并没有实质性的区别,在代码上体现出来的就是,Client 面向的是 Search 接口,而不是它的实现类。

代码实现

Subject

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* @author: fanyuzeng on 2018/2/10 10:24
*/
public interface Searcher {
/**
* 调用查询系统的接口
*
* @param userId 用户名
* @param password 密码
* @return 查询结果
*/
String doSeach(String userId, String password);
}

RealSubject

1
2
3
4
5
6
7
8
9
10
11
/**
* @author: fanyuzeng on 2018/2/10 10:30
*/
public class RealSearcher implements Searcher {
@Override
public String doSeach(String userId, String password) {
//模拟查询信息
return "userName:"+ userId +"success some info:........";
}
}

ProxySubject

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
/**
* @author: fanyuzeng on 2018/2/10 10:33
*/
@Module
public class ProxySearcher implements Searcher {
private static final String TAG = "==ProxySearcher==";
//持有真正进行查询过程对象的引用
private RealSearcher mRealSearcher;
//验证用户身份的工具
private AccessValidator mAccessValidator;
//记录查询次数的工具
private Logger mLogger;
//这里代理对象使用单例模式
private static ProxySearcher sProxySearcher;
private ProxySearcher() {
mRealSearcher = new RealSearcher();
mAccessValidator = new AccessValidator();
mLogger = new Logger();
}
public static ProxySearcher getInstance() {
if (sProxySearcher == null) {
sProxySearcher = new ProxySearcher();
}
return sProxySearcher;
}
@Override
public String doSeach(String userId, String password) {
if (userId == null || password == null)
return "params empty";
String result = "search fail";
if (mAccessValidator != null) {
if (isVertify(userId)) {
if (mRealSearcher != null) {
//调用 RealSearcher 的 doReach 方法
result = mRealSearcher.doSeach(userId, password);
}
} else {
System.out.println("Access deny");
}
}
if (!result.equals("search fail")) {
logSearchCount(userId);
}
return result;
}
//检查用户身份是否合法
private boolean isVertify(String userId) {
return mAccessValidator.Validate(userId);
}
//记录查询次数
private void logSearchCount(String userId) {
mLogger.searchLog(userId);
}
}

Client

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* @author: fanyuzeng on 2018/2/10 10:53
*/
public class Client {
private static Searcher mSearcher;
public static void main(String[] args) {
if (mSearcher == null) {
mSearcher = getSearchInstance();
}
mSearcher.doSeach("ZFY", "blabala");
}
//写入配置文件当中,然后通过反射调用,灵活性更高
private static Searcher getSearchInstance() {
return ProxySearcher.getInstance();
}
}

辅助类,验证身份、记录查询次数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* 身份验证类
*
* @author: fanyuzeng on 2018/2/10 10:33
*/
public class AccessValidator {
private static final String TAG = "==AccessValidator==";
/**
* 模拟实现登录验证
*
* @param userId
* @return
*/
public boolean Validate(String userId) {
System.out.println("在数据库中验证用户" + userId + "是否是合法用户?");
if (userId.equals("ZFY")) {
System.out.println("登录成功!" + userId);
return true;
} else {
System.out.println("登录失败!" + userId);
return false;
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 用于记录查询次数
*
* @author: fanyuzeng on 2018/2/10 10:37
*/
public class Logger {
private static final String TAG = "==Logger==";
/**
* 模拟记录查询次数
*
* @param userId
*/
public void searchLog(String userId) {
System.out.println("更新数据库,用户:" + userId + " 查询次数+1!");
}
}

输出

共82.3k字
0%
.gt-container a{border-bottom: none;}