杰瑞科技汇

java 设计模式 适配器模式

什么是适配器模式?

核心思想:

适配器模式是一种结构型设计模式,它允许不兼容的接口能够协同工作,它就像一个电源适配器(充电头)一样,将一个类的接口转换成客户端期望的另一个接口,从而使原本因接口不兼容而无法一起工作的类可以一起工作。

生活中的例子:

  • 电源适配器:中国的插座是两孔或三孔的扁型,而很多国外电子设备的插头是两圆脚的,电源适配器(充电头)就是一个中间层,它将外国的插头转换成中国插座可以接受的形状。
  • 读卡器:你的电脑没有 SD 卡槽,但你有一个读卡器,读卡器将 SD卡的接口转换成了 USB 接口,这样你的电脑(客户端)就能通过 USB 接口读取 SD 卡中的数据了。

UML 类图:

+----------------+       +-----------------------+       +----------------+
|   Client       |       |      Adapter          |       |    Adaptee     |
+----------------+       +-----------------------+       +----------------+
| +operation()  |------>| +adaptee: Adaptee     |------>| +specificRequest() |
+----------------+       |                       |       +----------------+
                        | +request()            |
                        +-----------------------+

图解:

  • Client (客户端):需要调用 request() 方法的代码。
  • Target (目标接口):客户端期望的接口,这里是 request() 方法。
  • Adaptee (被适配者):已经存在的、具有特定接口的类,它的方法是 specificRequest(),与客户端期望的接口不兼容。
  • Adapter (适配器):持有 Adaptee 的引用,并实现了 Target 接口,在 request() 方法内部,它会调用 AdapteespecificRequest() 方法,从而完成转换。

适配器模式的两种主要实现方式

适配器模式分为两种:类适配器对象适配器,在 Java 中,由于不支持多重继承(一个类不能同时继承两个类),所以类适配器使用较少,对象适配器是更常用和推荐的方式。

对象适配器(推荐)

这是最常用、最灵活的方式,它通过组合来实现,即适配器类持有一个被适配者的实例。

实现步骤:

  1. 定义一个 目标接口,这是客户端期望的接口。
  2. 定义一个 被适配者类,这是已存在的、具有特定接口的类。
  3. 创建一个 适配器类,实现目标接口,并在内部持有一个被适配者类的实例。
  4. 在适配器类的目标方法中,调用被适配者类的方法。

代码示例:对象适配器

假设我们要开发一个日志系统,客户端希望统一使用 LogApi 接口来记录日志,但项目中已经有一个功能强大的第三方日志库 LegacyLogger,它的接口与我们期望的不同。

第1步:定义目标接口

这是客户端希望使用的标准接口。

// 目标接口:客户端期望的日志API
public interface LogApi {
    void log(String message);
}

第2步:定义被适配者类

这是已经存在的、功能完善但接口不兼容的类。

// 被适配者:已有的第三方日志库,它的接口我们不希望直接修改
public class LegacyLogger {
    // 它的日志方法叫 doLog,参数和我们的期望不同
    public void doLog(String level, String message) {
        // 模拟一个复杂的日志记录过程
        System.out.println("[Legacy Logger] [" + level.toUpperCase() + "] " + message);
    }
}

第3步:创建适配器类

这是适配器的核心,它连接了 LogApiLegacyLogger

// 适配器:实现了目标接口,并组合了被适配者
public class LoggerAdapter implements LogApi {
    // 持有被适配者的实例
    private LegacyLogger legacyLogger;
    // 通过构造函数注入被适配者
    public LoggerAdapter(LegacyLogger legacyLogger) {
        this.legacyLogger = legacyLogger;
    }
    @Override
    public void log(String message) {
        // 将客户端的简单调用,转换成被适配者复杂调用的形式
        // 假设我们默认使用 "INFO" 级别
        this.legacyLogger.doLog("INFO", message);
    }
}

第4步:客户端使用

客户端代码现在可以完全基于 LogApi 接口编程,无需关心 LegacyLogger 的存在。

public class Client {
    public static void main(String[] args) {
        // 1. 客户端期望使用 LogApi
        LogApi logger = new LoggerAdapter(new LegacyLogger());
        // 2. 客户端可以像调用标准接口一样调用 log 方法
        logger.log("This is a log message from the client.");
        // 3. 输出结果:
        // [Legacy Logger] [INFO] This is a log message from the client.
    }
}

总结对象适配器: 客户端通过 LogApi 接口与 LoggerAdapter 交互。LoggerAdapter 在内部将 log() 调用翻译成 LegacyLoggerdoLog() 调用,完美实现了接口的转换。


类适配器(不常用,了解即可)

类适配器通过继承来实现,适配器类同时继承被适配者类和实现目标接口。

UML 类图:

+----------------+       +-----------------------+
|   Client       |       |      Adapter          |
+----------------+       +-----------------------+
| +operation()  |------>| +adaptee: Adaptee     |
+----------------+       |                       |
                        | +request()            |
                        +-----------------------+
                              ^
                              |
                        +-----------------------+
                        |    Adaptee           |
                        +-----------------------+
                        | +specificRequest()    |
                        +-----------------------+

Java 代码示例(仅作演示):

// 目标接口
interface LogApi {
    void log(String message);
}
// 被适配者类
class LegacyLogger {
    public void doLog(String level, String message) {
        System.out.println("[Legacy Logger] [" + level.toUpperCase() + "] " + message);
    }
}
// 适配器类:同时继承 LegacyLogger 并实现 LogApi
// 注意:Java 只能单继承,所以这种方式灵活性较差
class ClassLoggerAdapter extends LegacyLogger implements LogApi {
    @Override
    public void log(String message) {
        // 直接调用父类的方法
        this.doLog("INFO", message);
    }
}
// 客户端使用
public class Client {
    public static void main(String[] args) {
        LogApi logger = new ClassLoggerAdapter();
        logger.log("This is a log message from the client.");
    }
}

为什么对象适配器更推荐?

  1. 灵活性:对象适配器通过组合,可以在运行时动态地指定被适配者对象,而类适配器在编译时就确定了被适配者(通过继承)。
  2. 解耦:适配器与被适配者是松耦合的(通过组合关系),而不是紧耦合的(通过继承关系)。
  3. 多重继承的限制:Java 不支持多重继承,如果被适配者本身已经继承了一个类,那么类适配器就无法再继承它了。

适配器模式的优缺点

优点

  1. 提高复用性:使得原本由于接口不兼容而无法一起工作的类可以协同工作,复用了现有代码。
  2. 增加灵活性:引入适配器后,系统就不需要修改原有代码,符合“开闭原则”(对扩展开放,对修改关闭)。
  3. 解耦:将客户端与被适配者解耦,客户端只与目标接口交互。

缺点

  1. 增加复杂性:系统中会增加新的类和接口,使得系统整体结构变得复杂。
  2. 过度使用:如果被适配者类本身就提供了需要的功能,或者可以通过简单重构来匹配接口,那么使用适配器模式就是不必要的,会增加不必要的间接层。

适配器模式的应用场景

适配器模式在以下场景中特别有用:

  1. 需要整合第三方库或遗留系统:这是最经典的应用场景,当你想使用一个功能强大的旧系统或第三方库,但它的接口与你的系统不兼容时,适配器模式是最佳选择。
  2. 统一多个类的接口:当系统需要使用多个不同的类,但这些类的接口又各不相同时,可以创建适配器,为它们提供一个统一的接口,方便客户端调用。
  3. 版本升级:当系统升级某个模块时,新模块的接口可能与旧模块不兼容,可以使用适配器来保持与调用方代码的兼容性,实现平滑过渡。

与其他模式的区别

  • 适配器模式 vs 代理模式

    • 目的不同:适配器模式的目的是转换接口,让两个不兼容的接口能够协同工作,代理模式的目的是控制对对象的访问,通常在不改变接口的前提下,增加一些额外的功能(如日志、权限检查)。
    • 关注点不同:适配器关注“转换”,代理关注“控制”。
  • 适配器模式 vs 装饰器模式

    • 目的不同:装饰器模式的目的是动态地给对象添加新的职责,而不改变其接口,适配器模式的目的是改变接口
    • 结构不同:装饰器通常遵循“组合+继承”的结构,并且装饰的对象和被装饰的对象通常属于同一个类型体系,适配器则不一定。
特性 描述
模式类型 结构型模式
核心意图 将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
关键角色 Target (目标接口), Adaptee (被适配者), Adapter (适配器)
主要实现 对象适配器(组合) 是 Java 中最常用和推荐的方式。
优点 提高代码复用性,增加系统灵活性,符合开闭原则。
缺点 增加系统复杂度,可能产生不必要的间接层。
应用场景 整合第三方库、遗留系统,统一多个类的接口。

掌握适配器模式,能让你在处理复杂系统整合和接口兼容性问题时游刃有余。

分享:
扫描分享到社交APP
上一篇
下一篇