Java 设计模式06——适配器模式

模式定义

适配器模式,属于结构型模式,是将一个类的接口(源接口)转换成客户希望的另外一个接口(目标接口)。使得原本由于接口不兼容而不能一起工作的那些类可以一起工作,经过适配后,通过适配类可以使源接口目标接口一起工作。根据适配的写法不同,主要分为通过类型适配和通过对象适配。

类型适配

适配类继承源接口的实现类,并实现目标接口,通过类型继承来实现。

对象适配

适配类持有源接口实现类的对象引用,并通过构造函数传入,并实现目标接口。Java中ForkJoinTask中就大量的运用了这种类型适配方法,有了这些适配方法,像RunnableCallable都可以在适配后作为ForkJoinTask提交到ForkJoinPool中。

模式示例

ForkJoinPool中接收的任务类型是ForkJoinTask,如果我们要将实现Runnable的对象和实现Callable接口的对象(二者都是可执行的)提交到ForkJoinPool中,我们就需要对Runnable和Callable进行适配。下面看具体代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* Adaptor for Runnables. This implements RunnableFuture
* to be compliant with AbstractExecutorService constraints
* when used in ForkJoinPool.
*/
static final class AdaptedRunnable<T> extends ForkJoinTask<T>
implements RunnableFuture<T> {
final Runnable runnable;
T result;
AdaptedRunnable(Runnable runnable, T result) {
if (runnable == null) throw new NullPointerException();
this.runnable = runnable;
this.result = result; // OK to set this even before completion
}
public final T getRawResult() { return result; }
public final void setRawResult(T v) { result = v; }
public final boolean exec() { runnable.run(); return true; }
public final void run() { invoke(); }
private static final long serialVersionUID = 5232453952276885070L;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* Adaptor for Runnables without results
*/
static final class AdaptedRunnableAction extends ForkJoinTask<Void>
implements RunnableFuture<Void> {
final Runnable runnable;
AdaptedRunnableAction(Runnable runnable) {
if (runnable == null) throw new NullPointerException();
this.runnable = runnable;
}
public final Void getRawResult() { return null; }
public final void setRawResult(Void v) { }
public final boolean exec() { runnable.run(); return true; }
public final void run() { invoke(); }
private static final long serialVersionUID = 5232453952276885070L;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* Adaptor for Runnables in which failure forces worker exception
*/
static final class RunnableExecuteAction extends ForkJoinTask<Void> {
final Runnable runnable;
RunnableExecuteAction(Runnable runnable) {
if (runnable == null) throw new NullPointerException();
this.runnable = runnable;
}
public final Void getRawResult() { return null; }
public final void setRawResult(Void v) { }
public final boolean exec() { runnable.run(); return true; }
void internalPropagateException(Throwable ex) {
rethrow(ex); // rethrow outside exec() catches.
}
private static final long serialVersionUID = 5232453952276885070L;
}
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
/**
* Adaptor for Callables
*/
static final class AdaptedCallable<T> extends ForkJoinTask<T>
implements RunnableFuture<T> {
final Callable<? extends T> callable;
T result;
AdaptedCallable(Callable<? extends T> callable) {
if (callable == null) throw new NullPointerException();
this.callable = callable;
}
public final T getRawResult() { return result; }
public final void setRawResult(T v) { result = v; }
public final boolean exec() {
try {
result = callable.call();
return true;
} catch (Error err) {
throw err;
} catch (RuntimeException rex) {
throw rex;
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
public final void run() { invoke(); }
private static final long serialVersionUID = 2838392045355241008L;
}

上面采用的都是对象适配的方式,这也是更推荐的适配器模式实现方式,它采用的时组合方式,而设计模式的基本原则第6条推荐优先使用组合而不是继承。

再看看另一个例子。

定义两个接口:FlyAnimal(飞行动物)和CrawlAnimal(爬行动物);定义两个类:飞行动物实现类Bird以及爬行动物实现类Pig。

假设现在有一个飞行动物的聚会,而我们想让Pig这个爬行动物参加,这时我们就需要将Pig伪装(适配)成飞行动物。这样Pig就可以用伪装的身份去参加飞行动物的聚会了,具体代码如下:

1
2
3
4
5
public interface CrawlAnimal {
String crawlKindName();

void crawl();
}
1
2
3
4
5
public interface FlyAnimal {
String flyKindName();

void fly();
}
1
2
3
4
5
6
7
8
9
10
11
public class Pig implements CrawlAnimal{
@Override
public String crawlKindName() {
return "My kind is Pig";
}

@Override
public void crawl() {
System.out.println("I can crawl");
}
}
1
2
3
4
5
6
7
8
9
10
11
public class Bird implements FlyAnimal {
@Override
public String flyKindName() {
return "My kind is Bird";
}

@Override
public void fly() {
System.out.println("I can fly");
}
}

下面通过两种形式对Pig进行伪装,让其能参加飞行类动物的聚会。

类型适配

1
2
3
4
5
6
7
8
9
10
11
public class PigClassAdapter extends Pig implements FlyAnimal{
@Override
public String flyKindName() {
return super.crawlKindName();
}

@Override
public void fly() {
super.crawl();
}
}

对象适配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class PigObjectAdapter implements FlyAnimal{
private Pig pig;

public PigObjectAdapter(Pig pig) {
this.pig = pig;
}

@Override
public String flyKindName() {
return pig.crawlKindName();
}

@Override
public void fly() {
pig.crawl();
}
}

模式总结

适配器模式为只能相似的类或接口之间的兼容提供了一个兼容性方案,这种方案的代价就是在通过建立新的适配类以类型适配或对象适配的方式来让本来不能一起工作的类或接口能在一起工作。

优缺点

  • 优点
    • 1、可以让任何两个没有关联的类一起运行。
    • 2、提高了类的复用。
    • 3、增加了类的透明度。
    • 4、灵活性好。
  • 缺点
    • 过多地使用适配器,会让系统非常零乱,不易整体进行把握。比如,明明看到调用的是 A 接口,其实内部被适配成了 B 接口的实现,一个系统如果太多出现这种情况,无异于一场灾难。因此如果不是很有必要,可以不使用适配器,而是直接对系统进行重构。 (过于灵活的东西通常都会存在类似问题
    • 2.由于 JAVA 至多继承一个类,所以至多只能适配一个适配者类,而且目标类必须是抽象类。

使用场景

不能因为适配器模式的灵活性而滥用,适配器模式一般是对现有系统的一种兼容,当我们目标明确的要让正在运行的某个接口能使用另一个接口的某些功能时,我们可以使用适配器模式。

适配器模式是为了解决正在运行的系统问题而提出来的,只是一种解决现有问题的一种兼容措施。在系统设计的时候应该尽量避免这种情况的出现,而不是将适配器模式考虑在内。