18.2 策略模式的定义

策略模式(Strategy Pattern)是一种比较简单的模式,也叫做政策模式(Policy Pattern)。其定义如下:

Define a family of algorithms,encapsulate each one,and make them interchangeable.(定义一组算法,将每个算法都封装起来,并且使它们之间可以互换。)

这个定义是非常明确、清晰的,“定义一组算法”,看看我们的三个计谋是不是三个算法?“将每个算法都封装起来”,封装类Context不就是这个作用吗?“使它们可以互换”当然可以互换了,都实现是相同的接口,那当然可以相互转化了。我们看看策略模式的通用类图, 如图18-3所示。

image-20210929153712228

图18-3 策略模式通用类图

策略模式使用的就是面向对象的继承和多态机制,非常容易理解和掌握,我们再来看看策略模式的三个角色:

  • Context封装角色

它也叫做上下文角色,起承上启下封装作用,屏蔽高层模块对策略、算法的直接访问, 封装可能存在的变化。

  • Strategy抽象策略角色

策略、算法家族的抽象,通常为接口,定义每个策略或算法必须具有的方法和属性。各位看官可能要问了,类图中的AlgorithmInterface是什么意思,嘿嘿,algorithm是“运算法则”的意思,结合起来意思就明白了吧。

  • ConcreteStrategy具体策略角色

实现抽象策略中的操作,该类含有具体的算法。

我们再来看策略模式的通用源码,非常简单。先看抽象策略角色,它是一个非常普通的接口,在我们的项目中就是一个普通得不能再普通的接口了,定义一个或多个具体的算法, 如代码清单18-7所示。

代码清单18-7 抽象的策略角色

1
2
3
4
public interface Strategy {
//策略模式的运算法则
public void doSomething();
}

具体策略也是非常普通的一个实现类,只要实现接口中的方法就可以,如代码清单18-8 所示。

代码清单18-8 具体策略角色

1
2
3
4
5
6
7
8
9
10
11
public class ConcreteStrategy1 implements Strategy {
public void doSomething() {
System.out.println("具体策略1的运算法则");
}
}
public class ConcreteStrategy2 implements Strategy {

public void doSomething() {
System.out.println("具体策略2的运算法则");
}
}

策略模式的重点就是封装角色,它是借用了代理模式的思路,大家可以想想,它和代理模式有什么差别,差别就是策略模式的封装角色和被封装的策略类不用是同一个接口,如果是同一个接口那就成为了代理模式。我们来看封装角色,如代码清单18-9所示。

代码清单18-9 封装角色

1
2
3
4
5
6
7
8
9
10
11
12
public class Context {
//抽象策略
private Strategy strategy = null;
//构造函数设置具体策略
public Context(Strategy _strategy){
this.strategy = _strategy;
}
//封装后的策略方法
public void doAnythinig(){
this.strategy.doSomething();
}
}

高层模块的调用非常简单,知道要用哪个策略,产生出它的对象,然后放到封装角色中就完成任务了,如代码清单18-10所示。

代码清单18-10 高层模块

1
2
3
4
5
6
7
8
9
10
public class Client {
public static void main(String[] args) {
//声明一个具体的策略
Strategy strategy = new ConcreteStrategy1();
//声明上下文对象
Context context = new Context(strategy);
//执行封装后的方法
context.doAnythinig();
}
}

策略模式就是这么简单,偷着乐吧,它就是采用了面向对象的继承和多态机制,其他没什么玄机。想想看,你真实的业务环境有这么简单吗?一个类实现多个接口很正常,你要有火眼金睛看清楚哪个接口是抽象策略接口,哪些是和策略模式没有任何关系,这就是你作为系统分析师的价值所在。

19.1 业务发展——上帝才能控制

有这样一句名言:“智者千虑必有一失,愚者千虑必有一得”^1,意思是说不管多聪明的人,经过多少次的思考,也总是会出现一些微小的错误,“智者”都是如此,何况我们这些平庸之辈呢!我们在进行系统开发时,不管之前的可行性分析、需求分析、系统设计处理得多么完美,总会在关键时候、关键场合出现一些“意外”。对于这些“意外”,该来的还是要来, 躲是躲不过去的,那我们怎么来弥补这些“意外”呢?这难不倒我们的设计大师,他们创造出了一些补救模式,今天我们就来讲一个补救模式,这种模式可以让你从因业务扩展而系统无法迅速适应的苦恼中解脱而出。

2004年我带了一个项目,做一个人力资源管理项目,该项目是我们总公司发起的,公司一共有700多号人。这个项目还是比较简单的,分为三大模块:人员信息管理、薪酬管理、 职位管理。当时开发时业务人员明确指明:人员信息管理的对象是所有员工的所有信息,所有的员工指的是在职的员工,其他的离职的、退休的暂不考虑。根据需求我们设计了如图19-1所示的类图。

image-20210929155745649

图19-1 人员信息类图

非常简单,有一个对象UserInfo存储用户的所有信息(实际系统上还有很多子类,不多说了),也就是BO(Business Object,业务对象),这个对象设计为贫血对象(Thin Business Object),不需要存储状态以及相关的关系,本人是反对使用充血对象(Rich Business Object),这里提到两个名词:贫血对象和充血对象,这两个名词很简单,在领域模型中分别叫做贫血领域模型和充血领域模型,有什么区别呢?一个对象如果不存储实体状态以及对象之间的关系,该对象就叫做贫血对象,对应的领域模型就是贫血领域模型,有实体状态和对象关系的模型就是充血领域模型。看不懂没关系,都是糊弄人的东西,属于专用名词。扯远了,我们继续说我们的人力资源管理项目,这个UserInfo对象,在系统中很多地方使用,你可以查看自己的信息,也可以修改,当然这个对象是有setter方法的,我们这里用不到就隐藏掉了。先来看接口,员工信息接口如代码清单19-1所示。

代码清单19-1 员工信息接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public interface IUserInfo {
//获得用户姓名
public String getUserName();
//获得家庭地址
public String getHomeAddress();
//手机号码,这个太重要,手机泛滥呀
public String getMobileNumber();
//办公电话,一般是座机
public String getOfficeTelNumber();
//这个人的职位是什么
public String getJobPosition();
//获得家庭电话,这有点不好,我不喜欢打家庭电话讨论工作
public String getHomeTelNumber();
}

员工信息接口有了,就需要设计一个实现类来容纳数据,如代码清单19-2所示。

代码清单19-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
public class UserInfo implements IUserInfo {
/*
* 获得家庭地址,下属送礼也可以找到地方
*/
public String getHomeAddress() {
System.out.println("这里是员工的家庭地址...");
return null;
}
/*
* 获得家庭电话号码
*/
public String getHomeTelNumber() {
System.out.println("员工的家庭电话是...");
return null;
}
/*
* 员工的职位,是部门经理还是普通职员
*/
public String getJobPosition() {
System.out.println("这个人的职位是BOSS...");
return null;
}
/*
* 手机号码
*/
public String getMobileNumber() {
System.out.println("这个人的手机号码是0000...");
return null;
}
/*
* 办公室电话,烦躁的时候最好"不小心"把电话线踢掉
*/
public String getOfficeTelNumber() {
System.out.println("办公室电话是...");
return null;
}
/*
* 姓名,这个很重要
*/
public String getUserName() {

System.out.println("姓名叫做...");
return null;
}
}

这个项目是2004年年底投产的,运行到2005年年底还是比较平稳的,中间修修补补也很正常,2005年年底不知道是哪股风吹的,很多公司开始使用借聘人员的方式引进人员,我们公司也不例外,从一个劳动资源公司借用了一大批的低技术、低工资的人员,分配到各个子公司,总共有将近200人,然后人力资源部就找我们部门老大谈判,说要增加一个功能:借用人员管理,老大一看有钱赚呀,一拍大腿,做!

老大命令一下来,我立马带人过去调研,需求就一句话,但是真深入地调研还真不是那么简单。借聘人员虽然在我们公司干活,和我们一个样,干活时没有任何的差别,但是他们的人员信息、工资情况、福利情况等都是由劳动服务公司管理的,并且有一套自己的人员管理系统,人力资源部门就要求我们系统同步劳动服务公司的信息,当然是只要在我们公司工作的人员信息,其他人员信息是不需要的,而且还要求信息同步,也就是:劳动服务公司的人员信息一变更,我们系统就应该立刻体现出来,为什么要即时而不批量呢?是因为我们公司与劳动服务公司之间是按照人头收费的,甭管是什么人,只要我们公司借用,就这个价格,我要一个研究生,你派了一个高中生给我,那算什么事?因此,了解了业务需求用后, 项目组决定采用RMI(Remote Method Invocation,远程对象调用)的方式进行联机交互,但是深入分析后,一个重大问题立刻显现出来:劳动服务公司的人员对象和我们系统的对象不相同,他们的对象如下所示。

image-20210929160052594

图19-2 劳动服务公司的人员信息类图

劳动服务公司是把人员信息分为了三部分:基本信息、办公信息和个人家庭信息,并且都放到了HashMap中,比如人员的姓名放到BaseInfo信息中,家庭地址放到HomeInfo中,这也是一个可以接受的模式,我们来看看他们的代码,接口如代码清单19-3所示。

代码清单19-3 劳动服务公司的人员信息接口

1
2
3
4
5
6
7
8
public interface IOuterUser {
//基本信息,比如名称、性别、手机号码等
public Map getUserBaseInfo();
//工作区域信息
public Map getUserOfficeInfo();
//用户的家庭信息
public Map getUserHomeInfo();
}

劳动服务公司的人员信息是这样存放的,如代码清单19-4所示。

代码清单19-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
public class OuterUser implements IOuterUser {
/*
* 用户的基本信息
*/
public Map getUserBaseInfo() {
HashMap baseInfoMap = new HashMap();
baseInfoMap.put("userName", "这个员工叫混世魔王...");
baseInfoMap.put("mobileNumber", "这个员工电话是...");
return baseInfoMap;
}
/*
* 员工的家庭信息
*/
public Map getUserHomeInfo() {
HashMap homeInfo = new HashMap();
homeInfo.put("homeTelNumbner", "员工的家庭电话是...");
homeInfo.put("homeAddress", "员工的家庭地址是...");
return homeInfo;
}
/*
* 员工的工作信息,比如,职位等
*/
public Map getUserOfficeInfo() {
HashMap officeInfo = new HashMap();
officeInfo.put("jobPosition","这个人的职位是BOSS...");
officeInfo.put("officeTelNumber", "员工的办公电话是...");
return officeInfo;
}
}

看到这里,咱不好说他们系统设计得不好,问题是咱的系统要和他们的系统进行交互, 怎么办?我们不可能为了这一小小的功能而对我们已经运行良好系统进行大手术,那怎么办?我们可以转化,先拿到对方的数据对象,然后转化为我们自己的数据对象,中间加一层转换处理,按照这个思路,我们设计了如图19-3所示的类图。

image-20210929161402861

图19-3 增加了中转处理的人员信息类图

大家可能会问,这两个对象都不在一个系统中,你如何使用呢?简单!RMI已经帮我们做了这件事情,只要有接口,就可以把远程的对象当成本地的对象使用,这个大家有时间可以去看一下RMI文档,不多说了。OuterUserInfo可以看做是“两面派”,实现了IUserInfo接口, 还继承了OuterUser,通过这样的设计,把OuterUser伪装成我们系统中一个IUserInfo对象,这样,我们的系统基本不用修改,所有的人员查询、调用跟本地一样。


注意 我们之所以能够增加一个OuterUserInfo中转类,是因为我们在系统设计时严格遵守了依赖倒置原则和里氏替换原则,否则即使增加了中转类也无法解决问题。


说得口干舌燥,下边我们来看具体的代码实现,中转角色OuterUserInfo如代码清单19-5 所示。

代码清单19-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
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 OuterUserInfo extends OuterUser implements IUserInfo {
private Map baseInfo = super.getUserBaseInfo();
//员工的基本信息
private Map homeInfo = super.getUserHomeInfo();
//员工的家庭信息
private Map officeInfo = super.getUserOfficeInfo();
//工作信息
/*
* 家庭地址
*/
public String getHomeAddress() {
String homeAddress = (String)this.homeInfo.get("homeAddress");
System.out.println(homeAddress);
return homeAddress;
}
/*
* 家庭电话号码
*/
public String getHomeTelNumber() {
String homeTelNumber = (String)this.homeInfo.get("homeTelNumber");
System.out.println(homeTelNumber);
return homeTelNumber;
}
/*
*职位信息
*/
public String getJobPosition() {
String jobPosition = (String)this.officeInfo.get("jobPosition");
System.out.println(jobPosition);
return jobPosition;
}
/*
* 手机号码
*/
public String getMobileNumber() {
String mobileNumber = (String)this.baseInfo.get("mobileNumber");
System.out.println(mobileNumber);
return mobileNumber;
}
/*
* 办公电话
*/
public String getOfficeTelNumber() {
String officeTelNumber = (String)this.officeInfo.get("officeTelNumber");
System.out.println(officeTelNumber);
return officeTelNumber;
}
/*
* 员工的名称
*/
public String getUserName() {
String userName = (String)this.baseInfo.get("userName");
System.out.println(userName);
return userName;
}
}

大家看到没?中转的角色有很多的强制类型转换,就是(String)这个东西,如果使用泛型的话,就可以完全避免这个转化(当然了,泛型当时还没有诞生)。我们要看看这个中转是否真的起到了中转的作用,我们想象这样一个场景:公司大老板想看看我们自己公司年轻女孩子的电话号码,那该场景类就如代码清单19-6所示。

代码清单19-6 场景类

1
2
3
4
5
6
7
8
9
10
public class Client {
public static void main(String[] args) {
//没有与外系统连接的时候,是这样写的
IUserInfo youngGirl = new UserInfo();
//从数据库中查到101个
for(int i=0;i<101;i++){
youngGirl.getMobileNumber();
}
}
}

这老板比较色呀。从数据库中生成了101个UserInfo对象,直接打印出来就成了。老板回头一想,不对呀,兔子不吃窝边草,还是调取借用人员看看,于是要查询出借用人员中美女的电话号码,如代码清单19-7所示。

代码清单19-7 查看劳动服务公司人员信息场景

1
2
3
4
5
6
7
8
9
10
11
public class Client {
public static void main(String[] args) {
//老板一想不对呀,兔子不吃窝边草,还是找借用人员好点
//我们只修改了这句话
IUserInfo youngGirl = new OuterUserInfo();
//从数据库中查到101个
for(int i=0;i<101;i++){
youngGirl.getMobileNumber();
}
}
}

大家看,使用了适配器模式只修改了一句话,其他的业务逻辑都不用修改就解决了系统对接的问题,而且在我们实际系统中只是增加了一个业务类的继承,就实现了可以查本公司的员工信息,也可以查人力资源公司的员工信息,尽量少的修改,通过扩展的方式解决了该问题。这就是适配模式。

19.2 适配器模式的定义

适配器模式(Adapter Pattern)的定义如下:

Convert the interface of a class into another interface clients expect.Adapter lets classes work together that couldn’t otherwise because of incompatible interfaces.(将一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口不匹配而无法在一起工作的两个类能够在一起工作。)

适配器模式又叫做变压器模式,也叫做包装模式(Wrapper),但是包装模式可不止一个,还包括了第17章讲解的装饰模式。适配器模式的通用类图,如图19-4所示。

image-20210929162035428

图19-4 适配器模式通用类图
适配器模式在生活中还是很常见的,比如你笔记本上的电源适配器,可以使用在110~ 220V之间变化的电源,而笔记本还能正常工作,这也是适配器一个良好模式的体现,简单地说,适配器模式就是把一个接口或类转换成其他的接口或类,从另一方面来说,适配器模式也就是一个包装模式,为什么呢?它把Adaptee包装成一个Target接口的类,加了一层衣服, 包装成另外一个靓妞了。大家知道,设计模式原是为建筑设计而服务的,软件设计模式只是借用了人家的原理而已,那我们来看看最原始的适配器是如何设计的,如图19-5所示。

A、B两个图框代表已经塑模成型的物体A和物体B,那现在要求把A和B安装在一起使用,如何安装?两者的接口不一致,是不可能安装在一起使用的,那怎么办?引入一个物体C,如图19-6所示。

image-20210929162142279

图19-5 两个已经成型的物体

引入物体C后,C适应了物体A的接口,同时也适应了物体B的接口,然后三者就可以组合成一个完整的物体,如图19-7所示。

image-20210929162925512

图19-7 完美组合

其中的物体C就是我们说的适配器,它在中间起到了角色转换的作用,把原有的长条形接口转换了三角形接口。在我们软件业的设计模式中,适配器模式也是相似的功能,那我们先来看看适配器模式的三个角色。

  • Target目标角色

该角色定义把其他类转换为何种接口,也就是我们的期望接口,例子中的IUserInfo接口就是目标角色。

  • Adaptee源角色

你想把谁转换成目标角色,这个“谁”就是源角色,它是已经存在的、运行良好的类或对象,经过适配器角色的包装,它会成为一个崭新、靓丽的角色。

  • Adapter适配器角色

适配器模式的核心角色,其他两个角色都是已经存在的角色,而适配器角色是需要新建立的,它的职责非常简单:把源角色转换为目标角色,怎么转换?通过继承或是类关联的方式。

各个角色的职责都已经非常清楚,我们再来看看其通用源码,目标接口如代码清单19-8 所示。

代码清单19-8 目标角色

1
2
3
4
public interface Target {
//目标角色有自己的方法
public void request();
}

目标角色是一个已经在正式运行的角色,你不可能去修改角色中的方法,你能做的就是如何去实现接口中的方法,而且通常情况下,目标角色是一个接口或者是抽象类,一般不会是实现类。一个正在服役的目标角色,如代码清单19-9所示。

代码清单19-9 目标角色的实现类

1
2
3
4
5
public class ConcreteTarget implements Target {
public void request() {
System.out.println("if you need any help,pls call me!");
}
}

源角色也是已经在服役状态(当然,非要新建立一个源角色,然后套用适配器模式,那也没有任何问题),它是一个正常的类,其源代码如代码清单19-10所示。

代码清单19-10 源角色

1
2
3
4
5
6
public class Adaptee {
//原有的业务逻辑
public void doSomething(){
System.out.println("I'm kind of busy,leave me alone,pls!");
}
}

我们的核心角色要出场了,适配器角色如代码清单19-11所示。

代码清单19-11 适配器角色

1
2
3
4
5
public class Adapter extends Adaptee implements Target {
public void request() {
super.doSomething();
}
}

所有的角色都已经在场了,那我们就开始看看这场演出,场景类如代码清单19-12所示。

代码清单19-12 场景类

1
2
3
4
5
6
7
8
9
10
public class Client {
public static void main(String[] args) {
//原有的业务逻辑
Target target = new ConcreteTarget();
target.request();
//现在增加了适配器角色后的业务逻辑
Target target2 = new Adapter();
target2.request();
}
}

适配器模式的原理就讲这么多吧,但是别得意得太早了,如果你认为适配器模式就这么简单,那我告诉你,你错了!复杂的还在后面。

20.1 整理项目信息——苦差事

周五下午,我正在看技术网站,第六感官发觉有人在身后,扭头一看,老大站在背后, 我赶忙站起来。

“王经理,你找我?”

“哦,在看技术呀。有个事情找你谈一下,你到我办公室来一下。”

到老大办公室还没坐稳,老大就开始发话了。

“是这样,刚刚我在看季报,我们每个项目的支出费用都很高,项目情况复杂,人员情况也不简单,我看着也有点糊涂,你看,这是我们现在还在开发或者维护的103个项目,项目信息很乱,很多是两年前的信息,你能不能先把这些项目最新情况重新打印一份给我,咱们好查查到底有什么问题。”老大说。

“这个好办,我马上去办!”我爽快地答复道。

很快我设计了一个类图,准备实施,如图20-1所示。

image-20210929164604989

图20-1 项目信息类图

简单得不能再简单的类图,是个程序员都能实现。我们来看看这个简单的东西,先看接口,如代码清单20-1所示。

代码清单20-1 项目信息接口

1
2
3
4
public interface IProject {
//从老板这里看到的就是项目信息
public String getProjectInfo();
}

定义了一个接口,面向接口编程嘛,当然要定义接口了,然后看看实现类,如代码清单20-2所示。

代码清单20-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
public class Project implements IProject {
//项目名称
private String name = "";
//项目成员数量
private int num = 0;
//项目费用
private int cost = 0;
//定义一个构造函数,把所有老板需要看到的信息存储起来
public Project(String name,int num,int cost){
//赋值到类的成员变量中
this.name = name;
this.num = num;
this.cost=cost;
}
//得到项目的信息
public String getProjectInfo() {
String info = "";
//获得项目的名称
info = info+ "项目名称是:" + this.name;
//获得项目人数
info = info + "\t项目人数: "+ this.num;
//项目费用
info = info+ "\t 项目费用:"+ this.cost;
return info;
}
}

实现类也是极度简单,通过构造函数把要显示的数据传递过来,然后放到getProjectInfo 中显示,这太容易了!然后我们老大要看看结果了,如代码清单20-3所示。

代码清单20-3 老大看报表的场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Boss {
public static void main(String[] args) {
//定义一个List,存放所有的项目对象
ArrayList<IProject> projectList = new ArrayList<IProject>();
//增加星球大战项目
projectList.add(new Project("星球大战项目",10,100000));
//增加扭转时空项目
projectList.add(new Project("扭转时空项目",100,10000000));
//增加超人改造项目
projectList.add(new Project("超人改造项目",10000,1000000000));
//这边100个项目
for(int i=4;i<104;i++){
projectList.add(new Project("第"+i+"个项目",i*5,i*1000000));
}
//遍历一下ArrayList,把所有的数据都取出
for(IProject project:projectList){
System.out.println(project.getProjectInfo());
}
}
}

然后看一下我们的运行结果,如下所示:

1
2
3
4
5
6
7
8
项目名称是:星球大战项目 项目人数: 10 项目费用:100000 
项目名称是:扭转时空项目 项目人数: 100 项目费用:10000000
项目名称是:超人改造项目 项目人数: 10000 项目费用:1000000000
项目名称是:第4个项目 项目人数: 20 项目费用:4000000
项目名称是:第5个项目 项目人数: 25 项目费用:5000000
.
.
.

老大一看,非常开心,这么快就出结果了,大大地把我夸奖了一番,然后就去埋头研究那堆枯燥的报表了。我回到座位上,又看了一遍程序(心里很乐,就又想看看自己的成果),想想(一日三省嘛),应该还有另外一种实现方式,因为是遍历嘛,让我想到的就是Java的迭代器接口java.util.iterator,它的作用就是遍历Collection集合下的元素,那我们的程序还可以有另外一种实现,通过实现iterator接口来实现遍历,先修正一下类图,如图20-2所示。

image-20210929164914932

图20-2 增加迭代接口的类图
看着是不是复杂了很多?是的,是有点复杂了,是不是我们把简单的事情复杂化了?请读者继续阅读下去,我等会儿说明原因。我们先分析一下我们的类图java.util.Iterator接口中声明了三个方法,这是JDK定义的, ProjectIterator 实现该接口,并且聚合了Project对象,也就是把Project对象作为本对象的成员变量使用。看类图还不是很清晰,我们一起看一下代码,先看IProject接口的改变,如代码清单20-4所示。

代码清单20-4 项目信息接口

1
2
3
4
5
6
7
8
public interface IProject {
//增加项目
public void add(String name,int num,int cost);
//从老板这里看到的就是项目信息
public String getProjectInfo();
//获得一个可以被遍历的对象
public IProjectIterator iterator();
}

这里多了两个方法,一个是add方法,这个方法是增加项目,也就是说产生了一个对象后,直接使用add方法增加项目信息。我们再来看其实现类,如代码清单20-5所示。

代码清单20-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
34
35
36
37
38
public class Project implements IProject {
//定义一个项目列表,说有的项目都放在这里
private ArrayList<IProject> projectList = new ArrayList<IProject>();
//项目名称
private String name = "";
//项目成员数量
private int num = 0;
//项目费用
private int cost = 0;
public Project(){
}
//定义一个构造函数,把所有老板需要看到的信息存储起来
private Project(String name,int num,int cost){
//赋值到类的成员变量中
this.name = name;
this.num = num;
this.cost=cost;
}
//增加项目
public void add(String name,int num,int cost){
this.projectList.add(new Project(name,num,cost));
}
//得到项目的信息
public String getProjectInfo() {
String info = "";
//获得项目的名称
info = info+ "项目名称是:" + this.name;
//获得项目人数
info = info + "\t项目人数: "+ this.num;
//项目费用
info = info+ "\t 项目费用:"+ this.cost;
return info;
}
//产生一个遍历对象
public IProjectIterator iterator(){
return new ProjectIterator(this.projectList);
}
}

通过构造函数,传递了一个项目所必需的信息,然后通过iterator()方法,把所有项目都返回到一个迭代器中。Iterator()方法看不懂不要紧,继续向下阅读。再看IProjectIterator接口,如代码清单20-6所示。

代码清单20-6 项目迭代器接口

1
2
public interface IProjectIterator extends Iterator {
}

大家可能对该接口感觉很奇怪,你定义的这个接口方法、变量都没有,有什么意义呢? 有意义,所有的Java书上都会说要面向接口编程,你的接口是对一个事物的描述,也就是说我通过接口就知道这个事物有哪些方法,哪些属性,我们这里的IProjectIterator是要建立一个指向Project类的迭代器,目前暂时定义的就是一个通用的迭代器,可能以后会增加IProjectIterator的一些属性或者方法。当然了,你也可以在实现类上实现两个接口,一个是Iterator,一个是IProjectIterator(这时候,这个接口就不用继承Iterator),杀猪杀尾巴,各有各的杀法。我的习惯是:如果我要实现一个容器或者其他API提供接口时,我一般都自己先写一个接口继承,然后再继承自己写的接口,保证自己的实现类只用实现自己写的接口(接口传递,当然也要实现顶层的接口),程序阅读也清晰一些。我们继续看迭代器的实现类,如代码清单20-7所示。

代码清单20-7 项目迭代器

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 ProjectIterator implements IProjectIterator {
//所有的项目都放在ArrayList中
private ArrayList<IProject> projectList = new ArrayList<IProject>();
private int currentItem = 0;
//构造函数传入projectList
public ProjectIterator(ArrayList<IProject> projectList){
this.projectList = projectList;
}
//判断是否还有元素,必须实现
public boolean hasNext() {
//定义一个返回值
boolean b = true;
if(this.currentItem>=projectList.size()||this.projectList.get(this.currentItem)==null){
b =false;
}
return b;
}
//取得下一个值
public IProject next() {
return (IProject)this.projectList.get(this.currentItem++);
}
//删除一个对象
public void remove() {
//暂时没有使用到
}
}

细心的读者可能会从代码中发现一个问题,java.util.iterator接口中定义next()方法的返回值类型是E,而你在ProjectIterator中返回值却是IProject,E和IProject有什么关系?

E是JDK 1.5中定义的新类型:元素(Element),是一个泛型符号,表示一个类型,具体什么类型是在实现或运行时决定,总之它代表的是一种类型,你在这个实现类中把它定义为ProjectIterator,在另外一个实现类可以把它定义为String,都没有问题。它与Object这个类可是不同的,Object是所有类的父类,随便一个类你都可以把它向上转型到Object类,也只是因为它是所有类的父类,它才是一个通用类,而E是一个符号,代表所有的类,当然也代表Object了。

都写完毕了,看看我们的Boss类有多少改动,如代码清单20-8所示。

代码清单20-8 老板看报表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Boss {
public static void main(String[] args) {
//定义一个List,存放所有的项目对象
IProject project = new Project();
//增加星球大战项目
project.add("星球大战项目ddddd",10,100000);
//增加扭转时空项目
project.add("扭转时空项目",100,10000000);
//增加超人改造项目
project.add("超人改造项目",10000,1000000000);
//这边100个项目
for(int i=4;i<104;i++){
project.add("第"+i+"个项目",i*5,i*1000000);
}
//遍历一下ArrayList,把所有的数据都取出
IProjectIterator projectIterator = project.iterator();
while(projectIterator.hasNext()){
IProject p = (IProject)projectIterator.next();
System.out.println(p.getProjectInfo());
}
}
}

运行结果如下所示:

1
2
3
4
5
6
7
8
项目名称是:星球大战项目 项目人数: 10 项目费用:100000
项目名称是:扭转时空项目 项目人数: 100 项目费用:10000000
项目名称是:超人改造项目 项目人数: 10000 项目费用:1000000000
项目名称是:第4个项目 项目人数: 20 项目费用:4000000
项目名称是:第5个项目 项目人数: 25 项目费用:5000000
.
.
.

运行结果完全相同,但是上面的程序复杂性增加了不少,难道我们退回到原始时代了吗?非也,非也,只是我们回退到JDK 1.0.8版本的编程时代了,我们使用一种新的设计模式 ——迭代器模式。

20.2 迭代器模式的定义

迭代器模式(Iterator Pattern)目前已经是一个没落的模式,基本上没人会单独写一个迭代器,除非是产品性质的开发,其定义如下:

Provide a way to access the elements of an aggregate object sequentially without exposing its underlying representation.(它提供一种方法访问一个容器对象中各个元素,而又不需暴露该对象的内部细节。)

迭代器是为容器服务的,那什么是容器呢? 能容纳对象的所有类型都可以称之为容器,例如Collection集合类型、Set类型等,迭代器模式就是为解决遍历这些容器中的元素而诞生的。其通用类图,如图20-3所示。

image-20210929165357526

图20-3 迭代器模式的通用类图

迭代器模式提供了遍历容器的方便性,容器只要管理增减元素就可以了,需要遍历时交由迭代器进行。迭代器模式正是由于使用得太频繁,所以大家才会忽略,我们来看看迭代器模式中的各个角色:

  • Iterator抽象迭代器

抽象迭代器负责定义访问和遍历元素的接口,而且基本上是有固定的3个方法:first()获得第一个元素,next()访问下一个元素,isDone()是否已经访问到底部(Java叫做hasNext()方法)。

  • ConcreteIterator具体迭代器

具体迭代器角色要实现迭代器接口,完成容器元素的遍历。

  • Aggregate抽象容器

容器角色负责提供创建具体迭代器角色的接口,必然提供一个类似createIterator()这样的方法,在Java中一般是iterator()方法。

  • Concrete Aggregate具体容器

具体容器实现容器接口定义的方法,创建出容纳迭代器的对象。

我们来看迭代器模式的通用源代码,先看抽象迭代器Iterator,如代码清单20-9所示。

代码清单20-9 抽象迭代器

1
2
3
4
5
6
7
8
public interface Iterator {
//遍历到下一个元素
public Object next();
//是否已经遍历到尾部
public boolean hasNext();
//删除当前指向的元素
public boolean remove();
}

具体迭代器如代码清单20-10所示。

代码清单20-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
public class ConcreteIterator implements Iterator {
private Vector vector = new Vector();

//定义当前游标
public int cursor = 0;
@SuppressWarnings("unchecked") public ConcreteIterator(Vector _vector){
this.vector = _vector;
}
//判断是否到达尾部
public boolean hasNext() {
if(this.cursor == this.vector.size()){
return false;
}
else{
return true;
}
}
//返回下一个元素
public Object next() {
Object result = null;
if(this.hasNext()){
result = this.vector.get(this.cursor++);
}
else{
result = null;
}
return result;
}
//删除当前元素
public boolean remove() {
this.vector.remove(this.cursor);
return true;
}
}

注意 开发系统时,迭代器的删除方法应该完成两个逻辑:一是删除当前元素,二是当前游标指向下一个元素。


抽象容器如代码清单20-11所示。

代码清单20-11 抽象容器

1
2
3
4
5
6
7
8
public interface Aggregate {
//是容器必然有元素的增加
public void add(Object object);
//减少元素
public void remove(Object object);
//由迭代器来遍历所有的元素
public Iterator iterator();
}

具体容器如代码清单20-12所示。

代码清单20-12 具体容器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ConcreteAggregate implements Aggregate {
//容纳对象的容器
private Vector vector = new Vector();
//增加一个元素
public void add(Object object) {
this.vector.add(object);
}
//返回迭代器对象
public Iterator iterator() {
return new ConcreteIterator(this.vector);
}
//删除一个元素
public void remove(Object object) {
this.remove(object);
}
}

场景类如代码清单20-13所示。

代码清单20-13 场景类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Client {
public static void main(String[] args) {
//声明出容器
Aggregate agg = new ConcreteAggregate();
//产生对象数据放进去
agg.add("abc");
agg.add("aaa");
agg.add("1234");
//遍历一下
Iterator iterator = agg.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
}
}
}

简单地说,迭代器就类似于一个数据库中的游标,可以在一个容器内上下翻滚,遍历所有它需要查看的元素。

21.2 组合模式的定义

组合模式(Composite Pattern)也叫合成模式,有时又叫做部分-整体模式(Part-Whole), 主要是用来描述部分与整体的关系,其定义如下:

Compose objects into tree structures to represent part-whole hierarchies.Composite lets clients treat individual objects and compositions of objects uniformly.(将对象组合成树形结构以表示“部分-整体”的层次结构,使得用户对单个对象和组合对象的使用具有一致性。)

组合模式的通用类图,如图21-6所示。

image-20210929172108521

图21-6 组合模式通用类图
我们先来说说组合模式的几个角色。 - Component抽象构件角色

定义参加组合对象的共有方法和属性,可以定义一些默认的行为或属性,比如我们例子中的getInfo就封装到了抽象类中。

  • Leaf叶子构件

叶子对象,其下再也没有其他的分支,也就是遍历的最小单位。

  • Composite树枝构件

树枝对象,它的作用是组合树枝节点和叶子节点形成一个树形结构。

我们来看组合模式的通用源代码,首先看抽象构件,它是组合模式的精髓,如代码清单21-18所示。

代码清单21-18 抽象构件

1
2
3
4
5
6
public abstract class Component {
//个体和整体都具有的共享
public void doSomething(){
//编写业务逻辑
}
}

组合模式的重点就在树枝构件,其通用代码如代码清单21-19所示。

代码清单21-19 树枝构件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Composite extends Component {
//构件容器
private ArrayList<Component> componentArrayList = new ArrayList<Component>();
//增加一个叶子构件或树枝构件
public void add(Component component){
this.componentArrayList.add(component);
}
//删除一个叶子构件或树枝构件
public void remove(Component component){
this.componentArrayList.remove(component);
}
//获得分支下的所有叶子构件和树枝构件
public ArrayList<Component> getChildren(){
return this.componentArrayList;
}
}

树叶节点是没有子下级对象的对象,定义参加组合的原始对象行为,其通用源代码如代码清单21-20所示。

代码清单21-20 树叶构件

1
2
3
4
5
6
7
8
public class Leaf extends Component {
/*
* 可以覆写父类方法
* public void doSomething(){
*
* }
*/
}

场景类负责树状结构的建立,并可以通过递归方式遍历整个树,如代码清单21-21所示。

代码清单21-21 场景类

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
public class Client {
public static void main(String[] args) {
//创建一个根节点
Composite root = new Composite();
root.doSomething();
//创建一个树枝构件
Composite branch = new Composite();
//创建一个叶子节点
Leaf leaf = new Leaf();
//建立整体
root.add(branch);
branch.add(leaf);
}
//通过递归遍历树
public static void display(Composite root){
for(Component c:root.getChildren()){
if(c instanceof Leaf){
//叶子节点
c.doSomething();
}
else{
//树枝节点
display((Composite)c);
}
}
}
}

各位可能已经看出一些问题了,组合模式是对依赖倒转原则的破坏,但是它还有其他类型的变形,面向对象就是这么多的形态和变化,请读者继续阅读下去,就会找到解决方案。

22.2 观察者模式的定义

观察者模式(Observer Pattern)也叫做发布订阅模式(Publish/subscribe),它是一个在项目中经常使用的模式,其定义如下:

Define a one-to-many dependency between objects so that when one object changes state,all its dependents are notified and updated automatically.(定义对象间一种一对多的依赖关系,使得每当一个对象改变状态,则所有依赖于它的对象都会得到通知并被自动更新。)

观察者模式的通用类图,如图22-5所示。

image-20210929200041358

图22-5 观察者模式通用类图
我们先来解释一下观察者模式的几个角色名称: - Subject被观察者

定义被观察者必须实现的职责,它必须能够动态地增加、取消观察者。它一般是抽象类或者是实现类,仅仅完成作为被观察者必须实现的职责:管理观察者并通知观察者。

  • Observer观察者

观察者接收到消息后,即进行update(更新方法)操作,对接收到的信息进行处理。

  • ConcreteSubject具体的被观察者

定义被观察者自己的业务逻辑,同时定义对哪些事件进行通知。

  • ConcreteObserver具体的观察者

每个观察在接收到消息后的处理反应是不同,各个观察者有自己的处理逻辑。

各个名词介绍完毕,我们来看看各自的通用代码,先看被观察者角色,如代码清单22- 15所示。

代码清单22-15 被观察者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public abstract class Subject {
//定义一个观察者数组
private Vector<Observer> obsVector = new Vector<Observer>();
//增加一个观察者
public void addObserver(Observer o){
this.obsVector.add(o);
}
//删除一个观察者
public void delObserver(Observer o){
this.obsVector.remove(o);
}
//通知所有观察者
public void notifyObservers(){
for(Observer o:this.obsVector){
o.update();
}
}
}

被观察者的职责非常简单,就是定义谁能够观察,谁不能观察,程序中使用ArrayList和Vector没有太大的差别,ArrayList是线程异步,不安全;Vector是线程同步,安全——就这点区别。我们再来看具体的被观察者,如代码清单22-16所示。

代码清单22-16 具体被观察者

1
2
3
4
5
6
7
8
9
public class ConcreteSubject extends Subject {
//具体的业务
public void doSomething(){
/*
* do something
*/
super.notifyObservers();
}
}

我们现在看到的是一个纯净的观察者,在具体项目中该类有很多的变种,在22.4一节中介绍。

我们再来看观察者角色,如代码清单22-17所示。

代码清单22-17 观察者

1
2
3
4
public interface Observer {
//更新方法
public void update();
}

观察者一般是一个接口,每一个实现该接口的实现类都是具体观察者,如代码清单22- 18所示。

代码清单22-18 具体观察者

1
2
3
4
5
6
public class ConcreteObserver implements Observer {
//实现更新方法
public void update() {
System.out.println("接收到信息,并进行处理!");
}
}

那其他模块是怎么来调用的呢?我们编写一个Client类来描述,如代码清单22-19所示。

代码清单22-19 场景类

1
2
3
4
5
6
7
8
9
10
11
12
public class Client {
public static void main(String[] args) {
//创建一个被观察者
ConcreteSubject subject = new ConcreteSubject();
//定义一个观察者
Observer obs= new ConcreteObserver();
//观察者观察被观察者
subject.addObserver(obs);
//观察者开始活动了
subject.doSomething();
}
}

23.1 我要投递信件

我们都写过纸质信件吧,比如给女朋友写情书什么的。写信的过程大家应该都还记得 ——先写信的内容,然后写信封,再把信放到信封中,封好,投递到信箱中进行邮递,这个过程还是比较简单的,虽然简单,但是这4个步骤都不可或缺!我们先把这个过程通过程序实现出来,如图23-1所示。

image-20210929203017422

图23-1 写信过程类图

这一个过程还是比较简单的,我们看程序的实现,先看接口,如代码清单23-1所示。

代码清单23-1 写信过程接口

1
2
3
4
5
6
7
8
9
10
public interface ILetterProcess {
//首先要写信的内容
public void writeContext(String context);
//其次写信封
public void fillEnvelope(String address);
//把信放到信封里
public void letterInotoEnvelope();
//然后邮递
public void sendLetter();
}

在接口中定义了完成的一个写信过程,这个过程需要实现,其实现类如代码清单23-2所示。

代码清单23-2 写信过程的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class LetterProcessImpl implements ILetterProcess {
//写信
public void writeContext(String context) {
System.out.println("填写信的内容..." + context);
}
//在信封上填写必要的信息
public void fillEnvelope(String address) {
System.out.println("填写收件人地址及姓名..." + address);
}
//把信放到信封中,并封好
public void letterInotoEnvelope() {
System.out.println("把信放到信封中...");
}
//塞到邮箱中,邮递
public void sendLetter() {
System.out.println("邮递信件...");
}
}

在这种环境下,最累的是写信人,为了发送一封信要有4个步骤,而且这4个步骤还不能颠倒,我们先看看这个过程如何通过程序表现出来,有人开始用这个过程写信了,如代码清单23-3所示。

代码清单23-3 场景类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Client {
public static void main(String[] args) {
//创建一个处理信件的过程
ILetterProcess letterProcess = new LetterProcessImpl();
//开始写信
letterProcess.writeContext("Hello,It's me,do you know who I am? I'm your old lover. I'd like to...");
//开始写信封
letterProcess.fillEnvelope("Happy Road No. 666,God Province,Heaven");
//把信放到信封里,并封装好
letterProcess.letterInotoEnvelope();
//跑到邮局把信塞到邮箱,投递
letterProcess.sendLetter();
}
}

运行结果如下所示:

1
2
3
4
填写信的内容...Hello,It's me,do you know who I am? I'm your old lover.I'd like to...
填写收件人地址及姓名...Happy Road No.666,God Province,Heaven
把信放到信封中...
邮递信件...

我们回过头来看看这个过程,它与高内聚的要求相差甚远,更不要说迪米特法则、接口隔离原则了。你想想,你要知道这4个步骤,而且还要知道它们的顺序,一旦出错,信就不可能邮寄出去,这在面向对象的编程中是极度地不适合,它根本就没有完成一个类所具有的单一职责。

还有,如果信件多了就非常麻烦,每封信都要这样运转一遍,非得累死,更别说要发个广告信了,那怎么办呢?还好,现在邮局开发了一个新业务,你只要把信件的必要信息告诉我,我给你发,我来完成这4个过程,只要把信件交给我就成了,其他就不要管了。非常好的方案!我们来看类图,如图23-2所示。

image-20210929211319927

图23-2 增加现代化邮局的类图

这还是比较简单的类图,增加了一个ModenPostOffice类,负责对一个比较复杂的信件处理过程的封装,然后高层模块只要和它有交互就成了,如代码清单23-4所示。

代码清单23-4 现代化邮局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ModenPostOffice {
private ILetterProcess letterProcess = new LetterProcessImpl();
//写信,封装,投递,一体化
public void sendLetter(String context,String address){
//帮你写信
letterProcess.writeContext(context);
//写好信封
letterProcess.fillEnvelope(address);
//把信放到信封中
letterProcess.letterInotoEnvelope();
//邮递信件
letterProcess.sendLetter();
}
}

这个类是什么意思呢,就是说现在有一个Hell Road PostOffice(地狱路邮局)提供了一种新型服务,客户只要把信的内容以及收信地址给他们,他们就会把信写好,封好,并发送出去。这种服务推出后大受欢迎,这多简单,客户减少了很多工作,谁不乐意呀。那我们看看客户是怎么调用的,如代码清单23-5所示。

代码清单23-5 场景类

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Client {
public static void main(String[] args) {
//现代化的邮局,有这项服务,邮局名称叫Hell
Road ModenPostOffice hellRoadPostOffice = new ModenPostOffice();
//你只要把信的内容和收信人地址给他,他会帮你完成一系列的工作
//定义一个地址
String address = "Happy Road No. 666,God Province,Heaven";
//信的内容
String context = "Hello,It's me,do you know who I am? I'm your old lover. I'd like to....";
//你给我发送吧
hellRoadPostOffice.sendLetter(context, address);
}
}

运行结果是相同的。我们看看场景类是不是简化了很多,只要与ModenPostOffice交互就 成了,其他的什么都不用管,写信封啦、写地址啦……都不用关心,只要把需要的信息提交 过去就成了,邮局保证会按照我们指定的地址把指定的内容发送出去,这种方式不仅简单, 而且扩展性还非常好,比如一个非常时期,寄往God Province(上帝省)的邮件都必须进行 安全检查,那我们就很好处理了,如图23-3所示。

image-20210929220723841

图23-3 扩展后的系统类图

增加了一个Police类,负责对信件进行检查,如代码清单23-6所示。

代码清单23-6 信件检查类

1
2
3
4
5
6
public class Police {
//检查信件,检查完毕后警察在信封上盖个戳:此信无病毒
public void checkLetter(ILetterProcess letterProcess){
System.out.println(letterProcess+" 信件已经检查过了...");
}
}

我们再来看一下封装类ModenPostOffice的变更,它封装了这部分的变化,如代码清单23-7所示。

代码清单23-7 扩展后的现代化邮局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ModenPostOffice {
private ILetterProcess letterProcess = new LetterProcessImpl();
private Police letterPolice = new Police();
//写信,封装,投递,一体化了
public void sendLetter(String context,String address){
//帮你写信
letterProcess.writeContext(context);
//写好信封
letterProcess.fillEnvelope(address);
//警察要检查信件了
letterPolice.checkLetter(letterProcess);
//把信放到信封中
letterProcess.letterInotoEnvelope();
//邮递信件
letterProcess.sendLetter();
}
}

只是增加了一个letterPolice变量的声明以及一个方法的调用,那这个写信的过程就变成这样:先写信、写信封,然后警察开始检查,之后才把信放到信封,最后发送出去,那这个变更对客户来说是透明的,他根本就看不到有人在检查他的邮件,他也不用了解,反正现代化的邮件系统都帮他做了,这也是他乐意的地方。

场景类还是完全相同,但是运行结果稍有不同,如下所示:

1
2
3
4
5
6
填写信的内容...Hello,It's me,do you know who I am?I'm your old lover.I'd like to... 
填写收件人地址及姓名...Happy Road No.666,God Province,Heaven
com.cbf4life.common3.LetterProcessImpl@15ff48b
信件已经检查过了...
把信放到信封中...
邮递信件...

高层模块没有任何改动,但是信件却已经被检查过了。这正是我们设计所需要的模式, 不改变子系统对外暴露的接口、方法,只改变内部的处理逻辑,其他兄弟模块的调用产生了不同的结果,确实是一个非常棒的设计。这就是门面模式。

23.2 门面模式的定义

门面模式(Facade Pattern)也叫做外观模式,是一种比较常用的封装模式,其定义如下:

Provide a unified interface to a set of interfaces in a subsystem.Facade defines a higher-level interface that makes the subsystem easier to use.(要求一个子系统的外部与其内部的通信必须通过一个统一的对象进行。门面模式提供一个高层次的接口,使得子系统更易于使用。)

门面模式注重“统一的对象”,也就是提供一个访问子系统的接口,除了这个接口不允许有任何访问子系统的行为发生,其通用类图,如图23-4所示。

image-20210929221159108

图23-4 扩展后的系统类图

是的,类图就这么简单,但是它代表的意义可是异常复杂,Subsystem Classes是子系统所有类的简称,它可能代表一个类,也可能代表几十个对象的集合。甭管多少对象,我们把这些对象全部圈入子系统的范畴,其结构如图23-5所示。

image-20210929221239108

图23-5 门面模式示意图

再简单地说,门面对象是外界访问子系统内部的唯一通道,不管子系统内部是多么杂乱无章,只要有门面对象在,就可以做到“金玉其外,败絮其中”。我们先明确一下门面模式的角色。

  • Facade门面角色

客户端可以调用这个角色的方法。此角色知晓子系统的所有功能和责任。一般情况下, 本角色会将所有从客户端发来的请求委派到相应的子系统去,也就说该角色没有实际的业务逻辑,只是一个委托类。

  • subsystem子系统角色

可以同时有一个或者多个子系统。每一个子系统都不是一个单独的类,而是一个类的集合。子系统并不知道门面的存在。对于子系统而言,门面仅仅是另外一个客户端而已。

我们来看一下门面模式的通用源码,先来看子系统源代码。由于子系统是类的集合,因此要描述该集合很花费精力,每一个子系统都不相同,我们使用3个相互无关的类来代表, 如代码清单23-8所示。

代码清单23-8 子系统

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ClassA {
public void doSomethingA(){
//业务逻辑
}
}
public class ClassB {
public void doSomethingB(){
//业务逻辑
}
}
public class ClassC {
public void doSomethingC(){
//业务逻辑
}
}

我们认为这3个类属于近邻,处理相关的业务,因此应该被认为是一个子系统的不同逻辑处理模块,对于此子系统的访问需要通过门面进行,如代码清单23-9所示。

代码清单23-9 门面对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Facade {
//被委托的对象
private ClassA a = new ClassA();
private ClassB b = new ClassB();
private ClassC c = new ClassC();
//提供给外部访问的方法
public void methodA(){
this.a.doSomethingA();
}
public void methodB(){

this.b.doSomethingB();
}
public void methodC(){
this.c.doSomethingC();
}
}

24.1 如此追女孩子,你还不乐

大家有没有看过尼古拉斯·凯奇主演的《Next》(中文译名为《预见未来》)?尼古拉斯 ·凯奇饰演一个可以预视并且扭转未来的人,其中有一个情节很是让人心动——男女主角见面的那段情节:Cris Johnson(尼古拉斯·凯奇饰演)坐在咖啡吧台前,看着离自己近在咫尺的Callie Ferris(朱莉安·摩尔饰演),计划着怎么认识这个命中注定的女人,看Cris Johnson 如何利用自己的特异功能:

  • Cris Johnson端着一杯咖啡走过去,说“你好,可以认识你吗?”被拒绝,恢复到坐在咖 啡吧台前的状态。
  • 走过去询问是否可以搭车,被拒绝,恢复原状。
  • 帮助解决困境,被拒绝,恢复原状。
  • 采用嬉皮士的方式解决困境,被拒绝,恢复原状。
  • 帮助解决困境,被打伤,装可怜,Callie Ferris怜惜,于是乎相识了。

看看这是一件多么幸福的事情,追求一个女生可以多次反复地实验,直到找到好的方法和途径为止,这估计是大多数男生都希望获得的特异功能。想想看,看到一个心仪的女生, 我们若反复尝试,总会有一个方法打动她的,多美好的一件事。现在我们还得回到现实生活,我们来分析一下类似事情的经过:

  • 复制一个当前状态,保留下来,这个状态就是等会儿搭讪女孩子失败后要恢复的状 态,你不恢复原始状态,这不就露馅儿了吗?
  • 每次试探性尝试失败后,都必须恢复到这个原始状态。
  • N次试探总有一次成功吧,成功以后即可走成功路线。

想想看,我们这里的场景中最重要的是哪一块?对的,是原始状态的保留和恢复这块, 如何保留一个原始,如何恢复一个原始状态才是最重要的,那想想看,我们应该怎么实现呢?很简单呀,我们可以定义一个中间变量,保留这个原始状态。我们先看看类图,如图24-1所示。

image-20210929234433197

图24-1 男孩状态类图

太简单的类图了,我们来解释一下图中的状态state是什么意思,在某一时间点的所有位置信息、心理信息、环境信息都属于状态,我们这里用了一个标识性的名词state代表所有状态,比如在追女孩子前心情是期待、心理是焦躁不安等。每一次去认识女孩子都是会发生状态变化的,我们使用changeState方法来代替,由于程序比较简单,就没有编写接口,我们来看实现,如代码清单24-1所示。

代码清单24-1 男孩状态类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Boy {
//男孩的状态
private String state = "";
//认识女孩子后状态肯定改变,比如心情、手中的花等
public void changeState(){
this.state = "心情可能很不好";
}
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
}

程序是很简单,主要的业务逻辑是在场景类中,我们来看场景类是如何进行状态的保留、恢复的,如代码清单24-2所示。

代码清单24-2 场景类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Client {
public static void main(String[] args) {
//声明出主角
Boy boy = new Boy();
//初始化当前状态
boy.setState("心情很棒!");
System.out.println("=====男孩现在的状态======");
System.out.println(boy.getState());
//需要记录下当前状态呀
Boy backup = new Boy();
backup.setState(boy.getState());
//男孩去追女孩,状态改变
boy.changeState();
System.out.println("\n=====男孩追女孩子后的状态======");
System.out.println(boy.getState());
//追女孩失败,恢复原状
boy.setState(backup.getState());
System.out.println("\n=====男孩恢复后的状态======");
System.out.println(boy.getState());
}
}

程序运行结果如下所示:

1
2
3
4
5
6
=====男孩现在的状态====== 
心情很棒!
=====男孩追女孩子后的状态======
心情可能很不好
=====男孩恢复后的状态======
心情很棒!

程序运行正确,输出结果也是我们期望的,但是结果正确并不表示程序是最优的,我们来看看场景类Client,它代表的是高层模块,或者说是非“近亲”模块的调用者,注意看backup 变量的使用,它对于高层模块完全是多余的,为什么一个状态的保存和恢复要让高层模块来负责呢?这应该是Boy类的职责,而不应该让高层模块来完成,也就是破坏了Boy类的封装,或者说Boy类没有封装好,它应该是把backup的定义容纳进来,而不应该让高层模块来定义。

问题我们已经知道了,就是Boy类封装不够,那我们应该如何修改呢?如果在Boy类中再增加一个方法或者其他的内部类来保存这个状态,则对单一职责原则是一种破坏,想想看单一职责原则是怎么说的?一个类的职责应该是单一的,Boy类本身的职责是追求女孩子, 而保留和恢复原始状态则应该由另外一个类来承担,那我们把这个类取名就叫做备忘录,这和大家经常在桌面上贴的那个便签是一个概念,分析到这里我们的思路已经非常清楚了,我们来修改一下类图,如图24-2所示。

image-20210929234747033

图24-2 完善后的男孩状态类图
改动很小,增加了一个新的类Memento,负责状态的保存和备份;同时,在Boy类中增加了创建一份备忘录createMemento和恢复一个备忘录resotreMemento,我们先来看Boy类的变化,如代码清单24-3所示。

代码清单24-3 改进后的男孩状态类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Boy {
//男孩的状态
private String state = "";
//认识女孩子后状态肯定改变,比如心情、手中的花等
public void changeState(){
this.state = "心情可能很不好";
}
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
//保留一个备份
public Memento createMemento(){
return new Memento(this.state);
}
//恢复一个备份
public void restoreMemento(Memento _memento){
this.setState(_memento.getState());
}
}

注意看,确实只增加了两个方法创建备份和恢复备份,至于在什么时候创建备份和恢复备份则是由高层模块决定的。我们再来看备忘录模块,如代码清单24-4所示。

代码清单24-4 备忘录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Memento {
//男孩的状态
private String state = "";
//通过构造函数传递状态信息
public Memento(String _state){
this.state = _state;
}
public String getState() {
return state;
}
public void setState(String state) {

this.state = state;
}
}

这就是一个简单的JavaBean,保留男孩当时的状态信息。我们再来看场景类,稍做修改,如代码清单24-5所示。

代码清单24-5 改进后的场景类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Client {
public static void main(String[] args) {
//声明出主角
Boy boy = new Boy();
//初始化当前状态
boy.setState("心情很棒!");
System.out.println("=====男孩现在的状态======");
System.out.println(boy.getState());
//需要记录下当前状态呀
Memento mem = boy.createMemento();
//男孩去追女孩,状态改变
boy.changeState();
System.out.println("\n=====男孩追女孩子后的状态======");
System.out.println(boy.getState());
//追女孩失败,恢复原状
boy.restoreMemento(mem);
System.out.println("\n=====男孩恢复后的状态======");
System.out.println(boy.getState());
}
}

运行结果保持相同,虽然程序中不再重复定义Boy类的对象了,但是我们还是要关心备忘录,这对迪米特法则是一个亵渎,它告诉我们只和朋友类通信,那这个备忘录对象是我们必须要通信的朋友类吗?对高层模块来说,它最希望要做的就是创建一个备份点,然后在需要的时候再恢复到这个备份点就成了,它不用关心到底有没有备忘录这个类。那根据这一指导思想,我们就需要把备忘录类再包装一下,怎么包装呢?建立一个管理类,就是管理这个备忘录,如图24-3所示。

image-20210929235000932

图24-3 完整的男孩追女生类图

又增加了一个JavaBean,Boy类和Memento没有任何改变,不再赘述。我们来看增加的备忘录管理类,如代码清单24-6所示。

代码清单24-6 备忘录管理者

1
2
3
4
5
6
7
8
9
10
public class Caretaker {
//备忘录对象
private Memento memento;
public Memento getMemento() {
return memento;
}
public void setMemento(Memento memento) {
this.memento = memento;
}
}

这个太简单了,非常纯粹的一个JavaBean,甭管它多简单,只要有用就成,我们来看场景类如何调用,如代码清单24-7所示。

代码清单24-7 进一步改进后的场景类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Client {
public static void main(String[] args) {
//声明出主角
Boy boy = new Boy();
//声明出备忘录的管理者
Caretaker caretaker = new Caretaker();
//初始化当前状态
boy.setState("心情很棒!");
System.out.println("=====男孩现在的状态======");
System.out.println(boy.getState());
//需要记录下当前状态呀
caretaker.setMemento(boy.createMemento());
//男孩去追女孩,状态改变
boy.changeState();
System.out.println("\n=====男孩追女孩子后的状态======");
System.out.println(boy.getState());
//追女孩失败,恢复原状
boy.restoreMemento(caretaker.getMemento());
System.out.println("\n=====男孩恢复后的状态======");
System.out.println(boy.getState());
}
}

注意看黑体部分,就修改了这么多,看看程序的逻辑是不是清晰了很多,需要备份的时候就创建一个备份,然后丢给备忘录管理者进行管理,要取的时候再从管理者手中拿到这个备份。这个备份者就类似于一个备份的仓库管理员,创建一个丢进去,需要的时候再拿出来。这就是备忘录模式。