31.2 装饰模式VS适配器模式

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 最佳实践

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

  • 意图不同

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

  • 施与对象不同

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

  • 场景不同

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

  • 扩展性不同

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