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();
}
}

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

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

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

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();
}
}

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();
}
}