12.1 我是游戏至尊

2007年,感觉很无聊,于是就玩了一段时间的网络游戏,游戏名就不说了,反正就是打怪、升级、砍人、被人砍,然后继续打怪、升级、打怪、升级……我花了两个月的时间升到80级,已经很有成就感了,但是还会被人杀死,高手到处都是,GM(Game Master,游戏管理员)也不管,对于咱这种非RMB玩家基本上都是懒得搭理。在这段时间我是体会到网络游戏的乐与苦,参与家族(工会)攻城,胜利后那叫一个乐呀,感觉自己真是一个“狂暴战士”,无往不胜!那苦是什么呢?就是升级,为了升一级,就要到处杀怪,做任务,那个游戏还很变态,外挂管得很严,基本上出个外挂,没两天就开始封账号,不敢用,升级基本上都要靠自己手打,累呀!我曾经的记录是连着打了23个小时,睡觉在梦中还和大BOSS在PK。有这样一段经历还是很有意思的,作为架构师是不是可以把这段经历通过架构的方式记录下来呢?当然可以了,我们把这段打游戏的过程系统化,非常简单的一个过程,如图12-1所示。

image-20210928170514558

图12-1 游戏过程
太简单了,定义一个接口IGamePlayer,是所有喜爱网络游戏的玩家,然后定义一个具体的实现类GamePlayer,实现每个游戏爱好者为了玩游戏要执行的功能。代码也非常简单,我们先来看IGamePlayer,如代码清单12-1所示。

代码清单12-1 游戏者接口

1
2
3
4
5
6
7
8
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
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
public class Client {
public static void main(String[] args) {
//定义一个痴迷的玩家
IGamePlayer player = new GamePlayer("张三");

//开始打游戏,记下时间戳
System.out.println("开始时间是:2009-8-25 10:45");
player.login("zhangSan", "password");
//开始杀怪
player.killBoss();
//升级
player.upgrade();
//记录结束游戏时间
System.out.println("结束时间是:2009-8-26 03:40");
}
}

程序记录了游戏的开始时间和结束时间,同时也记录了在游戏过程中都需要做什么事情,运行结果如下:

1
2
3
4
5
开始时间是:2009-8-25 10:45 
登录名为zhangSan 的用户 张三登录成功!
张三在打怪!
张三 又升了一级!
结束时间是:2009-8-26 03:40

运行结果也是我们想要的,记录我这段时间的网游生涯。心理学家告诉我们,人类对于苦难的记忆比对喜悦的记忆要深刻,但是人类对于喜悦是“趋利”性的,每个人都想Happy, 都不想让苦难靠近,要想获得幸福,苦难也是在所难免的,我们的网游生涯也是如此。游戏打时间长了,腰酸背痛、眼睛干涩、手臂酸麻,等等,也就是网络成瘾综合症都出来了。其结果就类似吃了那个“一日丧命散”,“筋脉逆流,胡思乱想,而致走火入魔”。那怎么办呢? 我们想玩游戏,但又不想碰触到游戏中的烦恼,如何解决呢?有办法,现在游戏代练的公司非常多,我把自己的账号交给代练人员,由他们去帮我升级,去打怪,非常好的想法,我们来修改一下类图,如图12-2所示。

image-20210928170815982

图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
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
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");
}
}

运行结果也完全相同,还是张三这个用户在打怪,运行结果如下:

1
2
3
4
5
开始时间是: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所示。

image-20210928171355793

图12-3 代理模式的通用类图

代理模式也叫做委托模式,它是一项基本设计技巧。许多其他的模式,如状态模式、策略模式、访问者模式本质上是在更特殊的场合采用了委托模式,而且在日常的应用中,代理模式可以提供非常好的访问控制。在一些著名开源软件中也经常见到它的身影,如Struts2的Form元素映射就采用了代理模式(准确地说是动态代理模式)。我们先看一下类图中的三个角色的定义:

  • Subject抽象主题角色

抽象主题类可以是抽象类也可以是接口,是一个最普通的业务类型定义,无特殊要求。

  • RealSubject具体主题角色

也叫做被委托角色、被代理角色。它才是冤大头,是业务逻辑的具体执行者。

  • Proxy代理主题角色

也叫做委托类、代理类。它负责对真实角色的应用,把所有抽象主题类定义的方法限制委托给真实主题角色实现,并且在真实主题角色处理完毕前后做预处理和善后处理工作。

我们首先来看Subject抽象主题类的通用源码,如代码清单12-6所示。

代码清单12-6 抽象主题类

1
2
3
4
public interface Subject {
//定义一个方法
public void request();
}

在接口中我们定义了一个方法request来作为方法的代表,RealSubject对它进行实现,如代码清单12-7所示。

代码清单12-7 真实主题类

1
2
3
4
5
6
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
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
public Proxy(Subject _subject){
this.subject = _subject;
}

你要代理谁就产生该代理的实例,然后把被代理者传递进来,该模式在实际的项目应用中比较广泛。

13.1 个性化电子账单

现在电子账单越来越流行了,比如你的信用卡,每到月初的时候银行就会发一份电子邮件给你,说你这个月消费了多少,什么时候消费的,积分是多少等,这是每个月发一次。还有一种也是银行发的邮件你肯定非常有印象:广告信,现在各大银行的信用卡部门都在拉拢客户,电子邮件是一种廉价、快捷的通信方式,你用纸质的广告信那个费用多高呀,比如我行今天推出一个信用卡刷卡抽奖活动,通过电子账单系统可以一个晚上发送给600万客户, 为什么要用电子账单系统呢?直接找个发垃圾邮件的工具不就解决问题了吗?是个好主意, 但是这个方案在金融行业是行不通的,为什么?因为银行发送该类邮件是有要求的:

  • 个性化服务

一般银行都要求个性化服务,发过去的邮件上总有一些个人信息吧,比如“××先 生”,“××女士”等。

  • 递送成功率

邮件的递送成功率有一定的要求,由于大批量地发送邮件会被接收方邮件服务器误认是垃圾邮件,因此在邮件头要增加一些伪造数据,以规避被反垃圾邮件引擎误认为是垃圾邮件。

从这两方面考虑广告信的发送也是电子账单系统(电子账单系统一般包括:账单分析、 账单生成器、广告信管理、发送队列管理、发送机、退信处理、报表管理等)的一个子功能,我们今天就来考虑一下广告信这个模块是怎么开发的。那既然是广告信,肯定需要一个模板,然后再从数据库中把客户的信息一个一个地取出,放到模板中生成一份完整的邮件, 然后扔给发送机进行发送处理,类图如图13-1所示。

在类图中AdvTemplate是广告信的模板,一般都是从数据库取出,生成一个BO或者是DTO,我们这里使用一个静态的值来作代表;Mail类是一封邮件类,发送机发送的就是这个类。我们先来看AdvTemplate,如代码清单13-1所示。

image-20210928190441572

图13-1 发送电子账单类图

代码清单13-1 广告信模板代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class AdvTemplate {
//广告信名称
private String advSubject ="XX银行国庆信用卡抽奖活动";
//广告信内容
private String advContext = "国庆抽奖活动通知:只要刷卡就送你一百万!...";
//取得广告信的名称
public String getAdvSubject(){
return this.advSubject;
}
//取得广告信的内容
public String getAdvContext(){
return this.advContext;
}
}

邮件类Mail如代码清单13-2所示。

代码清单13-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
38
39
40
41
42
43
44
45
46
47
public class Mail {
//收件人
private String receiver;
//邮件名称
private String subject;
//称谓
private String appellation;
//邮件内容
private String contxt;
//邮件的尾部,一般都是加上"XXX版权所有"等信息 private String tail;
//构造函数
public Mail(AdvTemplate advTemplate){
this.contxt = advTemplate.getAdvContext();
this.subject = advTemplate.getAdvSubject();
}
//以下为getter/setter方法
public String getReceiver() {
return receiver;
}
public void setReceiver(String receiver) {
this.receiver = receiver;
}
public String getSubject() {
return subject;
}
public void setSubject(String subject) {
this.subject = subject;
}
public String getAppellation() {
return appellation;
}
public void setAppellation(String appellation) {
this.appellation = appellation;
}
public String getContxt() {
return contxt;
}
public void setContxt(String contxt) {
this.contxt = contxt;
}
public String getTail() {
return tail;
}
public void setTail(String tail) {
this.tail = tail;
}
}

Mail类就是一个业务对象,虽然比较长,还是比较简单的。我们再来看业务场景类是如何对邮件继续处理的,如代码清单11-3所示。

代码清单13-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
public class Client {
//发送账单的数量,这个值是从数据库中获得
private static int MAX_COUNT = 6;
public static void main(String[] args) {
//模拟发送邮件
int i=0;
//把模板定义出来,这个是从数据库中获得
Mail mail = new Mail(new AdvTemplate());
mail.setTail("XX银行版权所有");
while(i<MAX_COUNT){
//以下是每封邮件不同的地方
mail.setAppellation(getRandString(5)+" 先生(女士)");
mail.setReceiver(getRandString(5)+"@"+getRandString(8) +".com");
//然后发送邮件
sendMail(mail);
i++;
}
}
//发送邮件
public static void sendMail(Mail mail){
System.out.println("标题:"+mail.getSubject() + "\t收件人: "+mail.getReceiver()+"\t...发送成功!");
}
//获得指定长度的随机字符串
public static String getRandString(int maxLength){
String source ="abcdefghijklmnopqrskuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
StringBuffer sb = new StringBuffer();
Random rand = new Random();
for(int i=0;i<maxLength;i++){
sb.append(source.charAt(rand.nextInt(source.length())));
}
return sb.toString();
}
}

运行结果如下所示:

1
2
3
4
5
6
标题:XX银行国庆信用卡抽奖活动 收件人:fjQUm@ZnkyPSsL.com ...发送成功! 
标题:XX银行国庆信用卡抽奖活动 收件人:ZIKnC@NOKdloNM.com ...发送成功!
标题:XX银行国庆信用卡抽奖活动 收件人:zNkMI@HpMMSZaz.com ...发送成功!
标题:XX银行国庆信用卡抽奖活动 收件人:oMTFA@uBwkRjxa.com ...发送成功!
标题:XX银行国庆信用卡抽奖活动 收件人:TquWT@TLLVNFja.com ...发送成功!
标题:XX银行国庆信用卡抽奖活动 收件人:rkQbp@mfATHDQH.com ...发送成功!

由于是随机数,每次运行都有所差异,不管怎么样,我们这个电子账单发送程序是编写出来了,也能正常发送。我们再来仔细地想想,这个程序是否有问题?Look here,这是一个线程在运行,也就是你发送的是单线程的,那按照一封邮件发出去需要0.02秒(够小了,你还要到数据库中取数据呢),600万封邮件需要33个小时,也就是一个整天都发送不完,今天的没发送完,明天的账单又产生了,日积月累,激起甲方人员一堆抱怨,那怎么办?

好办,把sendMail修改为多线程,但是只把sendMail修改为多线程还是有问题的呀,产生第一封邮件对象,放到线程1中运行,还没有发送出去;线程2也启动了,直接就把邮件对象mail的收件人地址和称谓修改掉了,线程不安全了。说到这里,你会说这有N多种解决办法,其中一种是使用一种新型模式来解决这个问题:通过对象的复制功能来解决这个问题, 类图稍做修改,如图13-2所示。

image-20210928190854417

图13-2 修改后的发送电子账单类图

增加了一个Cloneable接口(Java自带的一个接口), Mail实现了这个接口,在Mail类中覆写clone()方法,我们来看Mail类的改变,如代码清单13-4所示。

代码清单13-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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
public class Mail implements Cloneable{
//收件人
private String receiver;
//邮件名称
private String subject;
//称谓
private String appellation;
//邮件内容
private String contxt;
//邮件的尾部,一般都是加上"XXX版权所有"等信息 private String tail;
//构造函数
public Mail(AdvTemplate advTemplate){
this.contxt = advTemplate.getAdvContext();
this.subject = advTemplate.getAdvSubject();
}
@Override public Mail clone(){
Mail mail =null;
try {
mail = (Mail)super.clone();
}
catch (CloneNotSupportedException e) {
// TODO Auto-generated catch block e.printStackTrace();
}
return mail;
}
//以下为getter/setter方法 public String getReceiver() {
return receiver;
}
public void setReceiver(String receiver) {
this.receiver = receiver;
}
public String getSubject() {
return subject;
}
public void setSubject(String subject) {
this.subject = subject;
}
public String getAppellation() {
return appellation;
}
public void setAppellation(String appellation) {
this.appellation = appellation;
}
public String getContxt() {
return contxt;
}
public void setContxt(String contxt) {
this.contxt = contxt;
}
public String getTail() {
return tail;
}
public void setTail(String tail) {
this.tail = tail;
}
}

注意看粗体部分,实现了一个接口,并重写了clone方法,大家可能看着这个类有点奇怪,先保留你的好奇,我们继续讲下去,稍后会给你清晰的答案。我们再来看场景Client的变化,如代码清单13-5所示。

代码清单13-5 修改后的场景类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Client {
//发送账单的数量,这个值是从数据库中获得
private static int MAX_COUNT = 6;
public static void main(String[] args) {
//模拟发送邮件
int i=0;
//把模板定义出来,这个是从数据中获得
Mail mail = new Mail(new AdvTemplate());
mail.setTail("XX银行版权所有");
while(i<MAX_COUNT){
//以下是每封邮件不同的地方
Mail cloneMail = mail.clone();
cloneMail.setAppellation(getRandString(5)+" 先生(女士)");
cloneMail.setReceiver(getRandString(5)+"@"+getRandString(8)+".com");
//然后发送邮件
sendMail(cloneMail);
i++;
}
}
}

运行结果不变,一样完成了电子广告信的发送功能,而且sendMail即使是多线程也没有关系。注意,看Client类中的粗体字mail.clone()这个方法,把对象复制一份,产生一个新的对象,和原有对象一样,然后再修改细节的数据,如设置称谓、设置收件人地址等。这种不通过new关键字来产生一个对象,而是通过对象复制来实现的模式就叫做原型模式

13.2 原型模式的定义

原型模式(Prototype Pattern)的简单程度仅次于单例模式和迭代器模式。正是由于简单,使用的场景才非常地多,其定义如下:

Specify the kinds of objects to create using a prototypical instance,and create new objects by copying this prototype.(用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。)

原型模式的通用类图如图13-3所示。

image-20210928191309540

图13-3 原型模式的通用类图

简单,太简单了!原型模式的核心是一个clone方法,通过该方法进行对象的拷贝,Java 提供了一个Cloneable接口来标示这个对象是可拷贝的,为什么说是“标示”呢?翻开JDK的帮助看看Cloneable是一个方法都没有的,这个接口只是一个标记作用,在JVM中具有这个标记的对象才有可能被拷贝。那怎么才能从“有可能被拷贝”转换为“可以被拷贝”呢?方法是覆盖clone()方法,是的,你没有看错是重写clone()方法,看看我们上面Mail类中的clone方法,如代码清单13-6所示。

代码清单13-6 邮件类中的clone方法

1
2
@Override public Mail clone(){
}

注意,在clone()方法上增加了一个注解@Override,没有继承一个类为什么可以覆写呢?想想看,在Java中所有类的老祖宗是谁?对嘛,Object类,每个类默认都是继承了这个类,所以用覆写是非常正确的——覆写了Object类中的clone方法!

在Java中原型模式是如此简单,我们来看通用源代码,如代码清单13-7所示。

代码清单13-7 原型模式通用源码

1
2
3
4
5
6
7
8
9
10
11
12
13
public class PrototypeClass implements Cloneable{
//覆写父类Object方法
@Override public PrototypeClass clone(){
PrototypeClass prototypeClass = null;
try {
prototypeClass = (PrototypeClass)super.clone();
}
catch (CloneNotSupportedException e) {
//异常处理
}
return prototypeClass;
}
}

实现一个接口,然后重写clone方法,就完成了原型模式

14.2 中介者模式的定义

中介者模式的定义为:Define an object that encapsulates how a set of objects interact.Mediator promotes loose coupling by keeping objects from referring to each other explicitly,and it lets you vary their interaction independently.(用一个中介对象封装一系列的对象 交互,中介者使各对象不需要显示地相互作用,从而使其耦合松散,而且可以独立地改变它 们之间的交互。)

中介者模式通用类图如图14-7所示。

image-20210928205435846

图14-7 中介者模式通用类图

从类图中看,中介者模式由以下几部分组成:

  • Mediator 抽象中介者角色

抽象中介者角色定义统一的接口,用于各同事角色之间的通信。

  • Concrete Mediator 具体中介者角色

具体中介者角色通过协调各同事角色实现协作行为,因此它必须依赖于各个同事角色。

  • Colleague 同事角色

每一个同事角色都知道中介者角色,而且与其他的同事角色通信的时候,一定要通过中介者角色协作。每个同事类的行为分为两种:一种是同事本身的行为,比如改变对象本身的状态,处理自己的行为等,这种行为叫做自发行为(Self-Method),与其他的同事类或中介者没有任何的依赖;第二种是必须依赖中介者才能完成的行为,叫做依赖方法(Dep-Method)。

中介者模式比较简单,其通用源码也比较简单,先看抽象中介者Mediator类,如代码清单14-12所示。

代码清单14-12 通用抽象中介者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public abstract class Mediator {
//定义同事类
protected ConcreteColleague1 c1;
protected ConcreteColleague2 c2;
//通过getter/setter方法把同事类注入进来
public ConcreteColleague1 getC1() {
return c1;
}
public void setC1(ConcreteColleague1 c1) {
this.c1 = c1;
}
public ConcreteColleague2 getC2() {
return c2;
}
public void setC2(ConcreteColleague2 c2) {
this.c2 = c2;
}
//中介者模式的业务逻辑
public abstract void doSomething1();
public abstract void doSomething2();
}

在Mediator抽象类中我们只定义了同事类的注入,为什么使用同事实现类注入而不使用抽象类注入呢?那是因为同事类虽然有抽象,但是没有每个同事类必须要完成的业务方法, 当然如果每个同事类都有相同的方法,比如execute、handler等,那当然注入抽象类,做到依赖倒置。

具体的中介者一般只有一个,即通用中介者,其源代码如代码清单14-13所示。

代码清单14-13 通用中介者

1
2
3
4
5
6
7
8
9
10
11
public class ConcreteMediator extends Mediator {
@Override public void doSomething1() {
//调用同事类的方法,只要是public方法都可以调用
super.c1.selfMethod1();
super.c2.selfMethod2();
}
public void doSomething2() {
super.c1.selfMethod1();
super.c2.selfMethod2();
}
}

中介者所具有的方法doSomething1和doSomething2都是比较复杂的业务逻辑,为同事类服务,其实现是依赖各个同事类来完成的。

同事类的基类如代码清单14-14所示。

代码清单14-14 抽象同事类

1
2
3
4
5
6
public abstract class Colleague {
protected Mediator mediator;
public Colleague(Mediator _mediator){
this.mediator = _mediator;
}
}

这个基类也非常简单。一般来说,中介者模式中的抽象都比较简单,是为了建立这个中介而服务的,具体同事类如代码清单14-15所示。

代码清单14-15 具体同事类

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
public class ConcreteColleague1 extends Colleague {
//通过构造函数传递中介者
public ConcreteColleague1(Mediator _mediator){
super(_mediator);
}
//自有方法
self-method public void selfMethod1(){
//处理自己的业务逻辑
}
//依赖方法
dep-method public void depMethod1(){
//处理自己的业务逻辑
//自己不能处理的业务逻辑,委托给中介者处理
super.mediator.doSomething1();
}
}
public class ConcreteColleague2 extends Colleague {
//通过构造函数传递中介者
public ConcreteColleague2(Mediator _mediator){
super(_mediator);
}
//自有方法
self-method public void selfMethod2(){
//处理自己的业务逻辑
}
//依赖方法
dep-method public void depMethod2(){
//处理自己的业务逻辑
//自己不能处理的业务逻辑,委托给中介者处理
super.mediator.doSomething2();
}
}

为什么同事类要使用构造函数注入中介者,而中介者使用getter/setter方式注入同事类呢?这是因为同事类必须有中介者,而中介者却可以只有部分同事类。

15.1 项目经理也难当

我是公司的项目经理,在国内做项目,项目经理需要什么都懂,什么都管。做好了,项目经理能分到一杯羹;做不好,都是项目经理的责任。这几乎是绝对的,我带过很多项目, 行政命令一压下来,那就一条道:做完做好!

虽然我们公司是一个集团公司,但是我们部门的业绩是独立核算的,也就是说,我们部门不仅可以为集团公司服务,还可以为其他甲方服务,赚取更多的外快。2007年,我曾负责一个比较小的项目(但是项目的合同金额可不少)——为某家旅行社建立一套内部管理系统。该旅行社的门店比较多,员工也比较多,这个内部管理系统用来管理客户、旅游资源、 票务以及内部事务,整体上类似于一个小型的MIS系统。客户的需求比较明确,因为他们曾经自己购买了一套内部管理系统,这次变动基本上是翻版;而且这家旅行社也有自己的IT部门,技术人员之间语言相通,比较好相处,也没有交流鸿沟。

该项目的成员分工采用了常规的分工方式,分为需求组(Requirement Group,RG)、美工组(Page Group,PG)、代码组(我们内部还有一个比较优雅的名字:逻辑实现组,这里使用大家经常称呼的名称,即Code Group,简称CG),加上我这个项目经理正好十个人。刚开始,客户(也就是旅行社,甲方)很乐意和我们每个组探讨,比如和需求组讨论需求、和美工讨论页面、和代码组讨论实现,告诉他们修改、删除、增加各种内容等。这是一种比较常见的甲乙方合作模式,甲方深入到乙方的项目开发中,我们可以使用类图来表示这个过程,如图15-1所示。

image-20210928212436694

图15-1 旅行社项目开发过程类图

这个类图很简单,客户和三个组都有交流,这也合情合理。那我们看看这个过程的实现,首先看抽象类Group,如代码清单15-1所示。

代码清单15-1 抽象组

1
2
3
4
5
6
7
8
9
10
11
12
public abstract class Group {
//甲乙双方分开办公,如果你要和某个组讨论,你首先要找到这个组
public abstract void find();
//被要求增加功能
public abstract void add();
//被要求删除功能
public abstract void delete();
//被要求修改功能
public abstract void change();
//被要求给出所有的变更计划
public abstract void plan();
}

大家看抽象类中的每个方法,其中的每个都是一个命令语气——“找到它,增加,删除,给我计划!”这些都是命令,给出命令然后由相关的人员去执行。我们再看3个实现类, 其中的需求组最重要,需求组RequirmentGroup类如代码清单15-2所示。

代码清单15-2 需求组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class RequirementGroup extends Group {
//客户要求需求组过去和他们谈
public void find() {
System.out.println("找到需求组...");
}
//客户要求增加一项需求
public void add() {
System.out.println("客户要求增加一项需求...");
}
//客户要求修改一项需求
public void change() {
System.out.println("客户要求修改一项需求...");
}
//客户要求删除一项需求
public void delete() {
System.out.println("客户要求删除一项需求...");
}
//客户要求给出变更计划
public void plan() {
System.out.println("客户要求需求变更计划...");
}
}

需求组有了,我们再看美工组。美工组也很重要,是项目的脸面,客户最终接触到的还是界面。美工组PageGroup类如代码清单15-3所示。

代码清单15-3 美工组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class PageGroup extends Group {
//首先这个美工组应该能找到吧,要不你跟谁谈?
public void find() {
System.out.println("找到美工组...");
}
//美工被要求增加一个页面
public void add() {
System.out.println("客户要求增加一个页面...");
}
//客户要求对现有界面做修改
public void change() {
System.out.println("客户要求修改一个页面...");

}
//甲方是老大,要求删除一些页面
public void delete() {
System.out.println("客户要求删除一个页面...");
}
//所有的增、删、改都要给出计划
public void plan() {
System.out.println("客户要求页面变更计划...");
}
}

最后看代码组。这个组的成员一般比较沉闷,不多说话,但多做事儿,这是这个组的典型特点。代码组CodeGroup类如代码清单15-4所示。

代码清单15-4 代码组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class CodeGroup extends Group {
//客户要求代码组过去和他们谈
public void find() {
System.out.println("找到代码组...");
}
//客户要求增加一项功能
public void add() {
System.out.println("客户要求增加一项功能...");
}
//客户要求修改一项功能
public void change() {
System.out.println("客户要求修改一项功能...");
}
//客户要求删除一项功能
public void delete() {
System.out.println("客户要求删除一项功能...");
}
//客户要求给出变更计划
public void plan() {
System.out.println("客户要求代码变更计划...");
}
}

整个项目的3个支柱都已经产生了,那看客户怎么和我们谈。客户刚开始提交了他们自己写的一份比较完整的需求,需求组根据这份需求写了一份分析说明书,客户看后,要求增加需求,该场景如代码清单15-5所示。

代码清单15-5 场景类

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Client {
public static void main(String[] args) {
//首先客户找到需求组说,过来谈需求,并修改
System.out.println("-----------客户要求增加一项需求---------------");
Group rg = new RequirementGroup();
//找到需求组
rg.find();
//增加一个需求
rg.add();
//要求变更计划
rg.plan();
}
}

运行的结果如下所示:

1
2
3
4
-------------客户要求增加一项需求----------------- 
找到需求组...
客户要求增加一项需求...
客户要求需求变更计划...

客户的需求暂时满足了,过了一段时间,客户又要求“界面多画了一个,过来谈谈”,于是又有一次场景变化,如代码清单15-6所示。

代码清单15-6 变化的场景类

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Client {
public static void main(String[] args) {
//首先客户找到美工组说,过来谈页面,并修改
System.out.println("----------客户要求删除一个页面--------------");
Group pg = new PageGroup();
//找到需求组
pg.find();
//删除一项需求
pg.delete();
//要求变更计划
pg.plan();
}
}

运行结果如下所示:

1
2
3
4
-------------客户要求删除一个页面----------------- 
找到美工组...
客户要求删除一个页面...
客户要求页面变更计划...

好了,界面也谈过了,应该没什么大问题了吧。过了一天后,客户又让代码组过去,说是数据库设计问题,然后又叫美工组过去,布置了一堆命令……这个就不一一写了,大家应该能够体会得到!问题来了,我们修改可以,但是每次都是叫一个组去,布置个任务,然后出计划,每次都这样,如果让你当甲方,你烦不烦?而且这种方式很容易出错误,并且还真发生过。客户把美工叫过去了,要删除,可美工说需求是这么写的,然后客户又命令需求组过去,一次次地折腾之后,客户也烦躁了,于是直接抓住我这个项目经理说:“我不管你们内部怎么安排,你就给我找个接头负责人,我告诉他怎么做,删除页面,增加功能,你们内部怎么处理我不管,我就告诉他我要干什么就成了……”

我一听,好啊,这也正是我想要的,我们项目组的兄弟们也已经受不了了,于是我改变了一下我的处理方式,如图15-2所示。

image-20210928213345960

图15-2 增加负责人后的类图

在原有的类图上增加了一个Invoker类,其作用是根据客户的命令安排不同的组员进行工作,例如,客户说“界面上删除一条记录”,Invoker类接收到该String类型命令后,通知美工组PageGroup开始delete,然后再找到代码组CodeGroup后台不要存到数据库中,最后反馈给客户一个执行计划。这是一个挺好的方案,但是客户的命令是一个String类型的,这有非常多的变化,仅仅通过一个字符串来传递命令并不是一个非常好的方案,因为在系统设计中, 字符串没有约束力,根据字符串判断相关的业务逻辑不是一个优秀的解决方案。那怎么才是一个优秀的方案呢?解决方案是:对客户发出的命令进行封装,每个命令是一个对象,避免客户、负责人、组员之间的交流误差,封装后的结果就是客户只要说一个命令,我的项目组就立刻开始启动,不用思考、解析命令字符串,如图15-3所示。

image-20210928213515332

图15-3 完美的类图

Command抽象类只有一个方法execute,其作用就是执行命令,子类非常坚决地实现该命令,与军队中类似,上级军官给士兵发布命令:爬上这个旗杆!然后士兵回答:Yes,Sir!完美的项目也与此类似,客户发送一个删除页面的命令,接头负责人Invoker接收到命令后,立刻执行DeletePageCommand的execute方法。对类图中增加的几个类说明如下。

  • Command抽象类:客户发给我们的命令,定义三个工作组的成员变量,供子类使用; 定义一个抽象方法execute,由子类来实现。
  • CInvoker实现类:项目接头负责人,setComand接收客户发给我们的命令,action方法是执行客户的命令(方法名写成是action,与command的execute区分开,避免混淆)。

其中,Command抽象类是整个扩展的核心,其源代码如代码清单15-7所示。

代码清单15-7 抽象命令类

1
2
3
4
5
6
7
8
9
10
11
public abstract class Command {
//把三个组都定义好,子类可以直接使用
protected RequirementGroup rg = new RequirementGroup();
//需求组
protected PageGroup pg = new PageGroup();
//美工组
protected CodeGroup cg = new CodeGroup();
//代码组
//只有一个方法,你要我做什么事情
public abstract void execute();
}

抽象类很简单,具体的实现类只要实现execute方法就可以了。在一个项目中,需求增加是很常见的,那就把“增加需求”定义为一个命令AddRequirementCommand类,如代码清单15- 8所示。

代码清单15-8 增加需求的命令

1
2
3
4
5
6
7
8
9
10
11
public class AddRequirementCommand extends Command {
//执行增加一项需求的命令
public void execute() {
//找到需求组
super.rg.find();
//增加一份需求
super.rg.add();
//给出计划
super.rg.plan();
}
}

页面变更也是比较频繁发生的,定义一个删除页面的命令DeletePageCommand类,如代码清单15-9所示。

代码清单15-9 删除页面的命令

1
2
3
4
5
6
7
8
9
10
11
public class DeletePageCommand extends Command {
//执行删除一个页面的命令
public void execute() {
//找到页面组
super.pg.find();
//删除一个页面
super.rg.delete();
//给出计划
super.rg.plan();
}
}

Command抽象类可以有N个子类,如增加一个功能命令(AddFunCommand),删除一份需求命令(DeleteRequirementCommand)等,这里就不再描述了,只要是由客户产生、时常性的行为都可以定义为一个命令,其实现类都比较简单,读者可以自行扩展。

客户发送的命令已经确定下来,我们再看负责人Invoker,如代码清单15-10所示。

代码清单15-10 负责人

1
2
3
4
5
6
7
8
9
10
11
12
public class Invoker {
//什么命令
private Command command;
//客户发出命令
public void setCommand(Command command){
this.command = command;
}
//执行客户的命令
public void action(){
this.command.execute();
}
}

这更简单了,负责人只要接到客户的命令,就立刻执行。我们模拟增加一项需求的过程,如代码清单15-11所示。

代码清单15-11 增加一项需求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Client {
public static void main(String[] args) {
//定义我们的接头人
Invoker xiaoSan = new Invoker();
//接头人就是小三
//客户要求增加一项需求
System.out.println("------------客户要求增加一项需求---------------");
//客户给我们下命令来
Command command = new AddRequirementCommand();
//接头人接收到命令
xiaoSan.setCommand(command);
//接头人执行命令
xiaoSan.action();
}
}

运行结果如下所示:

1
2
3
4
-------------客户要求增加一项需求----------------- 
找到需求组...
客户要求增加一项需求...
客户要求需求变更计划...

是不是我们的场景类简单了很多?客户只要给命令,我马上执行。简单!非常简单!那我们看看,如果客户要求删除一个页面,我们的修改有多大,如代码清单15-12所示。

代码清单15-12 删除一个页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Client {
public static void main(String[] args) {
//定义我们的接头人
Invoker xiaoSan = new Invoker();
//接头人就是小三
//客户要求增加一项需求
System.out.println("------------客户要求删除一个页面---------------");
//客户给我们下命令来
//Command command = new AddRequirementCommand();
Command command = new DeletePageCommand();
//接头人接收到命令
xiaoSan.setCommand(command);
//接头人执行命令
xiaoSan.action();
}
}

运行结果如下所示:

1
2
3
4
-------------客户要求删除一个页面----------------- 
找到美工组...
客户要求删除一项需求...
客户要求需求变更计划...

看到上面用粗体显示的代码了吗?只修改了这么多,是不是很简单,而且客户也不用知道到底由谁来修改,高内聚的要求体现出来了,这就是命令模式。

16.2 责任链模式的定义

责任链模式定义如下:

Avoid coupling the sender of a request to its receiver by giving more than one object a chance to handle the request.Chain the receiving objects and pass the request along the chain until an object handles it.(使多个对象都有机会处理请求,从而避免了请求的发送者和接受者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有对象处理它为止。)

责任链模式的重点是在“链”上,由一条链去处理相似的请求在链中决定谁来处理这个请求,并返回相应的结果,其通用类图如图16-4所示。

image-20210929113427126

图16-4 责任链模式通用类图

责任链模式的核心在“链”上,“链”是由多个处理者ConcreteHandler组成的,我们先来看抽象Handler类,如代码清单16-14所示。

代码清单16-14 抽象处理者

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
public abstract class Handler {
private Handler nextHandler;
//每个处理者都必须对请求做出处理
public final Response handleMessage(Request request){
Response response = null;
//判断是否是自己的处理级别
if(this.getHandlerLevel().equals(request.getRequestLevel())){
response = this.echo(request);
}
else{
//不属于自己的处理级别
//判断是否有下一个处理者
if(this.nextHandler != null){
response = this.nextHandler.handleMessage(request);
}
else{
//没有适当的处理者,业务自行处理
}
}
return response;
}
//设置下一个处理者是谁
public void setNext(Handler _handler){
this.nextHandler = _handler;
}
//每个处理者都有一个处理级别
protected abstract Level getHandlerLevel();
//每个处理者都必须实现处理任务
protected abstract Response echo(Request request);
}

抽象的处理者实现三个职责:一是定义一个请求的处理方法handleMessage,唯一对外开放的方法;二是定义一个链的编排方法setNext,设置下一个处理者;三是定义了具体的请求者必须实现的两个方法:定义自己能够处理的级别getHandlerLevel和具体的处理任务echo。


注意 在责任链模式中一个请求发送到链中后,前一节点消费部分消息,然后交由后续节点继续处理,最终可以有处理结果也可以没有处理结果,读者可以不用理会什么纯的、不纯的责任链模式。同时,请读者注意handlerMessage方法前的final关键字,可以阅读第10章的模板方法模式。


我们定义三个具体的处理者,以便可以组成一个链,如代码清单16-15所示。

代码清单16-15 具体处理者

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 ConcreteHandler1 extends Handler {
//定义自己的处理逻辑
protected Response echo(Request request) {
//完成处理逻辑
return null;

}
//设置自己的处理级别
protected Level getHandlerLevel() {
//设置自己的处理级别
return null;
}
}
public class ConcreteHandler2 extends Handler {
//定义自己的处理逻辑
protected Response echo(Request request) {
//完成处理逻辑
return null;
}
//设置自己的处理级别
protected Level getHandlerLevel() {
//设置自己的处理级别
return null;
}
}
public class ConcreteHandler3 extends Handler {
//定义自己的处理逻辑
protected Response echo(Request request) {
//完成处理逻辑
return null;
}
//设置自己的处理级别
protected Level getHandlerLevel() {
//设置自己的处理级别
return null;
}
}

在处理者中涉及三个类:Level类负责定义请求和处理级别,Request类负责封装请求,Response负责封装链中返回的结果,该三个类都需要根据业务产生,读者可以在实际应用中完成相关的业务填充,其框架代码如代码清单16-16所示。

代码清单16-16 模式中有关框架代码

1
2
3
4
5
6
7
8
9
10
11
12
public class Level {
//定义一个请求和处理等级
}
public class Request {
//请求的等级
public Level getRequestLevel(){
return null;
}
}
public class Response {
//处理者返回的数据
}

在场景类或高层模块中对链进行组装,并传递请求,返回结果,如代码清单16-17所示。

代码清单16-17 场景类

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Client {
public static void main(String[] args) {
//声明所有的处理节点
Handler handler1 = new ConcreteHandler1();
Handler handler2 = new ConcreteHandler2();
Handler handler3 = new ConcreteHandler3();
//设置链中的阶段顺序1-->2-->3
handler1.setNext(handler2);
handler2.setNext(handler3);
//提交请求,返回结果
Response response = handler1.handlerMessage(new Request());
}
}

在实际应用中,一般会有一个封装类对责任模式进行封装,也就是替代Client类,直接返回链中的第一个处理者,具体链的设置不需要高层次模块关系,这样,更简化了高层次模块的调用,减少模块间的耦合,提高系统的灵活性。

17.1 罪恶的成绩单

“中庸”是中国儒教文化的集中体现,说话或做事情都不能太直接,需要有技巧。比如谈话,如果你要批评某个人,你不能一上来就说他这做得不对,那也做得不对,你要先肯定他的成绩,表扬一下优点;然后再指出不足,指出错误的地方,最后再来点激励,你修改了这些缺点后有哪些好处,比如你能带更多的小兵、升职等。如果你一上来就是一顿批评,你瞅瞅看,对方肯定是不服气,甚至是顶撞你说:“此处不养爷,自有养爷处”,于是甩门而去。

这是说话,做事情也是一样。在山寨产品流行之前,假货也是比较“盛行”的。本人2002 年买了一部手机,当时老板吹得天花乱坠,承诺这部手机是最新的,我看着也像,壳子是崭新的,包装是崭新的,没有任何瑕疵,就是比正品便宜了很多,于是我买了,因为缺钱啊! 用了3个月,坏了,送修检查,结果诊断出这是新壳装旧机,我晕!拿一部旧手机的线路板,找个新的外壳、屏幕、包装就成了新手机,害人不浅啊!

我们不说不开心的事情,今天以什么例子为开场白呢?就说说我上小学的糗事吧。我上小学的时候学习成绩非常差,班级上有40多个同学,我基本上都是排在45名以后,按照老师给我的评价就是:“不是读书的料”。但是我父亲管得很严格,明知道我不是这块料,还是“赶鸭子上架”,每次考完试我都战战兢兢,“竹笋炒肉”是肯定少不了的,但是能少点就少点吧,因为肉可是自己的。四年级期末考试考完,学校出来个很损的招儿(这招儿现在很流行的),打印出成绩单,要家长签字,然后才能上五年级,我那个恐惧呀,不过也就是几秒钟的时间,玩起来什么都忘记了。我们做架构,做设计,任何值得我们回忆的事件都可以通过设计记录下来。当然了,这份成绩单的事情也是可以通过类图表示的,如图17-1所示。

image-20210929114648784

图17-1 成绩单类图

成绩单的抽象类,然后有一个四年级的成绩单实现类,So Easy,我们先来看抽象类, 如代码清单17-1所示。

代码清单17-1 抽象成绩单

1
2
3
4
5
6
public abstract class SchoolReport {
//成绩单主要展示的就是你的成绩情况
public abstract void report();
//成绩单要家长签字,这个是最要命的
public abstract void sign();
}

有抽象类了,我们再来看看具体的四年级成绩单FouthGradeSchoolReport,如代码清单17-2所示。

代码清单17-2 四年级成绩单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class FouthGradeSchoolReport extends SchoolReport {
//我的成绩单
public void report() {
//成绩单的格式是这个样子的
System.out.println("尊敬的XXX家长:");
System.out.println(" ......");

System.out.println(" 语文 62 数学65 体育 98 自然 63");
System.out.println(" .......");
System.out.println(" 家长签名: ");
}
//家长签名
public void sign(String name) {
System.out.println("家长签名为:"+name);
}
}

成绩单出来,你别看什么62、65之类的成绩,你要知道,在小学低于90分基本上就是中下等了,悲哀呀,爱学习的人咋就那么多!怎么着,那我把这个成绩单给老爸看看?好,我们修改一下类图,成绩单给老爸看,如图17-2所示。

image-20210929114828659

图17-2 老爸查看成绩单类图

老爸开始看成绩单,这个成绩单可是最真实的,啥都没有动过,原装,Father类如代码清单17-3所示。

代码清单17-3 老爸查看成绩单

1
2
3
4
5
6
7
8
9
public class Father {
public static void main(String[] args) {
//把成绩单拿过来
SchoolReport sr = new FouthGradeSchoolReport();
//看成绩单
sr.report();
//签名?休想!
}
}

运行结果如下:

1
2
3
4
尊敬的XXX家长: ...... 
语文 62 数学65 体育 98 自然 63
.......
家长签名:

就这成绩还要我签字?!老爸就开始找扫帚,我开始做准备:深呼吸,绷紧肌肉,提臀收腹。哈哈,幸运的是,这个不是当时的真实情况,我没有直接把成绩单交给老爸,而是在交给他之前做了点技术工作,我要把成绩单封装一下,封装分类两步来实现,如下所示。

  • 汇报最高成绩

跟老爸说各个科目的最高分,语文最高是75,数学是78,自然是80,然后老爸觉得我的成绩与最高分数相差不多,考的还是不错的嘛!这个是实情,但是不知道是什么原因,反正期末考试都考得不怎么样,但是基本上都集中在70分以上,我这60多分基本上还是垫底的角色。

  • 汇报排名情况

在老爸看完成绩单后,告诉他我在全班排第38名,这个也是实情,为啥呢?有将近十个同学退学了!这个情况我是不会说的。不知道是不是当时第一次发成绩单时学校没有考虑清楚,没有写上总共有多少同学,排第几名,反正是被我钻了个空子。

那修饰是说完了,我们看看类图如何修改,如图17-3所示。

image-20210929115100994

图17-3 修饰成绩单
我想这是大家最容易想到的类图,通过直接增加了一个子类,重写report方法,很容易地解决了这个问题,是不是这样?是的,这确实是一个比较好的办法,我们来看具体的实现,如代码清单17-4所示。

代码清单17-4 修饰成绩单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class SugarFouthGradeSchoolReport extends FouthGradeSchoolReport {
//首先要定义你要美化的方法,先给老爸说学校最高成绩
private void reportHighScore(){
System.out.println("这次考试语文最高是75,数学是78,自然是80");
}
//在老爸看完毕成绩单后,我再汇报学校的排名情况
private void reportSort(){
System.out.println("我是排名第38名...");
}
//由于汇报的内容已经发生变更,那所以要重写父类
@Override public void report(){
this.reportHighScore();
//先说最高成绩
super.report();
//然后老爸看成绩单
this.reportSort();
//然后告诉老爸学习学校排名
}

}

然后对Father类稍做修改就可以看到美化后的成绩单,如代码清单17-5所示。

代码清单17-5 老爸查看修饰后的成绩单

1
2
3
4
5
6
7
8
9
10
11
public class Father {
public static void main(String[] args) {
//把美化过的成绩单拿过来
SchoolReport sr= new SugarFouthGradeSchoolReport();
//看成绩单
sr.report();
//然后老爸,一看,很开心,就签名了
sr.sign("老三");
//我叫小三,老爸当然叫老三
}
}

运行结果如下所示:

1
2
3
4
5
6
7
这次考试语文最高是75,数学是78,自然是80 
尊敬的XXX家长: ......
语文 62 数学65 体育 98 自然 63
.......
家长签名:
我是排名第38名...
家长签名为:老三

通过继承确实能够解决这个问题,老爸看成绩单很开心,然后就给签字了,但现实的情况是很复杂的,可能老爸听我汇报最高成绩后,就直接乐开花了,直接签名了,后面的排名就没必要看了,或者老爸要先看排名情况,那怎么办?继续扩展?你能扩展多少个类?这还是一个比较简单的场景,一旦需要装饰的条件非常多,比如20个,你还通过继承来解决,你想象的子类有多少个?你是不是马上就要崩溃了!

好,你也看到通过继承情况确实出现了问题,类爆炸,类的数量激增,光写这些类不累死你才怪,而且还要想想以后维护怎么办,谁愿意接收这么一大摊本质相似的代码维护工作?并且在面向对象的设计中,如果超过两层继承,你就应该想想是不是出设计问题了,是不是应该重新找一条康庄大道了,这是经验值,不是什么绝对的,继承层次越多以后的维护成本越多,问题这么多,那怎么办?好办,我们定义一批专门负责装饰的类,然后根据实际情况来决定是否需要进行装饰,类图稍做修正,如图17-4所示。

image-20210929115341196

图17-4 增加专门的装饰类图

增加一个抽象类和两个实现类,其中Decorator的作用是封装SchoolReport类,如果大家还记得代理模式,那么很容易看懂这个类图,装饰类的作用也就是一个特殊的代理类,真实的执行者还是被代理的角色FouthGradeSchoolReport,如代码清单17-6所示。

代码清单17-6 修饰的抽象类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public abstract class Decorator extends SchoolReport{
//首先我要知道是哪个成绩单
private SchoolReport sr;
//构造函数,传递成绩单过来
public Decorator(SchoolReport sr){
this.sr = sr;
}
//成绩单还是要被看到的
public void report(){
this.sr.report();
}
//看完还是要签名的
public void sign(String name){
this.sr.sign(name);
}
}

看到没,装饰类还是把动作的执行委托给需要装饰的对象,Decorator抽象类的目的很简单,就是要让子类来封装SchoolReport的子类,怎么封装?重写report方法!先看HighScoreDecorator实现类,如代码清单17-7所示。

代码清单17-7 最高成绩修饰

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class HighScoreDecorator extends Decorator {
//构造函数
public HighScoreDecorator(SchoolReport sr){
super(sr);
}
//我要汇报最高成绩
private void reportHighScore(){
System.out.println("这次考试语文最高是75,数学是78,自然是80");
}
//我要在老爸看成绩单前告诉他最高成绩,否则等他一看,就抡起扫帚揍我,我哪里还有机会说啊
@Override public void report(){
this.reportHighScore();
super.report();
}
}

重写了report方法,先调用具体装饰类的装饰方法reportHighScore,然后再调用具体构件的方法,我们再来看怎么汇报学校排序情况SortDecorator代码,如代码清单17-8所示。

代码清单17-8 排名情况修饰

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class SortDecorator extends Decorator {
//构造函数
public SortDecorator(SchoolReport sr){

super(sr);
}
//告诉老爸学校的排名情况
private void reportSort(){
System.out.println("我是排名第38名...");
}
//老爸看完成绩单后再告诉他,加强作用
@Override public void report(){
super.report();
this.reportSort();
}
}

我准备好了这两个强力的修饰工具,然后就“毫不畏惧”地把成绩单交给老爸,看看老爸怎么看成绩单的,如代码清单17-9所示。

代码清单17-9 老爸查看修饰后的成绩单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Father {
public static void main(String[] args) {
//把成绩单拿过来
SchoolReport sr;
//原装的成绩单
sr = new FouthGradeSchoolReport();
//加了最高分说明的成绩单
sr = new HighScoreDecorator(sr);
//又加了成绩排名的说明
sr = new SortDecorator(sr);
//看成绩单
sr.report();
//然后老爸一看,很开心,就签名了
sr.sign("老三");
//我叫小三,老爸当然叫老三
}
}

老爸一看成绩单,听我这么一说,非常开心,儿子有进步呀,从40多名进步到30多名, 进步很大,躲过了一顿海扁。想想看,如果我还要增加其他的修饰条件,是不是就非常容易了,只要实现Decorator类就可以了!这就是装饰模式。

17.2 装饰模式的定义

装饰模式(Decorator Pattern)是一种比较常见的模式,其定义如下:

Attach additional responsibilities to an object dynamically keeping the same interface.Decorators provide a flexible alternative to subclassing for extending functionality.(动态地给一个对象添加一些额外的职责。 就增加功能来说,装饰模式相比生成子类更为灵活。)

装饰模式的通用类图如图17-5所示。

image-20210929115653611

图17-5 装饰模式的通用类图
在类图中,有四个角色需要说明:
  • Component抽象构件

Component是一个接口或者是抽象类,就是定义我们最核心的对象,也就是最原始的对象,如上面的成绩单。


注意 在装饰模式中,必然有一个最基本、最核心、最原始的接口或抽象类充当 Component抽象构件。


  • ConcreteComponent 具体构件

ConcreteComponent是最核心、最原始、最基本的接口或抽象类的实现,你要装饰的就是它。

  • Decorator装饰角色

一般是一个抽象类,做什么用呢?实现接口或者抽象方法,它里面可不一定有抽象的方法呀,在它的属性里必然有一个private变量指向Component抽象构件。

  • 具体装饰角色

ConcreteDecoratorA和ConcreteDecoratorB是两个具体的装饰类,你要把你最核心的、最原始的、最基本的东西装饰成其他东西,上面的例子就是把一个比较平庸的成绩单装饰成家长认可的成绩单。

装饰模式的所有角色都已经解释完毕,我们来看看如何实现,先看抽象构件,如代码清单17-10所示。

代码清单17-10 抽象构件

1
2
3
4
public abstract class Component {
//抽象的方法
public abstract void operate();
}

具体构件如代码清单17-11所示。

代码清单17-11 具体构件

1
2
3
4
5
6
public class ConcreteComponent extends Component {
//具体实现
@Override public void operate() {
System.out.println("do Something");
}
}

装饰角色通常是一个抽象类,如代码清单17-12所示。

代码清单17-12 抽象装饰者

1
2
3
4
5
6
7
8
9
10
11
public abstract class Decorator extends Component {
private Component component = null;
//通过构造函数传递被修饰者
public Decorator(Component _component){
this.component = _component;
}
//委托给被修饰者执行
@Override public void operate() {
this.component.operate();
}
}

当然了,若只有一个装饰类,则可以没有抽象装饰角色,直接实现具体的装饰角色即可。具体的装饰类如代码清单17-13所示。

代码清单17-13 具体的装饰类

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
public class ConcreteDecorator1 extends Decorator {
//定义被修饰者
public ConcreteDecorator1(Component _component){
super(_component);
}
//定义自己的修饰方法
private void method1(){
System.out.println("method1 修饰");
}

//重写父类的Operation方法
public void operate(){
this.method1();
super.operate();
}
}
public class ConcreteDecorator2 extends Decorator {
//定义被修饰者
public ConcreteDecorator2(Component _component){
super(_component);
}
//定义自己的修饰方法
private void method2(){
System.out.println("method2修饰");
}
//重写父类的Operation方法
public void operate(){
super.operate();
this.method2();
}
}

注意 原始方法和装饰方法的执行顺序在具体的装饰类是固定的,可以通过方法重载实现多种执行顺序。


我们通过Client类来模拟高层模块的耦合关系,看看装饰模式是如何运行的,如代码清单17-14所示。

代码清单17-14 场景类

1
2
3
4
5
6
7
8
9
10
11
public class Client {
public static void main(String[] args) {
Component component = new ConcreteComponent();
//第一次修饰
component = new ConcreteDecorator1(component);
//第二次修饰
component = new ConcreteDecorator2(component);
//修饰后运行
component.operate();
}
}

18.1 刘备江东娶妻,赵云他容易吗

在三国演义中,我最佩服诸葛亮的地方不是因为他未出茅庐而有三分天下的预测,也不是他在赤壁鏖战中借东风的法术,更不是他七擒七纵孟获的策略。那是什么呢?是他“气死周瑜,骂死王朗”的气度和风范!想想看,你用“气”能把一个轮胎打爆,用“气”枪能够把路灯打碎,但是要把跟你没有任何血缘关系的人气死有多困难呀,更何况是周瑜这种智慧型人物!

在诸葛亮气周瑜的过程中,有一件事情:那就是周瑜赔了夫人又折兵这件事情。事情经过是这样的:孙权看刘备有雄起之意,杀是不能杀了,那会惹天下人唾弃,就想个招儿挫他一下,那有什么办法呢?孙权有个妹妹——孙尚香,准备招刘备做女婿,然后孙权想办法把刘备软禁起来,孙权的想法还是很单纯的嘛,就是不让你刘备回西川,然后我东吴想干啥就干啥,夺荆州,吞西川也不是不可能的。东吴的想法是好的,无奈中间多了智谋无敌的诸葛亮,他早就预测了东吴有此招数,于是在刘备去东吴招亲之前,特授以伴郎赵云三个锦囊, 说是按天机拆开解决棘手问题。

这三个妙计分别是:找乔国老帮忙(也就是走后门了),求吴国太放行(诉苦)以及孙夫人断后,对这三个妙计不熟悉的读者可以去温习一下《三国演义》,这里就不多说了。想想看,这三个计谋有什么相似之处,他们都是告诉赵云要怎么执行,也就是说这三个计谋都有一个方法是执行,具体执行什么内容,每个计谋当然不同了,分析到这里,我们是不是就有这样一个设计思路:三个妙计应该实现的是同一个接口?聪明!是的,我们来看类图,如图18-1所示。

image-20210929153056467

图18-1 三个策略类图

这是非常简单的类图,在这个场景中的三个主要角色都已经有了,每个妙计都提供了一个可执行的方法,我们先来看接口,如代码清单18-1所示。
代码清单18-1 妙计接口

1
2
3
4
public interface IStrategy {
//每个锦囊妙计都是一个可执行的算法
public void operate();
}

接口很简单,定义了一个方法operate,每个妙计都是可执行的,否则那叫什么妙计,我们先看第一个妙计——找乔国老开后门,如代码清单18-2所示。

代码清单18-2 乔国老开后门

1
2
3
4
5
public class BackDoor implements IStrategy {
public void operate() {
System.out.println("找乔国老帮忙,让吴国太给孙权施加压力");
}
}

第二个妙计是找吴国太哭诉,企图给自己开绿灯,如代码清单18-3所示。

代码清单18-3 吴国太开绿灯

1
2
3
4
5
public class GivenGreenLight implements IStrategy {
public void operate() {
System.out.println("求吴国太开绿灯,放行!");
}
}

第三个妙计是在逃跑的时候,让新娘子孙夫人断后,谁来砍谁,这是非常好的主意,如代码清单18-4所示。

代码清单18-4 孙夫人断后

1
2
3
4
5
public class BlockEnemy implements IStrategy {
public void operate() {
System.out.println("孙夫人断后,挡住追兵");
}
}

在这个场景中,三个妙计都有了,那还缺少两个配角:第一,妙计肯定要放到一个地方吧,这么重要的东西要保管呀,也就是承装妙计的锦囊,所以俗称锦囊妙计嘛;第二,这些妙计都要有一个执行人吧,是谁?当然是赵云了,妙计是小亮给的,执行者是赵云。赵云就是一个干活的人,从锦囊中取出妙计,执行,然后获胜。过程非常清晰,我们把完整的过程设计出来,如图18-2所示。

在类图中增加了一个Context封装类(也就是锦囊),其作用是承装三个策略,方便赵云使用,我们来看Context代码,如代码清单18-5所示。

image-20210929153357624

图18-2 完整类图

代码清单18-5 锦囊

1
2
3
4
5
6
7
8
9
10
11
public class Context {
//构造函数,你要使用哪个妙计
private IStrategy straegy;
public Context(IStrategy strategy){
this.straegy = strategy;
}
//使用计谋了,看我出招了
public void operate(){
this.straegy.operate();
}
}

通过构造函数把策略传递进来,然后用operate()方法来执行相关的策略方法。三个妙计有了,锦囊也有了,然后就是赵云雄赳赳地揣着三个锦囊,拉着已步入老年行列的、还想着娶纯情少女的刘老爷子去入赘了。嗨,还别说,小亮同志的三个妙计还真是不错,如代码清单18-6所示。

代码清单18-6 使用计谋

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
public class ZhaoYun {
//赵云出场了,他根据诸葛亮给他的交代,依次拆开妙计
public static void main(String[] args) {
Context context;
//刚刚到吴国的时候拆第一个
System.out.println("---刚刚到吴国的时候拆第一个---");
context = new Context(new BackDoor());
//拿到妙计
context.operate();
//拆开执行
System.out.println("\n\n\n\n\n\n\n\n");
//刘备乐不思蜀了,拆第二个了
System.out.println("---刘备乐不思蜀了,拆第二个了---");
context = new Context(new GivenGreenLight());
context.operate();
//执行了第二个锦囊
System.out.println("\n\n\n\n\n\n\n\n");
//孙权的小兵追来了,咋办?拆第三个
System.out.println("---孙权的小兵追来了,咋办?拆第三个---");
context = new Context(new BlockEnemy());
context.operate();
//孙夫人退兵
System.out.println("\n\n\n\n\n\n\n\n");
}
}

我们来看看这段故事,运行结果如下:

1
2
3
4
5
6
---刚刚到吴国的时候拆第一个--- 
找乔国老帮忙,让吴国太给孙权施加压力
---刘备乐不思蜀了,拆第二个---
求吴国太开个绿灯,放行!
---孙权的小兵追来了,咋办?拆第三个---
孙夫人断后,挡住追兵

恩,不错,就这三招,搞得孙权是“赔了夫人又折兵”。那我们描述这个故事的过程就是策略模式。