代理模式
Posted Marvel_Will
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了代理模式相关的知识,希望对你有一定的参考价值。
12.1 一个游戏爱好者的经历:
几年前,感觉很无聊,于是就玩了一段时间的网络游戏,游戏名就不说了,要不就有做广告的嫌疑,反正就是打怪、升级、砍人、被人砍,然后继续打怪、升 级、打怪、升级······我花了两个月的时间升级到80级,已经很有成就感了,但是还被人杀死,高手到处都是,GM(Game Master,游戏管理员)也不管,对于咱这种非RMB玩家基本上都是懒得搭理。在这段时间我是体会到网络游戏的乐与苦,参与家族(工会)攻城,胜利后那 叫一个乐呀,感觉自己真是一个“狂暴战士”,无往不胜!那苦是什么呢?就是升级,为了升一级,就要到出杀怪,做任务,那个游戏还很变态,外挂管得很严,基 本上出个外挂,没了两天就开始封账号,不敢用,升级基本上都要靠自己手打,累呀!我曾经的记录是连着打了23个小时,睡觉在梦中还和大BOSS在PK。有 这样一段经历还是很有意思,作为架构师是不是可以把这段经历通过架构的方式记录下呢?当然可以了,我们把这段打游戏的过程系统化,非常简单的一个过程,如 图12-1所示。
图12-1 游戏过程
太简单了,定义一个接口IGamePlayer,是所有喜爱网络游戏的玩家,然后定义一个具体的实现类GamePlayer,实现每个游戏爱好者为了玩游戏要执行的功能。代码也非常简单,我们先来看IGamePlayer,如代码清单12-1所示。
代码清单12-1 游戏者接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
public interface IGamePlayer { //登录游戏 public void login(String user,String password); //杀怪,网络游戏的主要特色 public void killBoss(); //升级 public void upgrade(); } |
非常简单,定义了三个方法,分别是我们在网络游戏中最常用的功能:登录游戏、杀怪和升级,其实现类如代码清单12-2所示。
代码清单12-2 游戏者
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
|
public class GamePlayer implements IGamePlayer { private String name = "" ; //通过构造函数传递名称 public GamePlayer(String _name){ this .name = _name; } //打怪,最期望的就是杀老怪 public void killBoss() { System. out .println( this .name + "在打怪!" ); } //进游戏之前你肯定要登录吧,这是一个必要条件 public void login(String user, String password) { System. out .println( "登录名为" +user + " 的用户 " + this .name + "登录成功!" ); } //升级,升级有很多方法,花钱买是一种,做任务也是一种 public void upgrade() { System. out .println( this .name + " 又升了一级!" ); } } |
在实现类中通过构造函数传递进来玩家姓名,方便进行后期的调试工作。我们通过一个场景类来模拟这样的游戏过程,如代码清单12-3所示。
代码清单12-3 场景类
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
|
public class GamePlayer implements IGamePlayer { private String name = "" ; //通过构造函数传递名称 public GamePlayer(String _name){ this .name = _name; } //打怪,最期望的就是杀老怪 public void killBoss() { System. out .println( this .name + "在打怪!" ); } //进游戏之前你肯定要登录吧,这是一个必要条件 public void login(String user, String password) { System. out .println( "登录名为" +user + " 的用户 " + this .name + "登录成功!" ); } //升级,升级有很多方法,花钱买是一种,做任务也是一种 public void upgrade() { System. out .println( this .name + " 又升了一级!" ); } } |
程序记录了游戏的开始时间和结束时间,同时也记录了在游戏过程中都需要做什么事情,运行结果如下:
开始时间是:2009-8-25 10:45
登录名为zhangSan 的用户 张三登录成功!
张三在打怪!
张三 又升了一级!
结束时间是:2009-8-26 03:40
运行结果也是我们想要的,记录我这段时间的网游生涯。心理学家告诉我们,人类对于苦难的记忆比对喜悦的记忆要深刻,但是人类对于喜悦是“趋利”性的,每个 人都想Happy,都不想让苦难靠近,要想获得幸福,苦难也是再所难免的,我们的网游生涯也是如此,游戏打时间长了,腰酸背痛、眼涩干枯、手臂酸麻,等 等,也就是网络成瘾综合症都出来了,其结果就类似吃了那个“一日丧命散”,“筋脉逆流,胡思乱想,而致走火入魔”。那怎么办呢?我们想玩游戏,但又不想碰 触到游戏中的烦恼?如何解决呢?
有办法,现在游戏代练的公司非常多,我把自己的账号交给代练人员,由他们去帮我升级,去打怪,非常好的想法,我们来修改一下类图,如图12-2所示。
图12-2 游戏代练帮忙打怪
在类图中增加了一个GamePlayerProxy类来代表游戏代练者,它也不能有作弊的方法呀,游戏代练者也是手动打怪呀,因此同样继承IGamePlayer接口,其实现如代码清单12-4所示。
代码清单12-4 代练者
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
|
public class GamePlayerProxy implements IGamePlayer { private IGamePlayer gamePlayer = null ; //通过构造函数传递要对谁进行代练 public GamePlayerProxy(IGamePlayer _gamePlayer){ this .gamePlayer = _gamePlayer; } //代练杀怪 public void killBoss() { this .gamePlayer.killBoss(); } //代练登录 public void login(String user, String password) { this .gamePlayer.login(user, password); } //代练升级 public void upgrade() { this .gamePlayer.upgrade(); } } |
很简单,首先通过构造函数说明要代谁打怪升级,然后通过手动开始代用户打怪、升级。场景类Client代码也稍作改动,如代码清单12-5所示。
代码清单12-5 改进后的场景类
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
|
public class Client { public static void main(String[] args) { //定义一个痴迷的玩家 IGamePlayer player = new GamePlayer( "张三" ); //然后再定义一个代练者 IGamePlayer proxy = new GamePlayerProxy(player); //开始打游戏,记下时间戳 System. out .println( "开始时间是:2009-8-25 10:45" ); proxy.login( "zhangSan" , "password" ); //开始杀怪 proxy.killBoss(); //升级 proxy.upgrade(); //记录结束游戏时间 System. out .println( "结束时间是:2009-8-26 03:40" ); } } |
运行结果也完全相同,还是张三这个用户在打怪,运行结果如下:
开始时间是:2009-8-25 10:45
登录名为zhangSan 的用户 张三登录成功!
张三在打怪!
张三 又升了一级!
结束时间是:2009-8-26 03:40
是的,没有任何改变,但是你有没有发觉,你的游戏已经在升级,有人在帮你干活了!终于升级到120级,然后基本上是本服无敌手,除了GM外,这个你可惹不起!这就是代理模式。
12.2 代理模式的定义
代理模式(Proxy Pattern)是一个使用率非常高的模式,其定义如下:
provide a surrogate or placeholder for another object to control access to it. 为其他对象提供一种代理以控制对这个对象的访问。
代理模式的通用类图如图12-3所示。
图12-3 代理模式的通用类图
代理模式也叫做委托模式,它是一项基本设计技巧,许多其他的模式,如状态模式、策略模式、访问者模式本质上是在更特殊的场合采用了委托模式,而且在日常的 应用中,代理模式可以提供非常好的访问控制,在一些著名开源软件中也经常见到它的身影,如Struts2的Form元素映射就采用了代理模式(准确的说是 动态代理模式)。我们先看一下类图中的三个角色的定义:
- Subject抽象主题角色
抽象主题类可以是抽象类也可以是接口,是一个最普通的业务类型定义,无特殊要求。
- RealSubject 具体主题角色
也叫做被委托角色、被代理角色,它才是冤大头,是业务逻辑的具体执行者。
- Proxy 代理主题角色
也叫做委托类、代理类,它负责对真实角色的应用,把所有抽象主题类定义的方法限制委托给真实主题角色实现,并且在真实主题角色处理完毕前后做预处理和善后处理工作。
我们首先来看Subject抽象主题类的通用源码,如代码清单12-6所示。
代码清单12-6 抽象主题类
1
2
3
4
5
6
7
|
public interface Subject { //定义一个方法 public void request(); } |
在接口中我们定义了一个方法request来作为方法的代表,RealSubject对它进行实现,如代码清单12-7所示。
代码清单12-7 真实主题类
1
2
3
4
5
6
7
8
9
10
11
|
public class RealSubject implements Subject { //实现方法 public void request() { //业务逻辑处理 } } |
RealSubject是一个正常的业务实现类,代理模式的核心就在代理类上,如代码清单12-8所示。
代码清单12-8 代理类
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
|
public class Proxy implements Subject { //要代理哪个实现类 private Subject subject = null ; //默认被代理者 public Proxy(){ this .subject = new Proxy(); } //通过构造函数传递代理者 public Proxy(Object...objects ){ } //实现接口中定义的方法 public void request() { this .before(); this .subject.request(); this .after(); } //预处理 private void before(){ //do something } //善后处理 private void after(){ //do something } } |
看到这里,大家别惊讶,为什么会出现before和after方法,继续看下去,这是一个“引子”,能够引出一个崭新的编程模式。
一个代理类可以代理多个被委托者或被代理者,因此一个代理类具体代理哪个真实主题角色,是由场景类决定的,当然,最简单的情况就是一个主题类一个代理类, 这是最简洁的代理模式。在通常情况下,一个接口只需要一个代理类就可以了,具体代理哪个实现类由高层模块来决定,也就是在代理类的构造函数中传递被代理 者,例如我们可以在代理类Proxy中增加如代码清单12-9所示的构造函数。
代码清单12-9 代理的构造函数
1
2
3
4
5
|
public Proxy(Subject _subject){ this .subject = _subject; } |
你要代理谁,产生该代理的实例,然后把被代理者传递进来,该模式在实际的项目应用中比较广泛。
12.3 代理模式的应用
12.3.1 代理模式的优点
- 职责清晰
真实的角色就是实现实际的业务逻辑,不用关心其他非本职责的事务,通过后期的代理完成一件完成事务,附带的结果就是编程简洁清晰。
- 高扩展性
具体主题角色是随时都会发生变化的,只要它实现了接口,甭管它如何变化,都逃不脱如来佛的手掌(接口),那我们的代理类完全就可以在不做任何修改的情况下使用。
- 智能化
这在我们以上讲解中还没有体现出来,不过在我们以下的动态代理章节中你就会看到代理的智能化,读者有兴趣也可以看看Struts是如何把表单元素映射到对象上的。
12.3.2 代理模式的应用
我相信第一次接触到代理模式的读者肯定很郁闷,为什么要用代理呀,是的,为什么要用代理?想想现实世界吧,你为什么要找代理律师,你去打官司,为什 么要找个律师?因为你不想参与中间过程的是是非非,只要完成自己的答辩就成,其他的比如事前调查、事后追查都由律师来搞定,这就是为了减轻你的负担。代理 模式使用非常多,大家可以看看Spring AOP,这是一个非常典型的动态代理。
12.4 代理模式的扩展
12.4.1 普通代理
在网络上代理服务器设置分为透明代理和普通代理,是什么意思呢?透明代理就是用户不用设置代理服务器地址,就可以直接访问,也就是说代理服务器对用户来说 透明的,看不到,不用知道它存在的;普通代理则是需要用户自己设置代理服务器的IP地址,用户必须知道代理的存在。我们设计模式中的普通代理和强制代理也 是类似的一种结构,普通代理就是我们要知道代理的存在,也就是类似的GamePlayerProxy这个类的存在,然后才能访问;强制代理则是调用者直接 调用真实角色,而不用关心代理是否存在,其代理的产生是由真实角色决定的,这样解释还是比较复杂,我们还是用实例来讲解。
首先说普通代理,它的要求就是客户端只能访问代理角色,而不能访问真实角色,这是比较简单的,我们以上面的例子作为扩展,我自己作为一个游戏玩家,我肯定 自己不练级了,也就是场景类不能再直接new一个GamePlayer对象了,它必须由GampePlayerProxy来进行模拟场景,类图修改如图 12-4所示。
图12-4 普通代理类图
改动很小,仅仅修改了两个实现类的构造函数,GamePlayer的构造函数增加了_gamePlayer参数,而代理角色则只要传入代理者名字即可,而不需要说是替哪个对象做代理。GamePlayer类如代码清单12-10所示。
代码清单12-10 普通代理的游戏者
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
|
public class GamePlayer implements IGamePlayer { private String name = "" ; //构造函数限制谁能创建对象,并同时传递姓名 public GamePlayer(IGamePlayer _gamePlayer,String _name) throws Exception{ if (_gamePlayer == null ){ throw new Exception( "不能创建真是角色!" ); } else { this .name = _name; } } //打怪,最期望的就是杀老怪 public void killBoss() { System. out .println( this .name + "在打怪!" ); } //进游戏之前你肯定要登录吧,这是一个必要条件 public void login(String user, String password) { System. out .println( "登录名为" +user + " 的用户 " + this .name + "登录成功!" ); } //升级,升级有很多方法,花钱买是一种,做任务也是一种 public void upgrade() { System. out .println( this .name + " 又升了一级!" ); } } |
在构造函数中,传递进来一个IGamePlayer对象,检查谁能创建真实的角色,当然还可以有其他的限制,比如类名必须为Proxy类等等,读者可以根据实际情况进行扩展。GamePlayerProxy如代码清单12-11所示。
代码清单12-11 普通代理的代理者
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
|
public class GamePlayerProxy implements IGamePlayer { private IGamePlayer gamePlayer = null ; //通过构造函数传递要对谁进行代练 public GamePlayerProxy(String name){ try { gamePlayer = new GamePlayer( this ,name); } catch (Exception e) { // TODO 异常处理 } } //代练杀怪 public void killBoss() { this .gamePlayer.killBoss(); } //代练登录 public void login(String user, String password) { this .gamePlayer.login(user, password); } //代练升级 public void upgrade() { this .gamePlayer.upgrade(); } } |
仅仅修改了构造函数,传递进来一个代理者名称,即可进行代理,在这种改造下,系统更加简洁了,调用者只知道代理存在就可以,不用知道代理了谁。同时场景类也稍作改动,如代码清单12-12所示。
代码清单12-12 普通代理的场景类