适配器模式

适配器模式

案例

一天,张三把自己的iPhone手机玩的没电了想找充电器,但是完了带数据线。而李四有数据线,只不过是TypeC接口的,张三用不了,就不能继续愉快的玩耍了。下面我们来模拟出描述的内容:

1.首先定义了三个实体类:

TyecC数据线接口类:

/**
 * TypeC接口
 */
public class TypeC {
    private String username;

    public TypeC(String username) {
        this.username = username;
        System.out.println("我是" + this.username + "的Type-C数据线接口");
    }

    public void typeC() {
        System.out.println(this.username + "的数据线提供Type-C接口进行充电~~");
    }
}

Lightning数据线接口类:

/**
 * Lightning接口
 */
public class Lightning {
    private String username;

    public Lightning() {
    }

    public Lightning(String username) {
        this.username = username;
        System.out.println("我是" + this.username + "的Lighting数据线接口");
    }

    public void lightning() {
        System.out.println(this.username + "的数据线提供Lightning接口进行充电~~");
    }
}

手机类:

/**
 * 手机类
 */
public class IPhone {
    private String username;

    public IPhone(String username) {
        this.username = username;
        System.out.println("我是" + this.username + "的iPhone");
    }

    public void charge(Lightning lightning) {
        System.out.println(this.username + "的iPhone需要Lightning接口进行充电~~");
        if (lightning == null) {
            System.out.println("没带数据线,不能充电");
            return;
        }
        lightning.lightning();
    }
}

2.代码模拟出整个过程:

/**
 * 模拟手机充电过程
 */
public class Main {
    public static void main(String[] args) {
        String zs = "张三";
        String ls = "李四";
        IPhone iPhone = new IPhone(zs);
        System.out.println("我没电了");
        System.out.println();

        System.out.println(zs + "说:‘手机没电了,找数据线充电’");
        System.out.println();

        Lightning lightning = new Lightning(zs);
        lightning.lightning();
        System.out.println("今天" + zs + "忘了带上我>_<");
        lightning = null;
        iPhone.charge(lightning);
        System.out.println();

        System.out.println(ls + "说:‘我有数据线’");
        TypeC typeC = new TypeC(ls);
        typeC.typeC();
        System.out.println();

        System.out.println(zs + "的iPhone不能使用" + ls + "的数据线,不能继续玩手机了╯﹏╰");
    }
}

3.代码运行结果:

我是张三的iPhone
我没电了

张三说:‘手机没电了,找数据线充电’

我是张三的Lighting数据线接口
张三的数据线提供Lightning接口进行充电~~
今天张三忘了带上我>_<
张三的iPhone需要Lightning接口进行充电~~
没带数据线,不能充电

李四说:‘我有数据线’
我是李四的Type-C数据线接口
李四的数据线提供Type-C接口进行充电~~

张三的iPhone不能使用李四的数据线,不能继续玩手机了╯﹏╰

上述过程在生活中可能并不少见,一些人出门玩,手机没电了却没带充电器。向别人寻求帮助时,可能因为自己的手机与别人提供的数据线不匹配而充不了电,最后导致手机不能用了。这种时候我们如果实在找不到与手机匹配的数据线接口,还有种解决方法,那就是我们再找一个转接头就可以用别人的数据线接口进行充电了。这与我们设计模式中的适配器模式非常相似,下面就介绍一下适配器模式。

模式介绍

在计算机编程中,适配器模式-结构型模式(有时候也称包装样式或者包装)将一个类的接口适配成用户所期待的。一个适配允许通常因为接口不兼容而不能在一起工作的类工作在一起,做法是将类自己的接口包裹在一个已存在的类中。

注意:在适配器模式定义中所提及的接口是指广义的接口,它可以表示一个方法或者方法的集合。

角色构成:

  • Target(目标抽象类):目标抽象类定义客户所需接口,可以是一个抽象类或接口,也可以是具体类。
  • Adapter(适配器类):适配器可以调用另一个接口,作为一个转换器,对Adaptee和Target进行适配,适配器类是适配器模式的核心,在对象适配器中,它通过继承Target并关联一个Adaptee对象使二者产生联系。
  • Adaptee(适配者类):适配者即被适配的角色,它定义了一个已经存在的接口,这个接口需要适配,适配者类一般是一个具体类,包含了客户希望使用的业务方法,在某些情况下可能没有适配者类的源代码。

在适配器模式中,我们通过增加一个新的适配器类来解决接口不兼容的问题,使得原本没有任何关系的类可以协同工作。根据适配器类与适配者类的关系不同,适配器模式可分为对象适配器类适配器两种,在对象适配器模式中,适配器与适配者之间是关联关系;在类适配器模式中,适配器与适配者之间是继承(或实现)关系。两种形式最大的区别在于适配器和适配者之间的关系不同,因此它们的UML类图略有不同。

UML类图:

1.对象适配器:

adapter-object

2.类适配器:

adapter-class

与转接头相似,在适配器模式中引入了一个被称为适配器(Adapter)的包装类,而它所包装的对象称为适配者(Adaptee),即被适配的类。适配器的实现就是把客户类的请求转化为对适配者的相应接口的调用。也就是说:当客户类调用适配器的方法时,在适配器类的内部将调用适配者类的方法,而这个过程对客户类是透明的,客户类并不直接访问适配者类。因此,适配器让那些由于接口不兼容而不能交互的类可以一起工作。

代码改造

接下来我们就按照对象适配器形式的对手机充电这一过程进行改造。

1.首先依旧是上面的三个实体类:

代码与上面的一样,这里就不重复粘贴了。其中Lightning类是Target目标类角色,TypeC类是Adaptee适配者类。

2.引入Adapter适配器类:

/**
 * 接口转接头:适配器角色
 */
public class Adapter extends Lightning {
    private TypeC typeC;

    public Adapter(String username, TypeC typeC) {
        this.typeC = typeC;
        System.out.println("我是" + username + "的转接头");
    }

    @Override
    public void lightning() {
        System.out.println("我的一头适配了" + typeC.getUsername() + "的数据线");
        typeC.typeC();
        System.out.println("我的另一头提供Lightning接口连接手机进行充电~~");
    }
}

3.模拟整个过程:

public class Main {
    public static void main(String[] args) {
        String zs = "张三";
        String ls = "李四";
        IPhone iPhone = new IPhone(zs);
        System.out.println("我没电了");
        System.out.println();

        System.out.println(zs + "说:‘手机没电了,找数据线充电’");
        System.out.println();

        Lightning lightning = new Lightning(zs);
        lightning.lightning();
        System.out.println("今天" + zs + "忘了带上我>_<");
        lightning = null;
        iPhone.charge(lightning);
        System.out.println();

        System.out.println(ls + "说:‘我有数据线’");
        TypeC typeC = new TypeC(ls);
        typeC.typeC();
        System.out.println(zs + "的iPhone不能使用" + ls + "的数据线进行充电");
        System.out.println();

        String ww = "王五";
        System.out.println(ww + "说:‘我有转接头’");
        Adapter adapter = new Adapter(ww, typeC);

        iPhone.charge(adapter);
        System.out.println(zs + "的手机充完电又可以继续玩了Y(^_^)Y");
    }
}

4.运行结果:

我是张三的iPhone
我没电了

张三说:‘手机没电了,找数据线充电’

我是张三的Lighting数据线接口
张三的数据线提供Lightning接口进行充电~~
今天张三忘了带上我>_<
张三的iPhone需要Lightning接口进行充电~~
没带数据线,不能充电

李四说:‘我有数据线’
我是李四的Type-C数据线接口
李四的数据线提供Type-C接口进行充电~~
张三的iPhone不能使用李四的数据线进行充电

王五说:‘我有转接头’
我是王五的转接头
张三的iPhone需要Lightning接口进行充电~~
我的一头适配了李四的数据线
李四的数据线提供Type-C接口进行充电~~
我的另一头提供Lightning接口连接手机进行充电~~
张三的手机充完电又可以继续玩了Y(^_^)Y

可以看到我们引入了Adapter适配器类后,张三就可以用王五的转接头,再配合李四的TypeC数据线接口进行充电了。

模式应用

适配器模式将现有接口转化为客户类所期望的接口,实现对现有类的复用,它是一种使用频率非常高的设计模式,在软件开发中得以广泛应用。其中 Mybatis 框架的日志模块中就是使用了适配器模式。下面引入了 Mybatis 框架及其日志模块的代码来分析适配器模式的应用。

1.pom 依赖:

<dependencies>
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>3.4.6</version>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
        <version>1.7.30</version>
    </dependency>
    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-classic</artifactId>
        <version>1.2.3</version>
    </dependency>
</dependencies>

2.目标接口Log--Mybatis 内部使用日志时的统一接口:

/**
 * @author Clinton Begin
 */
public interface Log {

  boolean isDebugEnabled();

  boolean isTraceEnabled();

  void error(String s, Throwable e);

  void error(String s);

  void debug(String s);

  void trace(String s);

  void warn(String s);

}

3.适配器类Slf4jLoggerImpl

/**
 * @author Eduardo Macarron
 */
class Slf4jLoggerImpl implements Log {

  private final Logger log;

  public Slf4jLoggerImpl(Logger logger) {
    log = logger;
  }

  @Override
  public boolean isDebugEnabled() {
    return log.isDebugEnabled();
  }
  // 省略其他重写的接口

}

4.适配者角色是 slf4j 中的接口org.slf4j.Logger类,这是一个接口,它有很多的实现,这里引入的是 logback 中的实现ch.qos.logback.classic.Logger类。

下面是以上几个类之间的关系图:

adapter-mybatis-log

分析:

Java开发中经常用到的日志框架有很多,Log4j、Log4j2、slf4j等等,Mybatis定义了一套统一的日志接口供上层使用,并为上述常用的日志框架提供了相应的适配器 在Mybatis的日志模块中就是使用了适配器模式。

Mybatis内部在使用日志模块时,使用了其内部接口 org.apache.ibatis.logging.Log,但是常用的日志框架的对外接口各不相同,Mybatis为了复用和集成这些第三方日志组件,在其日志模块中,提供了多种Adapter,将这些第三方日志组件对外接口适配成org.apache.ibatis.logging.Log,这样Myabtis 就可以通过Log接口调用第三方日志了。

总结

1.主要优点

​ 无论是对象适配器模式还是类适配器模式都具有如下优点:

  • 将目标类和适配者类解耦,通过引入一个适配器类来重用现有的适配者类,无须修改原有结构。
  • 增加了类的透明性和复用性,将具体的业务实现过程封装在适配者类中,对于客户端类而言是透明的,而且提高了适配者的复用性,同一个适配者类可以在多个不同的系统中复用。
  • 灵活性和扩展性都非常好,通过使用配置文件,可以很方便地更换适配器,也可以在不修改原有代码的基础上增加新的适配器类,完全符合“开闭原则”。

​ 具体来说,类适配器模式还有如下优点:

  • 由于适配器类是适配者类的子类,因此可以在适配器类中置换一些适配者的方法,使得适配器的灵活性更强。

​ 对象适配器模式还有如下优点:

  • 一个对象适配器可以把多个不同的适配者适配到同一个目标;

  • 可以适配一个适配者的子类,由于适配器和适配者之间是关联关系,根据“里氏代换原则”,适配者的子类也可通过该适配器进行适配。

2.主要缺点

​ 类适配器模式的缺点如下:

  • 对于Java、C#等不支持多重类继承的语言,一次最多只能适配一个适配者类,不能同时适配多个适配者;
  • 适配者类不能为最终类,如在Java中不能为final类,C#中不能为sealed类;
  • 在Java、C#等语言中,类适配器模式中的目标抽象类只能为接口,不能为类,其使用有一定的局限性。

​ 对象适配器模式的缺点如下:

  • 与类适配器模式相比,要在适配器中置换适配者类的某些方法比较麻烦。如果一定要置换掉适配者类的一个或多个方法,可以先做一个适配者类的子类,将适配者类的方法置换掉,然后再把适配者类的子类当做真正的适配者进行适配,实现过程较为复杂。

3.适用场景

  • 系统需要使用一些现有的类,而这些类的接口(如方法名)不符合系统的需要,甚至没有这些类的源代码。
  • 想创建一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作。

参考资料

本篇文章github代码地址:https://github.com/Phoegel/design-pattern/tree/main/adapter
转载请说明出处,本篇博客地址:https://www.cnblogs.com/phoegel/p/13933499.html

posted @ 2020-11-05 19:26  Phoegel  阅读(68)  评论(0编辑  收藏  举报