定义

将一个类的接口转换为客户希望的另外一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些累可以一起工作。

结构和说明

结构型模式-适配器模式 - 图1

示例代码

  1. public class AdapterDemo {
  2. public static interface Target {
  3. void request();
  4. }
  5. public static class Adapter implements Target {
  6. private Adaptee adaptee;
  7. public Adapter(Adaptee adaptee) {
  8. this.adaptee = adaptee;
  9. }
  10. @Override
  11. public void request() {
  12. System.out.println("Target.request");
  13. adaptee.specificRequest();
  14. }
  15. }
  16. public static class Adaptee {
  17. public void specificRequest() {
  18. System.out.println("Adaptee.specificRequest");
  19. }
  20. }
  21. public static class Client {
  22. public static void main(String[] args) {
  23. Target target = new Adapter(new Adaptee());
  24. target.request();
  25. }
  26. }
  27. }

调用顺序

结构型模式-适配器模式 - 图2

双向适配器

注意: 使用适配器有一个潜在的问题,就是被适配的对象不再兼容 Adaptee 的接口,因为适配器只是实现了 Target 接口。这导致并不是所有 Adaptee 对象可以使用的地方都能使用适配器。

而双向适配器解决了这样的问题,双向适配器同时实现了 Target 和 Adaptee 的接口,使得双向适配器可以在 Target 或 Adaptee 被使用的地方使用,以提供对所有客户的透明性。尤其在两个客户端需要用不同的方式查看同一个对象时,适合使用双向适配器。

结构和说明

结构型模式-适配器模式 - 图3

示例代码

  1. public class TwoWayAdapterDemo {
  2. public static interface TargetApi {
  3. void request();
  4. }
  5. public static interface AdapteeApi {
  6. void specificRequest();
  7. }
  8. public static class TargetImpl implements TargetApi {
  9. @Override
  10. public void request() {
  11. System.out.println("Target.request");
  12. }
  13. }
  14. public static class AdapteeImpl implements AdapteeApi {
  15. @Override
  16. public void specificRequest() {
  17. System.out.println("Adaptee.specificRequest");
  18. }
  19. }
  20. public static class Adapter implements TargetApi, AdapteeApi {
  21. private TargetApi target;
  22. private AdapteeApi adaptee;
  23. public Adapter(TargetApi target, AdapteeApi adaptee) {
  24. this.target = target;
  25. this.adaptee = adaptee;
  26. }
  27. @Override
  28. public void request() {
  29. // TargetApi 的方法中使用 AdapteeApi 的方法实现
  30. System.out.println("Adapter.request");
  31. adaptee.specificRequest();
  32. }
  33. @Override
  34. public void specificRequest() {
  35. // AdapteeApi 的方法中使用 TargetApi 的方法实现
  36. System.out.println("Adapter.specificRequest");
  37. target.request();
  38. }
  39. }
  40. public static class ClientV1 {
  41. public static void main(String[] args) {
  42. TargetApi target = new Adapter(new TargetImpl(), new AdapteeImpl());
  43. target.request();
  44. }
  45. }
  46. public static class ClientV2 {
  47. public static void main(String[] args) {
  48. AdapteeApi adaptee = new Adapter(new TargetImpl(), new AdapteeImpl());
  49. adaptee.specificRequest();
  50. }
  51. }
  52. }

对象适配器和类适配器

  • 对象适配器的实现: 依赖于对象组合。就如同前面的实现示例,都是采用对象组合的方式,也就是对象适配器实现的方式。
  • 类适配器的实现: 采用多重继承对一个接口与另一个接口进行匹配。由于 Java 不支持多重集成,所以到目前为止还没有涉及。

权衡

  • 从实现上: 类适配器使用对象继承的方式,是静态的定义方式;而对象适配器使用对象组合的方式,是动态组合的方式。
  • 对于类适配器,由于适配器直接继承了 Adaptee, 使得适配器不能和 Adaptee 的子类一起工作,因为继承是静态的关系,当适配器继承了 Adaptee 后,就不可能再去处理 Adaptee 的子类了。

    对于对象适配器,允许一个 Adapter 和多个 Adaptee,包括 Adaptee 和它所有的子类一起工作。因为对象适配器采用的是对象组合的关系,只要对象类型正确,是不是子类都无所谓。

  • 对于类适配器,适配器可以重新定义 Adaptee 的部分行为,相当于子类覆盖父类的部分实现方法。

    对于对象适配器,要重新定义 Adaptee 的行为比较困难,这种情况下,需要定义 Adaptee 的子类来实现重定义,然后让适配器组合子类。

  • 对于类适配器,仅仅引入了一个对象,并不需要额外的引用来间接得到 Adaptee。对于对象适配器,需要额外的引用来间接得到 Adaptee。

    在 Java 开发中,建议大家尽量使用对象适配器的实现方式。当然,具体问题具体分析,根据需要来选用实现方式,最合适的才是最好的。

优缺点

优点

  • 更好的复用性

如果功能是已经用了的,只是接口不兼容,那么通过适配器模式就可以让这些功能得到更好的复用。

  • 更好的可扩展性

在实现适配器功能的时候,可以调用自己开发的功能,从而自然地扩展系统的功能。

缺点

  • 过多的使用适配器,会让系统非常凌乱,不容易整体进行把握

比如,命名看到的是 A 接口,其实内部被适配成了 B 接口来实现,一个系统如果太多出现这种情况,无异于一场灾难。因此如果不是很有必要,可以不使用适配器,而是直接对系统进行重构。

思考

本质

转换匹配,复用功能。

何时选用

  • 如果你想使用一个已经存在的类,但是它的接口不符合你的需求,这种情况可以使用适配器模式,来把已有的实现转换成你需要的接口。
  • 如果你想创建一个可以复用的类,这个类可能和一些不兼容的类一起工作,这种情况可以使用适配器模式,到时候需要什么就适配什么。
  • 如果你想使用一些已经存在的子类,但是不可能对每一个子类都进行适配,这种情况可以选用对象适配器,直接适配这些子类的父类就可以了。

相关模式

  • 适配器模式与桥接模式

其实这两个模式出了结构略为相似外,功能完全不同。
适配器模式是把两个或者多个接口的功能进行转换匹配;而桥接模式是让接口和实现部分分离,以便他们可以相对独立地变化。

  • 适配器模式与装饰模式

从某种意义上来讲,适配器模式能够模拟实现简单的装饰模式的功能,也就是为已有功能增添功能。比如我们在适配器里面这么写:

  1. public void adapterMethod() {
  2. System.out.println("在调用 Adaptee 的方法之前完成一定的工作");
  3. // 调用 Adaptee 的相关方法
  4. adaptee.method();
  5. System.out.println("在调用 Adaptee 的方法之后完成一定的工作");
  6. }

如上的写法,就相当于在调用 Adaptee 的适配方法前后添加了新的功能,这样适配过后,客户端得到得到的功能就不单纯是 Adaptee 的被适配方法的功能了。

注意,仅仅是类似,造成这种相似的原因是: 两种设计模式在实现上都是使用的对象组合,都可以在转调组合对象的功能的前后进行一些附加的处理,因此有这么一个相似性。它们的目的和本质都是不一样的。

两个模式又一个很大的不同: 一般适配器适配过后是需要改变接口的,如果不改接口就没有必要适配了;而装饰模式是不需要改变接口的,无论多少层装饰都是一个接口。因此装饰模式可以很容易地支持递归组合,而适配器做不到,每次的接口不同,无法递归。

  • 适配器模式与代理模式

适配器模式可以和代理模式组合使用。在实现适配器的时候,可以通过代理调用 Adaptee,这样可以获得更大的灵活性。

  • 适配器模式与抽象工厂模式

在适配器实现的时候,通常需要得到被适配的对象。如果被适配的是一个接口,那么就可以结合一些可以创造对象实例的设计模式,来得到被适配的对象实例,比如抽象工厂模式、单例模式、工厂方法模式等。