GoF设计模式(七):Adapter Pattern 适配器式

前面我们介绍了一种通过包装拓展功能的方法——装饰器模式,这里我们来了解另外一种使用包装思想的设计模式——Adapter Pattern适配器模式,其依然是结构型模式的一种。一般情况下,两个类如果接口不兼容是无法一起工作的。而如果通过Adapter Pattern适配器模式就可以很好的解决这个问题了

abstract.jpeg

现实世界的指引

现实生活中大家经常会遇到这样的一个麻烦,好不容易买了一个喜欢的台灯,结果回来一看,台灯是二脚插头而书桌旁边的插座却是三脚的。显然不能靠暴力直接插进去啊,这怎么办?有经验的小伙伴知道有一种神器可以完美解决这个尴尬——二脚转三脚的插头转换器。将台灯的二脚插头插在这个转换器上,此时台灯的插头就变成三脚的了,现在我们就可以将其插到三脚插座中

这个生活中的小例子,就很好地反应了我们今天所要介绍的设计模式的内在精神。在软件开发中,我们经常会遇到两个类因为接口不一样而导致无法配合使用的场景。有人可能会说,那为啥不去修改其中的一个类来保证接口一致呢?首先,一般不推荐对已有的类进行修改;其次,很多时候你使用的是第三方库所提供的类,没有源码无法直接修改。这个时候就可以利用我们从生活中获得的指引来解决这个问题——Adapter Pattern适配器模式。在该设计模式中,一般有三个角色:目标角色、被适配角色及适配器。其基本思想就是通过适配器的包装使得被适配角色与目标角色的接口一致,实现最终的 目标角色 与 被适配角色 的配合工作。具体地,Adapter Pattern适配器模式有类适配器、对象适配器两种实现方式

实现

这里我们来结合代码实现来具体解释下该模式中的思想与实践。目标接口是client使用的接口,一般会要求被适配角色最终要满足该接口,这样才能实现配合工作。这里我们定义了一个三脚插头的接口类ThreePlug

1
2
3
4
5
6
7
8
9
/**
* Target目标: 三脚插头
*/
public interface ThreePlug {
/**
* 使用三脚插头供电
*/
void use3Plug();
}

正常情况下,只要其它电器类实现了该接口,就可以通过use3Plug方法从三脚插座中取电工作了。比如下面的PC计算机类,其实现了该接口,即使用的是三脚插头

1
2
3
4
5
6
7
8
9
/**
* 使用三脚插头供电的电脑
*/
public class PC implements ThreePlug {
@Override
public void use3Plug() {
System.out.println("电脑已插电源,正常工作中...");
}
}

现在,我们利用下面的代码进行测试看看

1
2
3
4
5
6
7
8
9
/**
* Adapter Pattern 适配器模式 Demo
*/
public class AdapterPatternDemo {
public static void main(String[] args) {
ThreePlug pc = new PC();
pc.use3Plug();
}
}

显然我们的计算机是可以正常工作的

figure 1.png

基于类适配器的实现

现在我们添置了一个新电器——DeskLamp台灯,其定义如下。由于其使用的不是三脚插头,显然无法和ThreePlug接口配合工作

1
2
3
4
5
6
7
8
/**
* 使用两脚插头供电的台灯
*/
public class DeskLamp {
public void use2Plug() {
System.out.println("台灯已插电源,正常工作中...");
}
}

这个时候,就需要引入了我们的适配器角色了。通过它来将这里的DeskLamp台灯(即,被适配角色)适配ThreePlug接口。既然DeskLampAdapter台灯适配器需要满足适配ThreePlug接口,那么第一步自然是需要让其实现该接口;然后还需要让适配器将请求能够转发给真正的DeskLamp台灯,这里我们选择通过继承的方式实现。即所谓的类适配器

1
2
3
4
5
6
7
8
9
10
/**
* 台灯插头适配器, 使得其可以使用三脚插座
* @apiNote 类适配器
*/
public class DeskLampAdapter extends DeskLamp implements ThreePlug {
@Override
public void use3Plug() {
use2Plug();
}
}

现在,我们就可以让ThreePlug接口与DeskLamp共同配合工作了

1
2
3
4
5
6
7
8
9
10
11
12
/**
* Adapter Pattern 适配器模式 Demo
*/
public class AdapterPatternDemo {
public static void main(String[] args) {

System.out.println("-------- Test 1: 类适配器 ---------");
ThreePlug desklamp = new DeskLampAdapter();
desklamp.use3Plug();

}
}

可以看到通过适配器让原本两个不兼容的类可以配合工作了

figure 2.png

基于对象适配器的实现

通过继承实现的类适配器,其缺点也是显而易见的。首先,由于Java的单继承特性,所以其只能应用于适配目标为接口的场景;其次继承这一静态的方式会导致耦合性高、灵活度低。为此一般情况较为常见的是对象适配器,其是通过内部持有被适配对象的引用来完成请求的转发。显然通过这一动态的方式使之具备了低耦合、高灵活的优点

这里,我们继续添加一个需要被适配的电器——Fan电扇

1
2
3
4
5
6
7
8
/**
* 使用两脚插头供电的电扇
*/
public class Fan {
public void use2Plug() {
System.out.println("电扇已插电源,正常工作中...");
}
}

然后实现一个电扇的对象适配器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 电扇插头适配器, 使得其可以使用三脚插座
* @apiNote 对象适配器
*/
public class FanPlugAdapter implements ThreePlug{
/**
* 持有电扇对象引用
*/
private Fan fan;

public FanPlugAdapter(Fan fan) {
this.fan = fan;
}

@Override
public void use3Plug() {
fan.use2Plug();
}
}

现在,我们就来看看Fan如何与ThreePlug配合工作

1
2
3
4
5
6
7
8
9
10
11
12
/**
* Adapter Pattern 适配器模式 Demo
*/
public class AdapterPatternDemo {
public static void main(String[] args) {

System.out.println("-------- Test 2: 对象适配器 -------");
ThreePlug fan = new FanPlugAdapter( new Fan() );
fan.use3Plug();

}
}

可以看到测试结果符合预期

figure 3.png

Note

文章的最后还是需要提醒下,对于Adapter Pattern适配器模式而言。其更多是对于系统后期的维护拓展时的一种补救措施。不应该在系统设计之初就被滥用,此时如果出现接口不统一问题,应该首先考虑修改接口的设计。而且即使是在后期拓展维护阶段,过多的使用适配器也会导致系统整体非常凌乱、不利于宏观的把握,此时就应该考虑重构

参考文献

  1. Head First 设计模式 弗里曼著
0%