31.1 代理模式VS装饰模式

对于两个模式,首先要说的是,装饰模式就是代理模式的一个特殊应用,两者的共同点是都具有相同的接口,不同点则是代理模式着重对代理过程的控制,而装饰模式则是对类的功能进行加强或减弱,它着重类的功能变化,我们举例来说明它们的区别。

31.1.1 代理模式

一个著名的短跑运动员有自己的代理人。如果你很仰慕他,你找运动员说“你跑个我看看”,运动员肯定不搭理你,不过你找到他的代理人就不一样了,你可能和代理人比较熟, 可以称兄道弟,这个忙代理人还是可以帮的,于是代理人同意让你欣赏运动员的练习赛,这对你来说已经是莫大的荣耀了。我们来看类图,如图31-1所示。

image-20210930161729112

图31-1 运动员跑步
这是一个套用代理模式的简单应用,非常简单!一个对象,然后再是自己的代理。我们先来看一下代码,先看抽象主题类,如代码清单31-1所示。

代码清单31-1 抽象运动员

1
2
3
4
public interface IRunner {
//运动员的主要工作就是跑步
public void run();
}

一个具体的短跑运动员跑步是很潇洒的,如代码清单31-2所示。

代码清单31-2 运动员跑步

1
2
3
4
5
public class Runner implements IRunner {
public void run() {
System.out.println("运动员跑步:动作很潇洒");
}
}

看看现在的明星运动员,一般都有自己的代理人,要么是专职的,要么就是自己的教练兼职,那我们来看看代理人的职责,如代码清单31-3所示。

代码清单31-3 代理人

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class RunnerAgent implements IRunner {
private IRunner runner;
public RunnerAgent(IRunner _runner){
this.runner = _runner;
}
//代理人是不会跑的
public void run() {
Random rand = new Random();
if(rand.nextBoolean()){
System.out.println("代理人同意安排运动员跑步");
runner.run();
}
else{
System.out.println("代理人心情不好,不安排运动员跑步");
}
}
}

我们只是定义了一个代理人,并没有明确定义是哪一个运动员的代理,需要在运行时指定被代理者,而且我们还在代理人的run方法中做了判断,想让被代理人跑步就跑步,不乐意就拒绝,对于主题类的行为是否可以发生,代理类有绝对的控制权。我们编写一个场景类来模拟这种情况,如代码清单31-4所示。

代码清单31-4 场景类

1
2
3
4
5
6
7
8
9
10
11
public class Client {
public static void main(String[] args) {
//定义一个短跑运动员
IRunner liu = new Runner();
//定义liu的代理人
IRunner agent = new RunnerAgent(liu);
//要求运动员跑步
System.out.println("====客人找到运动员的代理要求其去跑步===");
agent.run();
}
}

由于我们使用了随机数产生模拟结果,因此运行结果有两种可能情况,第一种情况如下所示:

1
2
3
4
5
6
====客人找到运动员的代理要求其去跑步=== 
代理人同意安排运动员跑步
运动员跑步:动作很潇洒
运行结果的第二种情况如下所示:
====客人找到运动员的代理要求其去跑步===
代理人心情不好,不安排运动员跑步

不管是哪种情况,我们都证实了代理的一个功能:在不改变接口的前提下,对过程进行控制。在我们例子中,运动员要不要跑步是由代理人决定的,代理人说跑步就跑步,说不跑就不跑,它有绝对判断权。

31.1.2 装饰模式

如果使用装饰模式,我们该怎么实现这个过程呢?装饰模式是对类功能的加强,怎么加强呢?增强跑步速度!在屁股后面安装一个喷气动力装置,类似火箭的喷气装置,那速度变得很快,《蜘蛛侠》中的那个反面角色不就是这样的吗?好,我们来看类图,如图31-2所示。

image-20210930162043387

图31-2 增强运动员的功能
很惊讶?这个代理模式完全一样的类图?是的,完全一样!不过其实现的意图却不同, 我们先来看代码,IRunner和Runner与代理模式相同,详见代码清单31-1和代码清单31-2所示,在此不再赘述。我们来看装饰类RunnerWithJet,如代码清单31-5所示。

代码清单31-5 装饰类

1
2
3
4
5
6
7
8
9
10
public class RunnerWithJet implements IRunner {
private IRunner runner;
public RunnerWithJet(IRunner _runner){
this.runner = _runner;
}
public void run() {
System.out.println("加快运动员的速度:为运动员增加喷气装置");
runner.run();
}
}

这和代理模式中的代理类也是非常相似的,只是装饰类对类的行为没有决定权,只有增强作用,也就是说它不决定被代理的方法是否执行,它只是再次增加被代理的功能。我们来看场景类,如代码清单31-6所示。

代码清单31-6 场景类

1
2
3
4
5
6
7
8
9
10
11
public class Client {
public static void main(String[] args) {
//定义运动员
IRunner liu = new Runner();
//对其功能加强
liu = new RunnerWithJet(liu);
//看看它的跑步情况如何
System.out.println("===增强后的运动员的功能===");
liu.run();
}
}

运行结果如下所示:

1
2
3
===增强后的运动员的功能=== 
加快运动员的速度:为运动员增加喷气装置
运动员跑步:动作很潇洒

注意思考一下我们的程序,我们通过增加了一个装饰类,就完成了对原有类的功能增加,由一个普通的短跑运动员变成了带有喷气装置的超人运动员,其速度岂是普通人能相比的?!

31.1.3 最佳实践

31.2 装饰模式VS适配器模式

装饰模式和适配器模式在通用类图上没有太多的相似点,差别比较大,但是它们的功能有相似的地方:都是包装作用,都是通过委托方式实现其功能。不同点是:装饰模式包装的是自己的兄弟类,隶属于同一个家族(相同接口或父类),适配器模式则修饰非血缘关系类,把一个非本家族的对象伪装成本家族的对象,注意是伪装,因此它的本质还是非相同接口的对象。

大家都应该听过丑小鸭的故事吧,我们今天就用这两种模式分别讲述丑小鸭的故事。话说鸭妈妈有5个孩子,其中4个孩子都是黄白相间的颜色,而最小的那只也就是叫做丑小鸭的那只,是纯白色的,与兄弟姐妹不相同,在遭受了诸多的嘲讽和讥笑后,最终丑小鸭变成了一只美丽的天鹅。那我们如何用两种不同模式来描述这一故事呢?

31.2.1 用装饰模式描述丑小鸭

用装饰模式来描述丑小鸭,首先就要肯定丑小鸭是一只天鹅,只是因为她小或者是鸭妈妈的无知才没有被认出是天鹅,经过一段时间后,它逐步变成一个漂亮、自信、优美的白天鹅。根据分析我们可以这样设计,先设计一个丑小鸭,然后根据时间先后来进行不同的美化处理,怎么美化呢?先长出漂亮的羽毛,然后逐步展现出异于鸭子的不同行为,如飞行,最终在具备了所有的行为后,它就成为一只纯粹的白天鹅了,我们来看类图,如图31-3所示。

image-20210930162553626

图31-3 装饰模式实现丑小鸭

类图比较简单,非常标准的装饰模式。我们按照故事的情节发展一步一步地实现程序。 初期的时候,丑小鸭表现得很另类,叫声不同,外形不同,致使周围的亲戚、朋友都对她鄙视,那我们来建立这个过程,由于丑小鸭的本质就是一个天鹅,我们就先生成一个天鹅的接口,如代码清单31-7所示。

代码清单31-7 天鹅接口

1
2
3
4
5
6
7
8
public interface Swan {
//天鹅会飞
public void fly();
//天鹅会叫
public void cry();
//天鹅都有漂亮的外表
public void desAppaearance();
}

我们定义了天鹅的行为,都会飞行、会叫,并且可以描述她们漂亮的外表。丑小鸭是一只白天鹅,是”is-a”的关系,也就是需要实现这个接口了,其实现如代码清单31-8所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class UglyDuckling implements Swan {
//丑小鸭的叫声
public void cry() {
System.out.println("叫声是克噜——克噜——克噜");
}
//丑小鸭的外形
public void desAppaearance() {
System.out.println("外形是脏兮兮的白色,毛茸茸的大脑袋");
}
//丑小鸭还比较小,不能飞
public void fly() {
System.out.println("不能飞行");
}
}

丑小鸭具备了天鹅的所有行为和属性,因为她本来就是一只白天鹅,只是因为她太小了还不能飞行,也不能照顾自己,所以丑丑的,在经过长时间的流浪生活后,丑小鸭长大了。 终于有一天,她发现自己竟然变成了一只美丽的白天鹅,有着漂亮、洁白的羽毛,而且还可以飞行,这完全是一种升华行为。我们来看看她的行为(飞行)和属性(外形)是如何加强的,先看抽象的装饰类,如代码清单31-9所示。

代码清单31-9 抽象装饰类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Decorator implements Swan {
private Swan swan;
//修饰的是谁
public Decorator(Swan _swan){
this.swan =_swan;
}
public void cry() {
swan.cry();
}
public void desAppaearance() {
swan.desAppaearance();
}
public void fly() {
swan.fly();
}
}

这是一个非常简单的代理模式。我们再来看丑小鸭是如何开始变得美丽的,变化是由外及里的,有了漂亮的外表才有内心的实质变化,如代码清单31-10所示。

代码清单31-10 外形美化

1
2
3
4
5
6
7
8
9
10
11
public class BeautifyAppearance extends Decorator {
//要美化谁
public BeautifyAppearance(Swan _swan){
super(_swan);
}
//外表美化处理
@Override
public void desAppaearance(){
System.out.println("外表是纯白色的,非常惹人喜爱!");
}
}

丑小鸭最后发现自己还能飞行,这是一个行为突破,是对原有行为“不会飞行”的一种强化,如代码清单31-11所示。

代码清单31-11 强化行为

1
2
3
4
5
6
7
8
9
10
public class StrongBehavior extends Decorator {
//强化谁
public StrongBehavior(Swan _swan){
super(_swan);
}
//会飞行了
public void fly(){
System.out.println("会飞行了!");
}
}

所有的故事元素我们都具备了,就等有人来讲故事了,场景类如代码清单31-12所示。

代码清单31-12 场景类

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 Client {
public static void main(String[] args) {
//很久很久以前,这里有一个丑陋的小鸭子
System.out.println("===很久很久以前,这里有一只丑陋的小鸭子===");
Swan duckling = new UglyDuckling();
//展示一下小鸭子
duckling.desAppaearance();
//小鸭子的外形
duckling.cry();
//小鸭子的叫声
duckling.fly();
//小鸭子的行为
System.out.println("\n===小鸭子终于发现自己是一只天鹅====");
//首先外形变化了
duckling = new BeautifyAppearance(duckling);
//其次行为也发生了改变
duckling = new StrongBehavior(duckling);
//虽然还是叫丑小鸭,但是已经发生了很大变化
duckling.desAppaearance();
//小鸭子的新外形
duckling.cry();
//小鸭子的新叫声
duckling.fly();
//小鸭子的新行为
}
}

运行结果如下所示:

1
2
3
4
5
6
7
8
===很久很久以前,这里有一只丑陋的小鸭子=== 
外形是脏兮兮的白色,毛茸茸的大脑袋
叫声是克噜——克噜——克噜
不能飞行
===小鸭子终于发现自己是一只天鹅====
外表是纯白色的,非常惹人喜爱!
叫声是克噜——克噜——克噜
会飞行了!

使用装饰模式描述丑小鸭蜕变的过程是如此简单,它关注了对象功能的强化,是对原始对象的行为和属性的修正和加强,把原本被人歧视、冷落的丑小鸭通过两次强化处理最终转变为受人喜爱、羡慕的白天鹅。

31.2.2 用适配器模式实现丑小鸭

采用适配器模式实现丑小鸭变成白天鹅的过程要从鸭妈妈的角度来分析,鸭妈妈有5个孩子,它认为这5个孩子都是她的后代,都是鸭类,但是实际上是有一只(也就是丑小鸭) 不是真正的鸭类,她是一只小白天鹅,就像《木兰辞》中说的“雄兔脚扑朔,雌兔眼迷离。 双兔傍地走,安能辨我是雄雌?”同样,因为太小,差别太细微,很难分辨,导致鸭妈妈认为她是一只鸭子,从鸭子的审美观来看,丑小鸭是丑陋的。通过分析,我们要做的就是要设计两个对象:鸭和天鹅,然后鸭妈妈把一只天鹅看成了小鸭子,最终时间到来的时候丑小鸭变成了白天鹅。我们来看类图,如图31-4所示。

image-20210930163243263

图31-4 适配器模式实现丑小鸭

类图非常简单,我们定义了两个接口:鸭类接口和天鹅类接口,然后建立了一个适配器UglyDuckling,把一只白天鹅封装成了小鸭子。我们来看代码,先看鸭类接口,如代码清单31-13所示。

代码清单31-13 鸭类接口

1
2
3
4
5
6
7
8
public interface Duck {
//会叫
public void cry();
//鸭子的外形
public void desAppearance();
//描述鸭子的其他行为
public void desBehavior();
}

鸭类有3个行为,一个是鸭会叫,一个是外形描述,还有一个是综合性的其他行为描述,例如会游泳等。我们来看鸭妈妈的4个正宗孩子,如代码清单31-14所示。

代码清单31-14 小鸭子

1
2
3
4
5
6
7
8
9
10
11
12
public class Duckling implements Duck {
public void cry() {
System.out.println("叫声是嘎——嘎——嘎");
}
public void desAppearance() {
System.out.println("外形是黄白相间,嘴长");
}
//鸭子的其他行为,如游泳
public void desBehavior(){
System.out.println("会游泳");
}
}

4只正宗的小鸭子形象已经清晰地定义出来了。鸭妈妈还有一个孩子,就是另类的丑小 鸭,她实际是一只白天鹅。我们先定义出白天鹅,如代码清单31-15所示。

代码清单31-15 白天鹅

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class WhiteSwan implements Swan {
//白天鹅的叫声
public void cry() {
System.out.println("叫声是克噜——克噜——克噜");
}
//白天鹅的外形
public void desAppaearance() {
System.out.println("外形是纯白色,惹人喜爱");
}
//天鹅是能够飞行的
public void fly() {
System.out.println("能够飞行");
}
}

但是,鸭妈妈却不认为自己这个另类的孩子是白天鹅,它从自己的观点出发,认为她很丑陋,有碍自己的脸面,于是驱赶她——鸭妈妈把这只小天鹅误认为一只鸭。我们来看实现,如代码清单31-16所示。

代码清单31-16 把白天鹅当做小鸭子看待

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class UglyDuckling extends WhiteSwan implements Duck {
//丑小鸭的叫声
public void cry() {
super.cry();
}
//丑小鸭的外形
public void desAppearance() {
super.desAppaearance();
}
//丑小鸭的其他行为
public void desBehavior(){
//丑小鸭不仅会游泳
System.out.println("会游泳");
//还会飞行
super.fly();
}
}

天鹅被看成了鸭子,有点暴殄天物的感觉。我们再来创建一个场景类来描述这一场景, 如代码清单31-17所示。

代码清单31-17 场景类

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) {
//鸭妈妈有5个孩子,其中4个都是一个模样
System.out.println("===妈妈有五个孩子,其中四个模样是这样的:===");
Duck duck = new Duckling();
duck.cry();
//小鸭子的叫声
duck.desAppearance();
//小鸭子的外形
duck.desBehavior();
//小鸭子的其他行为
System.out.println("\n===一只独特的小鸭子,模样是这样的:===");
Duck uglyDuckling = new UglyDuckling();
uglyDuckling.cry();
//丑小鸭的叫声
uglyDuckling.desAppearance();
//丑小鸭的外形
uglyDuckling.desBehavior();
//丑小鸭的其他行为
}
}

运行结果如下所示:

1
2
3
4
5
6
7
8
9
===妈妈有5个孩子,其中4个模样是这样的:=== 
叫声是嘎——嘎——嘎
外形是黄白相间,嘴长
会游泳
===一只独特的小鸭子,模样是这样的:===
叫声是克噜——克噜——克噜
外形是纯白色,惹人喜爱
会游泳
能够飞行

可怜的小天鹅被认为是一只丑陋的小鸭子,造物弄人呀!采用适配器模式讲述丑小鸭的故事,我们首先观察到的是鸭与天鹅的不同点,建立了不同的接口以实现不同的物种,然后在需要的时候(根据故事情节)把一个物种伪装成另外一个物种,实现不同物种的相同处理过程,这就是适配器模式的设计意图。

31.2.3 最佳实践

我们用两个模式实现了丑小鸭的美丽蜕变。我们发现:这两个模式有较多的不同点。

  • 意图不同

装饰模式的意图是加强对象的功能,例子中就是把一个怯弱的小天鹅强化成了一个美丽、自信的白天鹅,它不改变类的行为和属性,只是增加(当然了,减弱类的功能也是可能存在的)功能,使美丽更加美丽,强壮更加强壮,安全更加安全;而适配器模式关注的则是转化,它的主要意图是两个不同对象之间的转化,它可以把一个天鹅转化为一个小鸭子看待,也可以把一只小鸭子看成是一只天鹅(那估计要在小鸭子的背上装个螺旋桨了),它关注转换。

  • 施与对象不同

装饰模式装饰的对象必须是自己的同宗,也就是相同的接口或父类,只要在具有相同的属性和行为的情况下,才能比较行为是增加还是减弱;适配器模式则必须是两个不同的对象,因为它着重于转换,只有两个不同的对象才有转换的必要,如果是相同对象还转换什么?!

  • 场景不同

装饰模式在任何时候都可以使用,只要是想增强类的功能,而适配器模式则是一个补救模式,一般出现在系统成熟或已经构建完毕的项目中,作为一个紧急处理手段采用。

  • 扩展性不同

装饰模式很容易扩展!今天不用这个修饰,好,去掉;明天想再使用,好,加上。这都没有问题。而且装饰类可以继续扩展下去;但是适配器模式就不同了,它在两个不同对象之间架起了一座沟通的桥梁,建立容易,去掉就比较困难了,需要从系统整体考虑是否能够撤销。

32.2 策略模式VS状态模式

在行为类设计模式中,状态模式和策略模式是亲兄弟,两者非常相似,我们先看看两者的通用类图,把两者放在一起比较一下,如图32-3所示。

image-20210930171106992

图32-3 策略模式(左)和状态模式(右)的通用类图

两个类图非常相似,都是通过Context类封装一个具体的行为,都提供了一个封装的方法,是高扩展性的设计模式。但根据两者的定义,我们发现两者的区别还是很明显的:策略模式封装的是不同的算法,算法之间没有交互,以达到算法可以自由切换的目的;而状态模式封装的是不同的状态,以达到状态切换行为随之发生改变的目的。这两种模式虽然都有变换的行为,但是两者的目标却是不同的。我们举例来说明两者的不同点。

人只要生下来就有工作可做,人在孩童时期的主要工作就是玩耍(学习只是在人类具有了精神意识行为后才产生的);成人时期的主要工作是养活自己,然后为社会做贡献;老年时期的主要工作就是享受天伦之乐。按照策略模式来分析,这三种不同的工作方式就是三个不同的具体算法,随着时光的推移工作内容随之更替,这和对一堆数组的冒泡排序、快速排序、插入排序一样,都是一系列的算法;而按照状态模式进行设计,则认为人的状态(孩童、成人、老人)产生了不同的行为结果,这里的行为都相同,都是工作,但是它们的实现方式确实不同,也就是产生的结果不同,看起来就像是类改变了。

32.2.1 策略模式实现人生

下面按照策略模式进行设计,先来看类图,如图32-4所示。

这是非常典型的策略模式,没有太多的玄机,它定义了一个工作算法,然后有三个实现类:孩童工作、成年人工作和老年人工作。我们来看代码,首先看抽象工作算法,如代码清单32-19所示。

image-20210930171228691

图32-4 策略模式实现人生的类图

代码清单32-19 抽象工作算法

1
2
3
4
public abstract class WorkAlgorithm {
//每个年龄段都必须完成的工作
public abstract void work();
}

无论如何,每个算法都必须实现work方法,完成对工作内容的定义,三个具体的工作算法如代码清单32-20、32-21、32-22所示。

代码清单32-20 孩童工作

1
2
3
4
5
6
7
public class ChildWork extends WorkAlgorithm {
//小孩的工作
@Override
public void work() {
System.out.println("儿童的工作是玩耍!");
}
}

代码清单32-21 成年人工作

1
2
3
4
5
6
7
public class AdultWork extends WorkAlgorithm {
//成年人的工作
@Override
public void work() {
System.out.println("成年人的工作就是先养活自己,然后为社会做贡献!");
}
}

代码清单32-22 老年人工作

1
2
3
4
5
6
7
public class OldWork extends WorkAlgorithm {
//老年人的工作
@Override
public void work() {
System.out.println("老年人的工作就是享受天伦之乐!");
}
}

我们再来看环境角色,如代码清单32-23所示。

代码清单32-23 环境角色

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Context {
private WorkAlgorithm workMethod;
public WorkAlgorithm getWork() {
return workMethod;
}
public void setWork(WorkAlgorithm work) {
this.workMethod = work;
}
//每个算法都有必须具有的功能
public void work(){
workMethod.work();
}
}

我们编写一个场景类来模拟该场景,如代码清单32-24所示。

代码清单32-24 场景类

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) {
//定义一个环境角色
Context context=new Context();
System.out.println("====儿童的主要工作=====");
context.setWork(new ChildWork());
context.work();

System.out.println("\n====成年人的主要工作=====");
context.setWork(new AdultWork());
context.work();
System.out.println("\n====老年人的主要工作=====");
context.setWork(new OldWork());
context.work();
}
}

在这里我们把每个不同的工作内容作为不同的算法,分别是孩童工作、成年人工作、老年人工作算法,然后在场景类中根据不同的年龄段匹配不同的工作内容,其运行结果如下所示:

1
2
3
4
5
6
====儿童的主要工作===== 
儿童的工作是玩耍!
====成年人的主要工作=====
成年人的工作就是先养活自己,然后为社会做贡献!
====老年人的主要工作=====
老年人的工作就是享受天伦之乐!

通过采用策略模式我们实现了“工作”这个策略的三种不同算法,算法可以自由切换,到底用哪个算法由调用者(高层模块)决定。策略模式的使用重点是算法的自由切换——老的算法退休,新的算法上台,对模块的整体功能没有非常大的改变,非常灵活。而如果想要增加一个新的算法,比如未出生婴儿的工作,只要继承WorkAlgorithm就可以了。

32.2.2 状态模式实现人生

我们再来看看使用状态模式是如何实现该需求的。随着时间的变化,人的状态变化了, 同时引起了人的工作行为改变,完全符合状态模式。我们来看类图,如图32-5所示。

image-20210930171554473

图32-5 状态模式实现人生的类图
这与策略模式非常相似,基本上就是几个类名称的修改而已,但是其中蕴藏的玄机就大了,看看代码你就会明白。我们先来看抽象状态类,如代码清单32-25所示。

代码清单32-25 人的抽象状态

1
2
3
4
5
6
7
8
9
10
public abstract class HumanState {
//指向一个具体的人
protected Human human;
//设置一个具体的人
public void setHuman(Human _human){
this.human = _human;
}
//不管人是什么状态都要工作
public abstract void work();
}

抽象状态定义了一个具体的人(human)必须进行工作(work),但是一个人在哪些状态下完成哪些工作则是由子类来实现的。我们先来看孩童状态,如代码清单32-26所示。

代码清单32-26 孩童状态

1
2
3
4
5
6
7
public class ChildState extends HumanState{
//儿童的工作就是玩耍
public void work(){
System.out.println("儿童的工作是玩耍!");
super.human.setState(Human.ADULT_STATE);
}
}

ChildState类代表孩童状态,在该状态下的工作就是玩耍。读者看着可能有点惊奇,在 work方法中为什么要设置下一个状态?因为我们的状态变化都是单方向的,从孩童到成年 人,然后到老年人,每个状态转换到其他状态只有一个方向,因此会在这里看到work有两个 职责:完成工作逻辑和定义下一状态。

我们再来看成年人状态和老年人状态,分别如代码清单32-27、32-28所示。

代码清单32-27 成年人状态

1
2
3
4
5
6
7
8
public class AdultState extends HumanState {
//成年人的工作就是先养活自己,然后为社会做贡献
@Override
public void work() {
System.out.println("成年人的工作就是先养活自己,然后为社会做贡献!");
super.human.setState(Human.OLD_STATE);
}
}

代码清单32-28 老年人状态

1
2
3
4
5
6
7
public class OldState extends HumanState {
//老年人的工作就是享受天伦之乐
@Override
public void work() {
System.out.println("老年人的工作就是享受天伦之乐!");
}
}

每一个HumanState的子类都代表了一种状态,虽然实现的方法名work都相同,但是实现的内容却不同,也就是在不同的状态下行为随之改变。我们来看环境角色是如何处理行为随状态的改变而改变的,如代码清单32-29所示。

代码清单32-29 环境角色

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Human {
//定义人类都具备哪些状态
public static final HumanState CHIILD_STATE = new ChildState();
public static final HumanState ADULT_STATE = new AdultState();
public static final HumanState OLD_STATE = new OldState();
//定义一个人的状态
private HumanState state;
//设置一个状态
public void setState(HumanState _state){
this.state = _state;
this.state.setHuman(this);
}
//人类的工作
public void work(){
this.state.work();
}
}

定义一个Human类代表人类,也就是状态模式中的环境角色,每个人都会经历从孩童到成年人再到老年人这样一个状态过渡(当然了,老顽童周伯通的情况我们就没有考虑进来),随着状态的改变,行为也改变。我们来看场景类,如代码清单32-30所示。

代码清单32-30 场景类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Client {
public static void main(String[] args) {
//定义一个普通的人
Human human = new Human();
//设置一个人的初始状态
human.setState(new ChildState());
System.out.println("====儿童的主要工作=====");
human.work();
System.out.println("\n====成年人的主要工作=====");
human.work();
System.out.println("\n====老年人的主要工作=====");
human.work();
}
}

运行结果如下所示:

1
2
3
4
5
6
====儿童的主要工作===== 
儿童的工作是玩耍!
====成年人的主要工作=====
成年人的工作就是先养活自己,然后为社会做贡献!
====老年人的主要工作=====
老年人的工作就是享受天伦之乐!

运行结果与策略模式相同,但是两者的分析角度是大相径庭的。策略模式的实现是通过分析每个人的工作方式的不同而得出三个不同的算法逻辑,状态模式则是从人的生长规律来分析,每个状态对应了不同的行为,状态改变后行为也随之改变。从以上示例中我们也可以看出,对于相同的业务需求,有很多种实现方法,问题的重点是业务关注的是什么,是人的生长规律还是工作逻辑?找准了业务的焦点,才能选择一个好的设计模式。

32.2.3 小结

从例子中我们可以看出策略模式和状态模式确实非常相似,称之为亲兄弟亦不为过,但是这两者还是存在着非常大的差别,而且也是很容易区分的。

  • 环境角色的职责不同

两者都有一个叫做Context环境角色的类,但是两者的区别很大,策略模式的环境角色只是一个委托作用,负责算法的替换;而状态模式的环境角色不仅仅是委托行为,它还具有登记状态变化的功能,与具体的状态类协作,共同完成状态切换行为随之切换的任务。

  • 解决问题的重点不同

策略模式旨在解决内部算法如何改变的问题,也就是将内部算法的改变对外界的影响降低到最小,它保证的是算法可以自由地切换;而状态模式旨在解决内在状态的改变而引起行为改变的问题,它的出发点是事物的状态,封装状态而暴露行为,一个对象的状态改变,从外界来看就好像是行为改变。

  • 解决问题的方法不同

策略模式只是确保算法可以自由切换,但是什么时候用什么算法它决定不了;而状态模式对外暴露的是行为,状态的变化一般是由环境角色和具体状态共同完成的,也就是说状态模式封装了状态的变化而暴露了不同的行为或行为结果。

  • 应用场景不同

两者都能实现前面例子中的场景,但并不表示两者的应用场景相同,这只是为了更好地展示出两者的不同而设计的一个场景。我们来想一下策略模式和状态模式的使用场景有什么不同,策略模式只是一个算法的封装,可以是一个有意义的对象,也可以是一个无意义的逻辑片段,比如MD5加密算法,它是一个有意义的对象吗?不是,它只是我们数学上的一个公式的相关实现,它是一个算法,同时DES算法、RSA算法等都是具体的算法,也就是说它们都是一个抽象算法的具体实现类,从这点来看策略模式是一系列平行的、可相互替换的算法封装后的结果,这就限定了它的应用场景:算法必须是平行的,否则策略模式就封装了一堆垃圾,产生了“坏味道”。状态模式则要求有一系列状态发生变化的场景,它要求的是有状态且有行为的场景,也就是一个对象必须具有二维(状态和行为)描述才能采用状态模式,如果只有状态而没有行为,则状态的变化就失去了意义。

  • 复杂度不同

通常策略模式比较简单,这里的简单指的是结构简单,扩展比较容易,而且代码也容易阅读。当然,一个具体的算法也可以写得很复杂,只有具备很高深的数学、物理等知识的人才可以看懂,这也是允许的,我们只是说从设计模式的角度来分析,它是很容易被看懂的。 而状态模式则通常比较复杂,因为它要从两个角色看到一个对象状态和行为的改变,也就是说它封装的是变化,要知道变化是无穷尽的,因此相对来说状态模式通常都比较复杂,涉及面很多,虽然也很容易扩展,但是一般不会进行大规模的扩张和修正。

33.1 策略模式VS桥梁模式

这对冤家终于碰头了,策略模式与桥梁模式是如此相似,简直就是孪生兄弟,要把它们两个分开可不太容易。我们来看看两者的通用类图,如图33-1所示。

image-20210930184916250

图33-1 策略模式(左)和桥梁模式(右)通用类图
两者之间确实很相似。如果把策略模式的环境角色变更为一个抽象类加一个实现类,或者桥梁模式的抽象角色未实现,只有修正抽象化角色,想想看,这两个类图有什么地方不一样?完全一样!正是由于类似场景的存在才导致了两者在实际应用中经常混淆的情况发生, 我们来举例说明两者有何差别。

大家都知道邮件有两种格式:文本邮件(Text Mail)和超文本邮件(HTML MaiL),在文本邮件中只能有简单的文字信息,而在超文本邮件中可以有复杂文字(带有颜色、字体等属性)、图片、视频等,如果你使用Foxmail邮件客户端的话就应该有深刻体验,看到一份邮件,怎么没内容?原来是你忘记点击那个“HTML邮件”标签了。下面我们就来讲解如何发送这两种不同格式的邮件,研究一下这两种模式如何处理这样的场景。

33.1.1 策略模式实现邮件发送

使用策略模式发送邮件,我们认为这两种邮件是两种不同的封装格式,给定了发件人、 收件人、标题、内容的一封邮件,按照两种不同的格式分别进行封装,然后发送之。按照这样的分析,我们发现邮件的两种不同封装格式就是两种不同的算法,具体到策略模式就是两种不同策略,这样看已经很简单了,我们可以直接套用策略模式来实现。先看类图,如图33-2所示。

image-20210930200945323

图33-2 策略模式实现邮件发送的类图

我们定义了一个邮件模板,它有两个实现类:TextMail(文本邮件)和HtmlMail(超文本邮件),分别实现两种不同格式的邮件封装。MailServer是一个环境角色,它接收一个MailTemplate对象,然后通过sendMail方法发送出去。我们来看具体的代码,先看抽象邮件,如代码清单33-1所示。

代码清单33-1 抽象邮件

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
public abstract class MailTemplate {
//邮件发件人
private String from;
//收件人
private String to;
//邮件标题
private String subject;
//邮件内容
private String context;
//通过构造函数传递邮件信息
public MailTemplate(String _from,String _to,String _subject,String _context){
this.from = _from;
this.to = _to;
this.subject = _subject;
this.context = _context;
}
public String getFrom() {
return from;
}
public void setFrom(String from) {
this.from = from;
}
public String getTo() {
return to;
}
public void setTo(String to) {
this.to = to;
}
public String getSubject() {
return subject;
}
public void setSubject(String subject) {
this.subject = subject;
}
public void setContext(String context){
this.context = context;
}
//邮件都有内容
public String getContext(){
return context;
}
}

很奇怪,是吗?抽象类没有抽象的方法,设置为抽象类还有什么意义呢?有意义,在这里我们定义了一个这样的抽象类:它具有邮件的所有属性,但不是一个具体可以被实例化的对象。例如,你对邮件服务器说“给我制造一封邮件”,邮件服务器肯定拒绝,为什么?你要产生什么邮件?什么格式的?邮件对邮件服务器来说是一个抽象表示,是一个可描述但不可形象化的事物。你可以这样说:“我要一封标题为XX,发件人是XXX的文本格式的邮件”, 这就是一个可实例化的对象,因此我们的设计就产生了两个子类以具体化邮件,而且每种邮件格式对邮件的内容都有不同的处理。我们首先看文本邮件,如代码清单33-2所示。

代码清单33-2 文本邮件

1
2
3
4
5
6
7
8
9
10
11
12
13
public class TextMail extends MailTemplate {
public TextMail(String _from, String _to, String _subject, String _context) {
super(_from, _to, _subject, _context);
}
public String getContext() {
//文本类型设置邮件的格式为:text/plain
String context = "\nContent-Type: text/plain;
charset=GB2312\n" +super.getContext();
//同时对邮件进行base64编码处理,这里用一句话代替
context = context + "\n邮件格式为:文本格式";
return context;
}
}

我们覆写了getContext方法,因为要把一封邮件设置为文本邮件必须加上一个特殊的标志:text/plain,用于告诉解析这份邮件的客户端:“我是一封文本格式的邮件,别解析错了”。同样,超文本格式的邮件也有类似的设置,如代码清单33-3所示。

代码清单33-3 超文本邮件

1
2
3
4
5
6
7
8
9
10
11
12
13
public class HtmlMail extends MailTemplate {
public HtmlMail(String _from, String _to, String _subject, String _context) {
super(_from, _to, _subject, _context);
}
public String getContext(){
//超文本类型设置邮件的格式为:multipart/mixed
String context = "\nContent-Type: multipart/mixed;
charset= GB2312\n" +super.getContext();
//同时对邮件进行HTML检查,是否有类似未关闭的标签
context = context + "\n邮件格式为:超文本格式";
return context;
}
}

优秀一点的邮件客户端会对邮件的格式进行检查,比如编写一封超文本格式的邮件,在内容中加上了标签,但是遗忘了结尾标签,邮件的产生者(也就是邮件的客户端)会提示进行修正,我们这里用了“邮件格式为:超文本格式”来代表该逻辑。

两个实现类实现了不同的算法,给定相同的发件人、收件人、标题和内容可以产生不同的邮件信息。我们看看邮件是如何发送出去的,如代码清单33-4所示。

代码清单33-4 邮件服务器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class MailServer {
//发送的是哪封邮件
private MailTemplate m;
public MailServer(MailTemplate _m){
this.m = _m;
}
//发送邮件
public void sendMail(){
System.out.println("====正在发送的邮件信息====");
//发件人
System.out.println("发件人:" + m.getFrom());
//收件人
System.out.println("收件人:" + m.getTo());
//标题
System.out.println("邮件标题:" + m.getSubject());
//邮件内容
System.out.println("邮件内容:" + m.getContext());
}
}

很简单,邮件服务器接收了一封邮件,然后调用自己的发送程序进行发送。可能读者要问了,为什么不把sendMail方法移植到邮件模板类中呢?这也是邮件模板类的一个行为,邮件可以被发送。是的,这确实是邮件的一个行为,完全可以这样做,两者没有什么区别,只是从不同的角度看待该方法而已。我们继续看场景类,如代码清单33-5所示。

代码清单33-5 场景类

1
2
3
4
5
6
7
8
9
10
public class Client {
public static void main(String[] args) {
//创建一封TEXT格式的邮件
MailTemplate m = new HtmlMail("a@a.com","b@b.com","外星人攻击地球了","结局是外星人被地球人打败了!");
//创建一个Mail发送程序
MailServer mail = new MailServer(m);
//发送邮件
mail.sendMail();
}
}

运行结果如下所示:

1
2
3
4
5
6
7
8
====正在发送的邮件信息==== 
发件人:a@a.com
收件人:b@b.com
邮件标题:外星人攻击地球了
邮件内容:
Content-Type: multipart/mixed;charset=GB2312
结局是外星人被地球人打败了!
邮件格式为:超文本格式

当然,如果想产生一封文本格式的邮件,只要稍稍修改一下场景类就可以了:new HtmlMail修改为new TextMail,读者可以自行实现,非常简单。在该场景中,我们使用策略模式实现两种算法的自由切换,它提供了这样的保证:封装邮件的两种行为是可选择的,至于选择哪个算法是由上层模块决定的。策略模式要完成的任务就是提供两种可以替换的算法。

33.1.2 桥梁模式实现邮件发送

桥梁模式关注的是抽象和实现的分离,它是结构型模式,结构型模式研究的是如何建立一个软件架构,下面我们就来看看桥梁模式是如何构件一套发送邮件的架构的,如图33-3所示。

类图中我们增加了SendMail和Postfix两个邮件服务器来实现类,在邮件模板中允许增加发送者标记,其他与策略模式都相同。我们在这里已经完成了一个独立的架构,邮件有了, 发送邮件的服务器也具备了,是一个完整的邮件发送程序。需要读者注意的是,SendMail类不是一个动词行为(发送邮件),它指的是一款开源邮件服务器产品,一般*nix系统的默认邮件服务器就是SendMail;Postfix也是一款开源的邮件服务器产品,其性能、稳定性都在逐步赶超SendMail。

image-20210930202528940

图33-3 桥梁模式实现邮件发送的类图

我们来看代码实现,邮件模板仅仅增加了一个add方法,如代码清单33-6所示。

代码清单33-6 邮件模板

1
2
3
4
5
6
7
8
9
public abstract class MailTemplate {
/*
*该部分代码不变,请参考代码清单33-1
*/
//允许增加邮件发送标志
public void add(String sendInfo){
context = sendInfo + context;
}
}

文本邮件、超文本邮件都没有任何改变,如代码清单33-2、33-3所示,这里不再赘述。

我们来看邮件服务器,也就是桥梁模式的抽象化角色,如代码清单33-7所示。

代码清单33-7 邮件服务器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public abstract class MailServer {
//发送的是哪封邮件
protected final MailTemplate m;
public MailServer(MailTemplate _m){
this.m = _m;
}
//发送邮件
public void sendMail(){
System.out.println("====正在发送的邮件信息====");
//发件人
System.out.println("发件人:" + m.getFrom());
//收件人
System.out.println("收件人:" + m.getTo());
//标题
System.out.println("邮件标题:" + m.getSubject());
//邮件内容
System.out.println("邮件内容:" + m.getContext());
}
}

该类相对于策略模式的环境角色有两个改变:

  • 修改为抽象类。为什么要修改成抽象类?因为我们在设计一个架构,邮件服务器是一 个具体的、可实例化的对象吗?“给我一台邮件服务器”能实现吗?不能,只能说“给我一台 Postfix邮件服务器”,这才能实现,必须有一个明确的可指向对象。
  • 变量m修改为Protected访问权限,方便子类调用。

我们再来看看Postfix邮件服务器的实现,如代码清单33-8所示。

代码清单33-8 Postfix邮件服务器

1
2
3
4
5
6
7
8
9
10
11
12
public class Postfix extends MailServer {
public Postfix(MailTemplate _m) {
super(_m);
}
//修正邮件发送程序
public void sendMail(){
//增加邮件服务器信息
String context ="Received: from XXXX (unknown [xxx.xxx.xxx.xxx]) by aaa.aaa.com (Postfix) with ESMTP id 8DBCD172B8\n" ;
super.m.add(context);
super.sendMail();
}
}

为什么要覆写sendMail程序呢?这是因为每个邮件服务器在发送邮件时都会在邮件内容上留下自己的标志,一是广告作用,二是为了互联网上统计需要,三是方便同质软件的共振。我们再来看SendMail邮件服务器的实现,如代码清单33-9所示。

代码清单33-9 SendMail邮件服务器

1
2
3
4
5
6
7
8
9
10
11
12
13
public class SendMail extends MailServer {
//传递一封邮件
public SendMail(MailTemplate _m) {
super(_m);
}
//修正邮件发送程序
@Override
public void sendMail(){
//增加邮件服务器信息
super.m.add("Received: (sendmail);7 Nov 2009 04:14:44 +0100");
super.sendMail();
}
}

邮件和邮件服务器都有了,我们来看怎么发送邮件,如代码清单33-10所示。

代码清单33-10 场景类

1
2
3
4
5
6
7
8
9
10
public class Client {
public static void main(String[] args) {
//创建一封TEXT格式的邮件
MailTemplate m = new HtmlMail("a@a.com","b@b.com","外星人攻击地球了"," 结局是外星人被地球人打败了!");
//使用Postfix发送邮件
MailServer mail = new Postfix(m);
//发送邮件
mail.sendMail();
}
}

运行结果如下所示:

1
2
3
4
5
6
7
8
====正在发送的邮件信息==== 
发件人:a@a.com
收件人:b@b.com
邮件标题:外星人攻击地球了
邮件内容: Content-Type: multipart/mixed;charset=GB2312
Received: from XXXX (unknown [xxx.xxx.xxx.xxx]) by aaa.aaa.com (Postfix) with ESMTP id 8DBCD172B8
结局是外星人被地球人打败了!
邮件格式为:超文本格式

当然了,还有其他三种发送邮件的方式:Postfix发送文本邮件以及SendMail发送文本邮件和超文本邮件。修改量很小,读者可以自行修改实现,体会一下桥梁模式的特点。

33.1.3 最佳实践

策略模式和桥梁模式是如此相似,我们只能从它们的意图上来分析。策略模式是一个行为模式,旨在封装一系列的行为,在例子中我们认为把邮件的必要信息(发件人、收件人、 标题、内容)封装成一个对象就是一个行为,封装的格式(算法)不同,行为也就不同。而桥梁模式则是解决在不破坏封装的情况下如何抽取出它的抽象部分和实现部分,它的前提是不破坏封装,让抽象部分和实现部分都可以独立地变化,在例子中,我们的邮件服务器和邮件模板是不是都可以独立地变化?不管是邮件服务器还是邮件模板,只要继承了抽象类就可以继续扩展,它的主旨是建立一个不破坏封装性的可扩展架构。

简单来说,策略模式是使用继承和多态建立一套可以自由切换算法的模式,桥梁模式是在不破坏封装的前提下解决抽象和实现都可以独立扩展的模式。桥梁模式必然有两个“桥墩”——抽象化角色和实现化角色,只要桥墩搭建好,桥就有了,而策略模式只有一个抽象角色,可以没有实现,也可以有很多实现。

还是很难区分,是吧?多想想两者的意图,就可以理解为什么要建立两个相似的模式了。我们在做系统设计时,可以不考虑到底使用的是策略模式还是桥梁模式,只要好用,能够解决问题就成,“不管黑猫白猫,抓住老鼠的就是好猫”。

33.2 门面模式VS中介者模式

门面模式为复杂的子系统提供一个统一的访问界面,它定义的是一个高层接口,该接口使得子系统更加容易使用,避免外部模块深入到子系统内部而产生与子系统内部细节耦合的问题。中介者模式使用一个中介对象来封装一系列同事对象的交互行为,它使各对象之间不再显式地引用,从而使其耦合松散,建立一个可扩展的应用架构。

33.2.1 中介者模式实现工资计算

大家工作会得到工资,那么工资与哪些因素有关呢?这里假设工资与职位、税收有关, 职位提升工资就会增加,同时税收也增加,职位下降了工资也同步降低,当然税收也降低。 而如果税收比率增加了呢?工资自然就减少了!这三者之间两两都有关系,很适合中介者模式的场景,类图如图33-4所示。

image-20210930203153669

图33-4 工资、职位、税收的示意类图

类图中的方法比较简单,我们主要分析的是三者之间的关系,通过类图可以发现三者之间已经没有耦合,原本在需求分析时我们发现三者有直接的交互,采用中介者模式后,三个对象之间已经相互独立了,全部委托中介者完成。我们在类图中还定义了一个抽象同事类, 它是一个标志性接口,其子类都是同事类,都可以被中介者接收,如代码清单33-11所示。

代码清单33-11 抽象同事类

1
2
3
4
5
6
7
public abstract class AbsColleague {
//每个同事类都对中介者非常了解
protected AbsMediator mediator;
public AbsColleague(AbsMediator _mediator){
this.mediator = _mediator;
}
}

在抽象同事类中定义了每个同事类对中介者都非常了解,如此才能把请求委托给中介者完成。三个同事类都具有相同的设计,即定义一个业务接口以及每个对象必须实现的职责, 同时既然是同事类就都继承AbsColleague。抽象同事类只是一个标志性父类,并没有限制子类的业务逻辑,因此每一个同事类并没有违背单一职责原则。首先来看职位接口,如代码清单33-12所示。

代码清单33-12 职位接口

1
2
3
4
5
6
public interface IPosition {
//升职
public void promote();
//降职
public void demote();
}

职位会有升有降,职位变化如代码清单33-13所示。

代码清单33-13 职位

1
2
3
4
5
6
7
8
9
10
11
public class Position extends AbsColleague implements IPosition {
public Position(AbsMediator _mediator){
super(_mediator);
}
public void demote() {
super.mediator.down(this);
}
public void promote() {
super.mediator.up(this);
}
}

每一个职位的升降动作都委托给中介者执行,具体一个职位升降影响到谁这里没有定义,完全由中介者完成,简单而且扩展性非常好。下面我们来看工资接口,如代码清单33- 14所示。

代码清单33-14 工资接口

1
2
3
4
5
6
public interface ISalary {
//加薪
public void increaseSalary();
//降薪
public void decreaseSalary();
}

工资也会有升有降,如代码清单33-15所示。

代码清单33-15 工资

1
2
3
4
5
6
7
8
9
10
11
public class Salary extends AbsColleague implements ISalary {
public Salary(AbsMediator _mediator){
super(_mediator);
}
public void decreaseSalary() {
super.mediator.down(this);
}
public void increaseSalary() {
super.mediator.up(this);
}
}

交税是公民的义务,税收接口如代码清单33-16所示。

代码清单33-16 税收接口

1
2
3
4
5
6
public interface ITax {
//税收上升
public void raise();
//税收下降
public void drop();
}

税收的变化对我们的工资当然有影响,如代码清单33-17所示。

代码清单33-17 税收

1
2
3
4
5
6
7
8
9
10
11
12
public class Tax extends AbsColleague implements ITax {
//注入中介者
public Tax(AbsMediator _mediator){
super(_mediator);
}
public void drop() {
super.mediator.down(this);
}
public void raise() {
super.mediator.up(this);
}
}

以上同事类的业务都委托给了中介者,其本类已经没有任何的逻辑了,非常简单,现在的问题是中介者类非常复杂,因为它要处理三者之间的关系。我们首先来看抽象中介者,如代码清单33-18所示。

代码清单33-18 抽象中介者

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 abstract class AbsMediator {
//工资
protected final ISalary salary;
//职位
protected final IPosition position;
//税收
protected final ITax tax;
public AbsMediator(){
salary = new Salary(this);
position = new Position(this);
tax = new Tax(this);
}
//工资增加了
public abstract void up(ISalary _salary);
//职位提升了
public abstract void up(IPosition _position);
//税收增加了
public abstract void up(ITax _tax);
//工资降低了
public abstract void down(ISalary _salary);
//职位降低了
public abstract void down(IPosition _position);
//税收降低了
public abstract void down(ITax _tax);
}

在抽象中介者中我们定义了6个方法,分别处理职位升降、工资升降以及税收升降的业务逻辑,采用Java多态机制来实现,我们来看实现类,如代码清单33-19所示。

代码清单33-19 中介者

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 Mediator extends AbsMediator{
//工资增加了
public void up(ISalary _salary) {
upSalary();
upTax();
}
//职位提升了
public void up(IPosition position) {
upPosition();
upSalary();
upTax();

}
//税收增加了
public void up(ITax tax) {
upTax();
downSalary();
}
/**工资、职位、税收降低的处理方法相同,不再赘述 *///工资增加
private void upSalary(){
System.out.println("工资翻倍,乐翻天");
}
private void upTax(){
System.out.println("税收上升,为国家做贡献");
}
private void upPosition(){
System.out.println("职位上升一级,狂喜");
}
private void downSalary(){
System.out.println("经济不景气,降低工资");
}
private void downTax(){
System.out.println("税收减低,国家收入减少");
}
private void downPostion(){
System.out.println("官降三级,比自杀还痛苦");
}
}

该类的方法较多,但是还是非常简单的,它的12个方法分为两大类型:一类是每个业务的独立流程,比如增加工资,仅仅实现单独增加工资的职能,而不关心职位、税收是如何变化的,该类型的方法是private私有类型,只能提供本类内访问;另一类是实现抽象中介者定义的方法,完成具体的每一个逻辑,比如职位上升,同时也引起了工资增加、税收增加。我们编写一个场景类,看看运行结果,如代码清单33-20所示。

代码清单33-20 场景类

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Client {
public static void main(String[] args) {
//定义中介者
Mediator mediator = new Mediator();
//定义各个同事类
IPosition position = new Position(mediator);
ISalary salary = new Salary(mediator);
ITax tax = new Tax(mediator);
//职位提升了
System.out.println("===职位提升===");
position.promote();
}
}

运行结果如下所示:

1
2
3
4
===职位提升=== 
职位上升一级,狂喜
工资翻倍,乐翻天
税收上升,为国家做贡献

我们回过头来分析一下设计,在接收到需求后我们发现职位、工资、税收之间有着紧密的耦合关系,如果不采用中介者模式,则每个对象都要与其他两个对象进行通信,这势必会增加系统的复杂性,同时也使系统处于僵化状态,很难实现拥抱变化的理想。通过增加一个中介者,每个同事类的职位、工资、税收都只与中介者通信,中介者封装了各个同事类之间的逻辑关系,方便系统的扩展和维护。

33.2.2 门面模式实现工资计算

工资计算是一件非常复杂的事情,简单来说,它是对基本工资、月奖金、岗位津贴、绩效、考勤、税收、福利等因素综合运算后的一个数字。即使设计一个HR(人力资源)系统,员工工资计算也是非常复杂的模块,但是对于外界,比如高管层,最希望看到的结果是张三拿了多少钱,李四拿了多少钱,而不是看中间的计算过程,怎么计算那是人事部门的事情。换句话说,对外界的访问者来说,它只要传递进去一个人员名称和月份即可获得工资数,而不用关心其中的计算有多么复杂,这就用得上门面模式了。

门面模式对子系统起封装作用,它可以提供一个统一的对外服务接口,如图33-5所示。

image-20210930203813115

图33-5 HR系统的类图

该类图主要实现了工资计算,通过HRFacade门面可以查询用户的工资以及出勤天数等,而不用关心这个工资或者出勤天数是怎么计算出来的,从而屏蔽了外系统对工资计算模块的内部细节依赖。我们先看子系统内部的各个实现,考勤情况如代码清单33-21所示。

代码清单33-21 考勤情况

1
2
3
4
5
6
public class Attendance {
//得到出勤天数
public int getWorkDays(){
return (new Random()).nextInt(30);
}
}

非常简单,只用一个方法获得一个员工的出勤天数。我们再来看奖金计算,如代码清单33-22所示。

代码清单33-22 奖金计算

1
2
3
4
5
6
7
8
9
10
11
12
public class Bonus {
//考勤情况
private Attendance atte = new Attendance();
//奖金
public int getBonus(){
//获得出勤情况
int workDays = atte.getWorkDays();
//奖金计算模型
int bonus = workDays * 1800 / 30;
return bonus;
}
}

我们在这里实现了一个示意方法,实际的奖金计算是非常复杂的,与考勤、绩效、基本工资、岗位都有关系,单单一个奖金计算就可以设计出一个门面。我们再来看基本工资,这个基本上是按照职位而定的,比较固定,如代码清单33-23所示。

代码清单33-23 基本工资

1
2
3
4
5
6
public class BasicSalary {
//获得一个人的基本工资
public int getBasicSalary(){
return 2000;
}
}

我们定义了员工的基本工资都为2000元,没有任何浮动的余地。再来看绩效,如代码清单33-24所示。

代码清单33-24 绩效

1
2
3
4
5
6
7
8
9
10
public class Performance {
//基本工资
private BasicSalary salary = new BasicSalary();
//绩效奖励
public int getPerformanceValue(){
//随机绩效
int perf = (new Random()).nextInt(100);
return salary.getBasicSalary() * perf /100;
}
}

绩效按照一个非常简单的算法,即基本工资乘以一个随机的百分比。我们再来看税收,如代码清单33-25所示。

代码清单33-25 税收

1
2
3
4
5
6
7
public class Tax {
//收取多少税金
public int getTax(){
//交纳一个随机数量的税金
return (new Random()).nextInt(300);
}
}

一个计算员工薪酬的所有子元素都已经具备了,剩下的就是编写组合逻辑类,总工资的计算如代码清单33-26所示。

代码清单33-26 总工资计算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class SalaryProvider {
//基本工资
private BasicSalary basicSalary = new BasicSalary();
//奖金
private Bonus bonus = new Bonus();
//绩效
private Performance perf = new Performance();
//税收
private Tax tax = new Tax();
//获得用户的总收入
public int totalSalary(){
return basicSalary.getBasicSalary() + bonus.getBonus() + perf.getPerformanceValue() - tax.getTax();
}
}

这里只是对前面的元素值做了一个加减法计算,这是对实际HR系统的简化处理,如果把这个类暴露给外系统,那么被修改的风险是非常大的,因为它的方法totalSalary是一个具体的业务逻辑。我们采用门面模式的目的是要求门面是无逻辑的,与业务无关,只是一个子系统的访问入口。门面模式只是一个技术层次上的实现,全部业务还是在子系统内实现。我们来看HR门面,如代码清单33-27所示。

代码清单33-27 HR门面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class HRFacade {
//总工资情况
private SalaryProvider salaryProvider = new SalaryProvider();
//考勤情况
private Attendance attendance = new Attendance();
//查询一个人的总收入
public int querySalary(String name,Date date){
return salaryProvider.totalSalary();
}
//查询一个员工一个月工作了多少天
public int queryWorkDays(String name){
return attendance.getWorkDays();
}
}

所有的行为都是委托行为,由具体的子系统实现,门面只是提供了一个统一访问的基础而已,不做任何的校验、判断、异常等处理。我们编写一个场景类查看运行结果,如代码清单33-28所示。

代码清单33-28 场景类

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Client {
public static void main(String[] args) {
//定义门面
HRFacade facade = new HRFacade();
System.out.println("===外系统查询总收入===");
int salary = facade.querySalary("张三",new Date(System. currentTimeMillis()));
System.out.println( "张三 11月 总收入为:" +salary);
//再查询出勤天数
System.out.println("\n===外系统查询出勤天数===");
int workDays = facade.queryWorkDays("李四");
System.out.println("李四 本月出勤:" +workDays);
}
}

运行结果如下所示:

1
2
3
4
===外系统查询总收入=== 
张三 11月 总收入为:4133
===外系统查询出勤天数===
李四 本月出勤:22

在该例中,我们使用了门面模式对薪水计算子系统进行封装,避免子系统内部复杂逻辑外泄,确保子系统的业务逻辑的单纯性,即使业务流程需要变更,影响的也是子系统内部功能,比如奖金需要与基本工资挂钩,这样的修改对外系统来说是透明的,只需要子系统内部变更即可。

33.2.3 最佳实践

门面模式和中介者模式之间的区别还是比较明显的,门面模式是以封装和隔离为主要任务,而中介者模式则是以调和同事类之间的关系为主,因为要调和,所以具有了部分的业务逻辑控制。两者的主要区别如下:

  • 功能区别

门面模式只是增加了一个门面,它对子系统来说没有增加任何的功能,子系统若脱离门 面模式是完全可以独立运行的。而中介者模式则增加了业务功能,它把各个同事类中的原有 耦合关系移植到了中介者,同事类不可能脱离中介者而独立存在,除非是想增加系统的复杂 性和降低扩展性。

  • 知晓状态不同

对门面模式来说,子系统不知道有门面存在,而对中介者来说,每个同事类都知道中介 者存在,因为要依靠中介者调和同事之间的关系,它们对中介者非常了解。

  • 封装程度不同

门面模式是一种简单的封装,所有的请求处理都委托给子系统完成,而中介者模式则需要有一个中心,由中心协调同事类完成,并且中心本身也完成部分业务,它属于更进一步的业务功能封装。

34.2 混编小结

在这里的例子中用到了以下模式。

  • 责任链模式

负责对命令的参数进行解析,而且所有的扩展都是增加链数量和节点,不涉及原有的代码变更。

  • 命令模式

负责命令的分发,把适当的命令分发到指定的链上。

  • 模板方法模式

在Command类以及子类中,buildChain方法是模板方法,只是没有基本方法而已;在责任链模式的CommandName类中,用了一个典型的模板方法handlerMessage,它调用了基本方法,基本方法由各个实现类实现,非常有利于扩展。

  • 迭代器模式

在for循环中我们多次用到类似for(Class c:classes)的结构,是谁来支撑该方法运行?当然是迭代器模式,只是JDK已经把它融入到了API中,更方便使用了。

可能读者已经注意到了,”ls-l-a”这样的组合选项还没有处理。确实没有处理,以下提供两个思路来处理。

  • 独立处理

“ls-l-a”等同于”ls-la”,也等同于”ls-al”命令,可以把”ls-la”中的选项”la”作为一个参数来 进行处理,扩展一个类就可以了。该方法的缺点是类膨胀得太大,但是简单。

  • 混合处理

修正命令族处理链,每个命令处理节点运行完毕后,继续由后续节点处理,最终由Command类组装结果,根据每个节点的处理结果,组合后生成完整的返回信息,如”ls-l-a”就应该是LS_L类与LS_A类两者返回值组装的结果,当然链上的节点返回值就要放在Collection 类型中了。

该框架还有一个名称,叫做命令链(Chain of Command)模式,具体来说就是命令模式作为责任链模式的排头兵,由命令模式分发具体的消息到责任链模式。对于该框架,读者可以继续扩展下去。当然,上面的程序还可以优化,优化的结果就是Command类缩为一个类, 通过CommandEnum配置文件类传递命令,这比较容易实现,读者可以自行设计。

35.2 混编小结

回顾一下我们在该案例中使用了几个模式。

  • 策略模式

负责对扣款策略进行封装,保证两个策略可以自由切换,而且日后增加扣款策略也非常简单容易。

  • 工厂方法模式

修正策略模式必须对外暴露具体策略的问题,由工厂方法模式直接产生一个具体策略对象,而其他模块则不需要依赖具体的策略。

  • 门面模式

负责对复杂的扣款系统进行封装,封装的结果就是避免高层模块深入子系统内部,同时提供系统的高内聚、低耦合的特性。

我们主要使用了这三个模式,它们的好处是灵活、稳定,我们可以设想一下可能有哪些业务变化。

  • 扣款策略变更

增加一个新扣款策略,三步就可以完成:实现IDeduction接口,增加StrategyMan配置项,扩展扣款策略的利用(也就是门面模式的getDeductionType方法,在实际项目中这里只需要增加数据库的配置项)。减少一个策略很简单,修改扣款策略的利用即可。变更一个扣款策略也很简单,扩展一个实现类口就可以了。

  • 变更扣款策略的利用规则

我们的系统不想大修改,还记得我们提出的状态模式吗?这个就是为策略的利用服务的,变更它就能满足要求。想把IC卡也纳入策略利用的规则也不复杂。其实这个变更还真发生了,系统投产后,业务提出考虑退休人员的情况,退休人员的IC卡与普通在职员工一样, 但是它的扣款不仅仅是根据交易编码,还要根据IC卡对象,系统的变更做法是增加一个扣款策略,同时扩展扣款利用策略,也就是数据库的配置项,在getDeductionType中新扩展了一个功能:根据IC卡号,确认是否是退休人员,是退休人员,则使用新的扣款策略,这是一个非常简单的扩展。

这就是一个mini版的金融交易系统,没啥复杂的,剩下的问题就是开始考虑系统的鲁棒性,这才是难点。

36.2 混编小结

该事件触发框架结构清晰,扩展性好,读者可以进行抽象化处理后应用于实际开发中。 我们回头看看在这个案例中使用了哪些设计模式。

  • 工厂方法模式

负责产生产品对象,方便产品的修改和扩展,并且实现了产品和工厂的紧耦合,避免产品随意被创建而无触发事件的情况发生。

  • 桥梁模式

在产品和事件两个对象的关系中我们使用了桥梁模式,如此设计后,两者都可以自由地扩展(前提是需要抽取抽象化)而不会破坏原有的封装。

  • 观察者模式

观察者模式解决了事件如何通知处理者的问题,而且观察者模式还有一个优点是可以有多个观察者,也就是我们的架构是可以有多层级、多分类的处理者。想重新扩展一个新类型 (新接口)的观察者?没有问题,扩展ProductEvent即可。

  • 中介者模式

事件有了,处理者也有了,这些都会发生变化,并且处理者之间也有耦合关系,中介者则可以完美地处理这些复杂的关系。

我们再来思考一下,如果我们要扩展这个框架,可能还会用到什么模式?首先是责任链模式,它可以帮助我们解决一个处理者处理多个事件的问题;其次是模板方法模式,处理者的启用、停用等,都可以通过模板方法模式来实现;再次是装饰模式,事件的包装、处理者功能的强化都会用到装饰模式。当然了,我们还可能用到其他的模式,只要能够很好地解决我们的困境,那就好好使用吧,这也是我们学习设计模式的目的。

37.2 最佳实践

本章我们粗略地讲解了一个MVC框架。一个MVC框架要考虑的外界环境因素太多了, 而且本身MVC框架也是一个轻量型的,就是希望我们编写的程序在没有Struts、Spring MVC 等框架的环境中不需要大规模的修改照样能够运行,所以编写一个框架不是一件容易的事情。幸运的是我们以学习模式为主,通过设计MVC框架来了解设计模式。我们来看看本章用到了哪些模式。

  • 工厂方法模式:通过工厂方法模式把所有的拦截器链实现出来,方便在系统初始化时 直接处理。
  • 单例模式:Action的默认配置都是单例模式,在一般的应用中单例已经足够了,在复 杂情况下可以使用享元模式提供应用性能,减少单例模式的性能隐患。
  • 责任链模式:建立拦截器链以及过滤器链,实现任务的链条化处理。
  • 迭代器模式:非常方便地遍历拦截器链内的拦截器,而不用再自己写遍历拦截器链的 方法。
  • 中介者模式:以核心控制器为核心,其他同事类都负责为核心控制器“打工”,保证核 心控制器瘦小、稳定。
  • 观察者模式:配置文件修改时,不用重启应用可以即刻生效,提供使用者的体验。
  • 桥梁模式:使不同的视图配合不同的语言文件,为终端用户展示不同的界面。
  • 策略模式:对XML文件的检查可以使用两种不同的策略,而且可以在测试机和开发机 中使用不同的检查策略,方便系统间自由切换。
  • 访问者模式:在解析XML文件时,使用访问者非常方便地访问到需要的对象。
  • 适配器模式:把一个开发者不熟悉的对象转换为熟悉的对象,避免工具或框架对开发 者的影响。
  • 门面模式:Action分发器负责所有的Action的分发工作,它提供了一个调用Action的唯 一入口,避免外部模块深入到模型模块内部。
  • 代理模式:大量使用动态代理,确保了框架的智能化。

MVC框架有非常成熟的源码,有兴趣的读者可以看看Struts、Spring MVC等源码,其中 包含了非常多的设计模式。读源码是提高设计技能和开发技能的一个重要途径,看一本书是 与作者进行了一次心灵交互,看一份源码是与一群作者进行心灵交互,对提高自己的技术修 养有非常大的帮助。

38.2 对象池模式

上周二,师兄过来找我,他负责运维一个大型新闻网站,说是网站出现性能,让我帮忙分析调优。我这几天正好闲得手痒,同时又卖个人情,何乐而不为呢。于是我们俩就到机房蹲点,追查问题。

38.2.1 正确的池化

简单说明一下该系统的场景,这是一个专业的新闻追踪网站,关注的是专业新闻的深度,在行业内具有相当大的影响力。最近一段时间内出现偶发性缓慢,从监控情况上看,响应时间在2秒以上,由于最近软硬件环境都没有变更过,因此直觉判断:最快捷、直观的解决方案就是增加DB硬件设备。但由于东家是穷惯了,不同意在没有彻查问题之前而依靠增强硬件来解决问题,于是我们这些软件工程师就忙活起来了。

网站首页内容基本都是静态的(轮询生成),唯一的动态部分是网站的激励语,比如“积一时之跬步,臻千里之遥程”、“业精于勤,荒于嬉;行成于思,毁于随”等励志语句, 这是一个简单的SQL随机查询结果,表中的数量在5000条左右,而且结构简单,查询性能不是问题。示例代码如代码清单38-29所示。

代码清单38-29 无缓存的SQL随机读取

1
2
3
4
5
6
7
8
@Service
public class WisdomProvider {
@Autowire
private WisdomDao wisdomDao;
public String getOneWord() {
return wisdomDao.randomOneWisdom();
}
}

对于代码中的@Service、@Autowire注解,做过Spring开发的都懂,这是一个典型的三层架构,WisdomDao的randomOneWisdom方法是通过数据库随机函数查询一条记录。在跟踪过程中,发现高峰期数据库连接偶尔出现占满情况,而且都是查询该表(顺便说下,该数据库的随机查询算法有缺陷),问题找到了:每一次访问都会直接查询数据库,没有缓存。通常情况下,这没有问题,但是在高并发的情况下,例如在10万PV的压力下服务器基本就垮掉了,这是非常严重的问题。

怎么解决呢?好办,引入一个对象池,把这5000条记录(根据评估最多不超过20000条记录)在启动时直接加载到内存中,在需要时再从内存中取得,以后查询不再与数据库交互。示例代码如代码清单38-30所示。

代码清单38-30 增加缓存后的随机读取

1
2
3
4
5
6
7
8
9
10
11
12
13
@Service
public class WisdomProvider {
@Autowire
private WisdomDao wisdomDao;
private List<String> wisdoms = null;
@PostConstruct
public void init() {
wisdoms = wisdomDao.getAll();
}
public String getOneWord() {
return RandomUtils.getOne(wisdoms);
}
}

@PostConstruct注解的作用是Spring容器在启动完毕后,直接执行init方法,一次性读取 所有的数据,然后在应用运行期间不再与数据库交互,直接从List列表中获取数据。通过这 样的修正,系统性能有了大幅提升,在不增加硬件的情况下,彻底解决了性能问题。这就是 对象池模式。

38.2.2 对象池模式的意图

对象池模式,或者称为对象池服务,其意图如下:
通过循环使用对象,减少资源在初始化和释放时的昂贵损耗[^1]。

注意这里的“昂贵”可能是时间效益(如性能),也可能是空间效益(如并行处理),在大多的情况下,“昂贵”指性能。

简单地说,在需要时,从池中提取;不用时,放回池中,等待下一个请求。典型例子是连接池和线程池,这是我们开发中经常接触到的。类图如图38-6所示。

image-20211001233332624

图38-6 对象池模式通用类图

对象池提供两个公共的方法:checkOut负责从池中提取对象,checkIn负责把回收对象 (当然,很多时候checkIn已经自动化处理,不需要显式声明,如连接池),对象池代码如代码清单38-31所示。

代码清单38-31 对象池示例代码

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 abstract class ObjectPool<T> {
//容器,容纳对象
private Hashtable<T, ObjectStatus> pool = new Hashtable<T, ObjectStatus>();
//初始化时创建对象,并放入到池中
public ObjectPool() {
pool.put(create(), new ObjectStatus());
}
//从Hashtable中取出空闲元素
public synchronized T checkOut() {
//这是最简单的策略
for (T t : pool.keySet()) {
if (pool.get(t).validate()) {
pool.get(t).setUsing();
return t;
}
}
return null;
}
//归还对象
public synchronized void checkIn(T t) {
pool.get(t).setFree();
}
class ObjectStatus {
//占用
public void setUsing() {
}
//释放
public void setFree() {
//注意:若T是有状态,则需要回归到初始化状态
}
//检查是否可用
public boolean validate() {
return false;
}
}
//创建池化对象
public abstract T create();
}

这是一个简单的对象池实现,在实际应用中还需要考虑池的最小值、最大值、池化对象状态(若有的话,需要重点考虑)、异常处理(如满池情况)等方面,特别是池化对象状态,若是有状态的业务对象则需要重点关注。

38.2.3 最佳实践

把对象池化的本意是期望一次性初始化所有对象,减少对象在初始化上的昂贵性能开销,从而提高系统整体性能。然而池化处理本身也要付出代价,因此,并非任何情况下都适合采用对象池化。

通常情况下,在重复生成对象的操作成为影响性能的关键因素时,才适合进行对象池化。但是若池化所能带来的性能提高并不显著或重要的话,建议放弃对象池化技术,以保持代码的简明,转而使用更好的硬件来提高性能为佳。

对象池技术在Java领域已经非常成熟,只要做过企业级开发的人员,基本都用过C3P0、 DBCP、Proxool等连接池,也配置过minPoolSize、maxPoolSize等参数,这是对象池模式的典型应用。在实际开发中若需要对象池,建议使用common-pool工具包来实现,简单、快捷、 高效。

[^1]: 原文是Avoid expensive acquisition and release of resources by recycling objects that are no longer in use。