设计模式:适配器模式
适配器模式是什么?
什么是适配器?什么又是适配器模式?看到 “适配器” 这 3 个字,你可能会觉得很陌生,但这其实在我们生活中随处可见。适配器其实就是转换器,或者说是转换接口,比如说你没办法将 USB-C 的设备插到 USB-B 的接口上,这时你就需要 USB-C 到 USB-B 的转换接口,电源转换器也是同理。
放到面向对象当中,很多时候我们需要去使用一个对象,但是我们现有的接口并不能很好地兼容该对象,这个时候直接去改接口并不是特别方便,其他依赖接口的模块也需要相应地改变,而且任意更改接口的定义并不是一个好的举措。
适配器在这个时候就能够派上用场,我们只需要根据两边的接口定义一个适配器,通过适配器将两个接口相连。这解决了接口不兼容的问题,并且我们也不需要改变现有的代码逻辑。
简单范例解析
假设这里我们有一个鸭子接口,定义如下:
1
2
3
4
interface Duck {
public void fly();
public void quack();
}
我们默认任意一只鸭子都有 fly
和 quack
的行为。比如一只绿头鸭就可以很好地实现该接口。
1
2
3
4
5
6
7
8
9
class MallardDuck implements Duck {
public void quack() {
System.out.println("Quack");
}
public void fly() {
System.out.println("I'm flying");
}
}
同时,我们也有一只鸡的接口:
1
2
3
4
interface Chicken {
public void fly();
public void gobble();
}
同样地,这个接口也能够被具体的类实现,只要是能够实现接口内定义的方法:
1
2
3
4
5
6
7
8
9
class WildChicken implements Chicken {
public void gobble() {
System.out.println("Gobble");
}
public void fly() {
System.out.println("I'm flying a short distance");
}
}
那么现在问题来了,如果用户端使用的接口是鸭子,但是我们定义的鸡的接口也需要被用户端调用,如果直接传过去,肯定会不兼容。因此,这个时候的解决办法就是定义一个适配器:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class ChickenDuckAdapter implements Duck {
private Chicken chicken;
public ChickenDuckAdapter(Chicken chicken) {
this.turkey = turkey;
}
public void fly() {
for (int i = 0; i < 5; i++) {
chicken.fly();
}
}
public void quack() {
chicken.gobble();
}
}
在这个适配器中,我们依照双方的特征,将行为通过一定的方式进行转换。比如对于 fly
这个行为,鸭子显然要比鸡飞的距离更长,一只鸡要飞更多的次数才能达到鸭子的距离。鸭子的发声虽然和鸡的不太一样,但是二者均为发声,这里就没有做特殊的变化。
有了这个适配器,在用户端,鸡也能够当成鸭子来使用,解决了接口的不兼容性:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class DuckTestClient {
public static void main(String[] args) {
MallardDuck duck = new MallardDuck();
WildChicken chicken = new WildChicken();
Duck chickenDuckAdapter = new ChickenDuckAdapter(chicken);
System.out.println("The Chicken says...");
chicken.gobble();
chicken.fly();
System.out.println("\nThe Duck says...");
testDuck(duck);
System.out.println("\nThe ChickenDuckAdapter says...");
testDuck(chickenDuckAdapter);
}
static void testDuck(Duck duck) {
duck.quack();
duck.fly();
}
}
范例解析 - 枚举迭代适配器
在任何一个编程语言中,迭代器都是不可或缺的。比如在 Java 语言中,常见的 Collection 类就实现了 Iterator 的接口。Iterator 的接口定义并不复杂,在 Java 1.7 之前,Iterator 接口定义如下:
1
2
3
4
5
public interface Iterator<E> {
boolean hasNext();
E next();
void remove();
}
另外一个常见的接口是枚举接口 - Enumeration:
1
2
3
4
public interface Enumeration<E> {
boolean hasMoreElements();
E nextElement();
}
你会发现这两个接口的方法其实干的事情非常类似,但是方法的名称不一样,因而导致两个接口无法兼容。这时,我们就可以创建相应的适配器来解决这个问题。
比如,我们可以创建 Iterator 到 Enumeration 的适配器,可以将 Iterator 的对象进行一层包装,这样包装后的对象可以被当成是 Enumeration 来看待:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class IteratorEnumerationAdapter implements Enumeration {
private Iterator iter;
public IteratorEnumerationAdapter(Iterator iter) {
this.iter = iter;
}
public boolean hasMoreElements() {
return iter.hasNext();
}
public Object nextElement() {
return iter.next();
}
}
反过来也是一样的,我们也可以创建 Enumeration 到 Iterator 的适配器:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class EnumerationIteratorAdapter implements Iterator {
private Enumeration enumeration;
public EnumerationIteratorAdapter(Enumeration enumeration) {
this.enumeration = enumeration;
}
public boolean hasNext() {
return enumeration.hasMoreElements();
}
public Object next() {
return enumeration.nextElement();
}
public void remove() {
throw new UnsupportedOperationException();
}
}
总结与延伸
适配器模式的目的是包裹一个对象,使之能够兼容一个接口。通过上面的例子,我们也可以感受到适配器模式的实用性,适配器就像一个黏合剂,把两个不相关的接口给融合在一起。但是在实际的使用中需要注意的是,适配器相连的两个接口需要有一定的相关性,虽然你也可以把两个毫不相关的东西通过适配器相连,但是这样的话你的适配器内部需要重新实现很多的复杂逻辑,提升代码复杂度不说,也会对代码阅读造成一定的困惑。
还有就是,关于适配器类的命名,一般来说可以采用 Interface1Interface2Adapter
的格式,末尾的 Adapter
表明这是一个适配器类,Interface1Interface2
说明这个适配器是将实现了 Interface1
接口的对象转换成实现 Interface2
接口的对象。这样,代码读者只看类名就大致知道这个类的具体是做什么的了。当然,命名只是一个参考,每个人都有自己的命名习惯,只要能够把一个东西解释清楚就行。
最后提一点,这里我们所展示的适配器模式的实现方式是通过对象组合的方式来实现的。另外还有一种通过多重继承来实现适配器模式的实现方式。在 Java 中,是没有多重继承的,因而,我们会选择组合的方式来实现。当然,在其他的语言中,比如 C++,继承方式就成了首选。不论哪种实现方式,背后的原理都是一样的。