结构型模式:适配器模式

什么是适配器模式?

适配器模式(Adapter Pattern)是一种常用的结构型设计模式,它的主要作用是将一个类的接口转换成客户端期望的另一个接口。就像现实生活中的各种转接头一样,适配器模式使得原本因接口不兼容而无法一起工作的类能够协同合作。

想象一下,你有一个美国制造的电器,插头是两孔扁头,但你在中国旅行,插座是三孔。这时,你需要一个电源转换器(适配器)来解决这个问题。在软件设计中,适配器模式正是解决这类"接口不匹配"问题的优雅解决方案。

适配器模式也常被称为包装器(Wrapper)模式,因为它就像一个包装纸,将原本不兼容的接口"包装"起来,使其能与目标接口兼容。

适配器模式的类型

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

1. 类适配器

类适配器通过多重继承(在Java中通过继承被适配类并实现目标接口)实现适配。类适配器使用的是继承机制。

2. 对象适配器

对象适配器通过组合方式实现适配,即在适配器中持有被适配对象的实例。对象适配器使用的是组合机制。

适配器模式的结构

下面是适配器模式的UML类图,它清晰地展示了这种设计模式的结构:

在这个结构中,Target(目标接口)是客户端所期望的接口,Adaptee(被适配者)是需要被适配的类或接口,而Adapter(适配器)则是将Adaptee转换成Target的类。

适配器模式的基本实现

对象适配器模式实现

对象适配器使用组合方式,将被适配的类的实例包装在适配器中:

java 复制代码
// 目标接口:客户端期望使用的接口
public interface Target {
    void request();  // 客户端期望调用的方法
}

// 被适配的类:已经存在的、接口不兼容的类
public class Adaptee {
    // 被适配类的方法与目标接口不兼容
    public void specificRequest() {
        System.out.println("适配者的特殊请求方法");
    }
}

// 对象适配器类:通过组合方式包含被适配对象
public class ObjectAdapter implements Target {
    // 持有一个被适配类的引用
    private Adaptee adaptee;
    
    // 通过构造函数注入被适配对象
    public ObjectAdapter(Adaptee adaptee) {
        this.adaptee = adaptee;
    }
    
    // 实现目标接口的方法,在内部调用被适配对象的方法
    @Override
    public void request() {
        System.out.println("对象适配器: 转换请求");
        // 调用被适配对象的方法完成真正的功能
        adaptee.specificRequest();
    }
}

// 客户端代码
public class Client {
    public static void main(String[] args) {
        // 创建被适配对象
        Adaptee adaptee = new Adaptee();
        // 创建适配器对象,将被适配对象传入
        Target adapter = new ObjectAdapter(adaptee);
        
        System.out.println("客户端通过适配器调用请求...");
        // 客户端通过目标接口调用方法,实际上最终会调用被适配对象的方法
        adapter.request();
    }
}

在这个对象适配器实现中,我们定义了一个Target接口作为客户端期望使用的接口,而Adaptee是一个已经存在但接口不兼容的类。ObjectAdapter充当适配器角色,它实现了Target接口,同时在内部持有一个Adaptee实例。当客户端调用适配器的request()方法时,适配器会将调用转发给AdapteespecificRequest()方法,从而实现接口的适配。这样,客户端就能够通过目标接口间接使用被适配类的功能,而不必关心它们之间的接口差异。

类适配器模式实现

类适配器使用继承方式,同时继承被适配类并实现目标接口:

java 复制代码
// 目标接口:客户端期望使用的接口
public interface Target {
    void request();  // 客户端期望调用的方法
}

// 被适配的类:已经存在的、接口不兼容的类
public class Adaptee {
    // 被适配类的方法与目标接口不兼容
    public void specificRequest() {
        System.out.println("适配者的特殊请求方法");
    }
}

// 类适配器:通过继承被适配类并实现目标接口
public class ClassAdapter extends Adaptee implements Target {
    // 实现目标接口的方法
    @Override
    public void request() {
        System.out.println("类适配器: 转换请求");
        // 直接调用父类(被适配类)的方法
        specificRequest();
    }
}

// 客户端代码
public class Client {
    public static void main(String[] args) {
        // 使用类适配器
        Target adapter = new ClassAdapter();
        
        System.out.println("客户端通过适配器调用请求...");
        // 客户端通过目标接口调用方法
        adapter.request();
    }
}

类适配器与对象适配器的主要区别在于实现方式。类适配器通过继承Adaptee类,直接获得了被适配类的方法,而无需像对象适配器那样持有被适配对象的引用。ClassAdapter同时继承了Adaptee类并实现了Target接口,当客户端调用request()方法时,适配器可以直接调用继承自AdapteespecificRequest()方法。类适配器的优点是实现更加简洁,但由于Java只支持单继承,这种方式会受到继承体系的限制,灵活性不如对象适配器。

实际应用示例:电源适配器

让我们用一个现实世界中的例子------电源适配器------来展示适配器模式的应用:

java 复制代码
// 美国标准电源接口(110V)
interface USPowerSource {
    void supplyPowerAt110V();  // 提供110V电力的方法
}

// 欧洲标准电源接口(220V)
interface EUPowerSource {
    void supplyPowerAt220V();  // 提供220V电力的方法
}

// 美国电源实现
class USPowerSupply implements USPowerSource {
    @Override
    public void supplyPowerAt110V() {
        System.out.println("提供110V的电力");
    }
}

// 欧洲电源实现
class EUPowerSupply implements EUPowerSource {
    @Override
    public void supplyPowerAt220V() {
        System.out.println("提供220V的电力");
    }
}

// 电子设备接口(期望220V)
interface ElectronicDevice {
    void powerOn();  // 设备开机方法
}

// 欧洲电子设备(需要220V电源)
class EuropeanDevice implements ElectronicDevice {
    private EUPowerSource powerSource;  // 依赖欧洲标准电源
    
    public EuropeanDevice(EUPowerSource powerSource) {
        this.powerSource = powerSource;
    }
    
    @Override
    public void powerOn() {
        System.out.println("欧洲设备启动中...");
        // 使用欧洲标准电源
        powerSource.supplyPowerAt220V();
        System.out.println("欧洲设备工作正常!");
    }
}

// 电源适配器:将110V转为220V(对象适配器模式)
class PowerAdapter implements EUPowerSource {
    private USPowerSource usPowerSource;  // 持有美国电源对象
    
    public PowerAdapter(USPowerSource usPowerSource) {
        this.usPowerSource = usPowerSource;
    }
    
    // 实现欧洲电源接口方法
    @Override
    public void supplyPowerAt220V() {
        System.out.println("适配器转换中: 110V -> 220V");
        // 调用美国电源方法
        usPowerSource.supplyPowerAt110V();
        System.out.println("电压转换完成,输出220V");
    }
}

// 测试代码
public class PowerAdapterDemo {
    public static void main(String[] args) {
        // 在美国使用欧洲设备
        System.out.println("=== 在美国使用欧洲电器 ===");
        
        // 创建美国电源
        USPowerSource usPower = new USPowerSupply();
        
        // 创建适配器(将美国电源适配为欧洲电源)
        EUPowerSource adapter = new PowerAdapter(usPower);
        
        // 创建欧洲设备并使用适配器供电
        ElectronicDevice europeanDevice = new EuropeanDevice(adapter);
        
        // 启动设备
        europeanDevice.powerOn();
        
        System.out.println("\n=== 在欧洲使用欧洲电器(无需适配器)===");
        
        // 欧洲电源
        EUPowerSource euPower = new EUPowerSupply();
        
        // 直接使用欧洲电源
        ElectronicDevice deviceInEurope = new EuropeanDevice(euPower);
        deviceInEurope.powerOn();
    }
}

运行结果

diff 复制代码
=== 在美国使用欧洲电器 ===
欧洲设备启动中...
适配器转换中: 110V -> 220V
提供110V的电力
电压转换完成,输出220V
欧洲设备工作正常!

=== 在欧洲使用欧洲电器(无需适配器)===
欧洲设备启动中...
提供220V的电力
欧洲设备工作正常!

这个例子模拟了现实世界中的电源适配器场景。我们有美国标准的110V电源和欧洲标准的220V电源,而欧洲电子设备需要220V电源才能正常工作。当我们在美国(只有110V电源)使用欧洲设备时,需要一个电源适配器来进行转换。适配器PowerAdapter在内部调用美国电源的方法,然后进行必要的转换,最终提供欧洲设备所需的220V电源。这样,欧洲设备就可以通过适配器在美国使用了。而在欧洲使用欧洲设备时,由于电源标准匹配,就不需要适配器了。

这个例子非常直观地展示了适配器的作用:让不兼容的接口(110V和220V)能够协同工作,就像现实中的电源转换器一样。

实际应用示例:旧系统集成

在企业应用中,系统集成是适配器模式的一个典型应用场景。下面我们来看一个旧系统集成的例子:

java 复制代码
// 旧的用户信息系统接口
class LegacyUserSystem {
    // 旧系统返回格式化的字符串
    public String fetchUserData(String userId) {
        // 模拟从旧系统获取用户数据,格式为:USER:ID:姓名:性别:年龄:地址
        return String.format("USER:%s:张三:男:30:北京", userId);
    }
}

// 新系统期望的用户模型
class User {
    private String id;        // 用户ID
    private String name;      // 用户姓名
    private String gender;    // 性别
    private int age;          // 年龄
    private String address;   // 地址
    
    // 构造函数
    public User(String id, String name, String gender, int age, String address) {
        this.id = id;
        this.name = name;
        this.gender = gender;
        this.age = age;
        this.address = address;
    }
    
    // 重写toString方法,方便输出用户信息
    @Override
    public String toString() {
        return "User{" +
                "id='" + id + '\'' +
                ", name='" + name + '\'' +
                ", gender='" + gender + '\'' +
                ", age=" + age +
                ", address='" + address + '\'' +
                '}';
    }
    
    // Getters 和 Setters省略
}

// 新的用户服务接口(新系统期望的接口)
interface UserService {
    User getUser(String userId);     // 获取用户信息
    void saveUser(User user);        // 保存用户信息
}

// 适配器:将旧系统集成到新系统(对象适配器模式)
class UserSystemAdapter implements UserService {
    private LegacyUserSystem legacySystem;  // 持有旧系统的引用
    
    public UserSystemAdapter(LegacyUserSystem legacySystem) {
        this.legacySystem = legacySystem;
    }
    
    // 实现新接口的获取用户方法
    @Override
    public User getUser(String userId) {
        // 从旧系统获取数据
        String userData = legacySystem.fetchUserData(userId);
        
        // 解析旧系统返回的字符串数据并转换为User对象
        String[] parts = userData.split(":");
        if (parts.length < 5) {
            throw new RuntimeException("无效的用户数据格式");
        }
        
        String id = parts[1];
        String name = parts[2];
        String gender = parts[3];
        int age = Integer.parseInt(parts[4]);
        String address = parts[5];
        
        // 返回新系统能理解的用户对象
        return new User(id, name, gender, age, address);
    }
    
    // 实现新接口的保存用户方法
    @Override
    public void saveUser(User user) {
        // 这里可以实现将新系统User对象保存到旧系统的逻辑
        System.out.println("将用户保存到旧系统:" + user);
        // 在实际应用中,应当调用旧系统的API来保存用户
    }
}

// 新系统的用户管理类
class UserManager {
    private UserService userService;  // 依赖用户服务接口
    
    public UserManager(UserService userService) {
        this.userService = userService;
    }
    
    // 显示用户信息的方法
    public void displayUserInfo(String userId) {
        try {
            // 通过用户服务获取用户信息
            User user = userService.getUser(userId);
            System.out.println("用户信息:" + user);
        } catch (Exception e) {
            System.out.println("获取用户信息失败:" + e.getMessage());
        }
    }
}

// 测试代码
public class SystemIntegrationDemo {
    public static void main(String[] args) {
        // 创建旧系统实例
        LegacyUserSystem legacySystem = new LegacyUserSystem();
        
        // 创建适配器,将旧系统适配到新接口
        UserService adapter = new UserSystemAdapter(legacySystem);
        
        // 新系统使用适配后的服务
        UserManager userManager = new UserManager(adapter);
        
        // 通过新系统接口访问旧系统数据
        System.out.println("=== 使用适配器访问旧系统 ===");
        userManager.displayUserInfo("12345");
    }
}

在这个系统集成的例子中,我们有一个旧的用户信息系统LegacyUserSystem,它以字符串格式返回用户数据。而新系统需要使用结构化的User对象。为了解决这个接口不匹配的问题,我们创建了一个适配器UserSystemAdapter,它实现了新系统期望的UserService接口,同时在内部调用旧系统的API。

适配器负责将旧系统返回的字符串数据解析并转换为新系统需要的User对象。这样,新系统的UserManager就可以通过UserService接口与适配器交互,而不需要知道后面实际上是旧系统在提供数据。通过这种方式,适配器模式使得系统集成变得优雅且松耦合,新系统不需要直接适应旧系统的接口,而是通过适配器间接地使用旧系统的功能。

适配器模式在Java标准库中的应用

Java标准库中有许多适配器模式的例子,了解这些例子有助于我们理解适配器模式在实际开发中的应用:

Java的InputStreamReaderOutputStreamWriter类就是典型的适配器模式应用。InputStreamReader将字节流(InputStream)适配为字符流(Reader),解决了字节与字符的转换问题。同样,OutputStreamWriter将字节输出流(OutputStream)适配为字符输出流(Writer)。这样,开发者就可以用统一的字符流接口处理不同编码的输入输出,而不必关心底层的字节处理细节。

Arrays.asList()方法也是一个适配器的例子,它将数组适配为List集合,使数组可以使用集合的方法。通过这个适配器,我们可以将一个固定长度的数组转换为一个List接口的对象,从而能够使用集合框架提供的丰富功能。

另外,Collections.list()将旧式的Enumeration适配为现代的List集合,这是为了兼容早期Java版本的代码而设计的适配器。Java XML绑定API中的XmlAdapter则是在XML数据与Java对象之间进行转换的适配器,它使得XML序列化和反序列化过程更加灵活可控。

适配器模式的优缺点

优点

优点 说明
增加了类的透明性 客户端通过目标接口与适配器交互,不需要了解适配器背后的实现细节
提高了类的复用性 通过适配器,原本不兼容的类可以在新环境中得到复用
灵活性和可扩展性 可以引入更多适配器支持更多类型的适配者,系统更易于扩展
遵循开闭原则 无需修改现有代码,通过添加适配器来满足新需求
结构清晰 适配器的职责明确,系统结构清晰易于理解和维护

缺点

缺点 说明
增加系统复杂度 引入适配器会增加系统中的类和间接层,使系统略微复杂化
可能需要修改多个适配器 当适配者接口发生变化时,所有相关适配器可能都需要更新
可能导致性能损失 通过中间层转换可能带来轻微的性能损失
调试复杂度增加 当出现问题时,可能需要调试适配层而非业务层,增加排错难度

最后的一丢丢总结

适配器模式是一种强大的结构型设计模式,它能够将不兼容的接口转换成客户端期望的接口,让原本无法一起工作的类能够协同工作。通过适配器模式,我们可以集成新系统和遗留系统,重用现有的类,使第三方库和现有系统无缝协作,以及在不修改现有代码的情况下满足新的接口需求。

适配器模式有两种主要实现方式:类适配器(通过继承)和对象适配器(通过组合)。在实际应用中,对象适配器更为常用,因为它更加灵活且符合"组合优于继承"的设计原则。虽然适配器模式增加了一定的间接性和复杂性,但它提供的接口转换能力使得系统更加灵活、可扩展,特别是在系统集成和演化过程中,适配器模式能够发挥重要作用。

当你面临接口不兼容的问题,或需要集成多个系统时,不妨考虑使用适配器模式。它就像现实生活中的转接头一样,能够让不兼容的部分和谐地工作在一起,让系统更加灵活和可维护。

相关推荐
hope_wisdom22 分钟前
实战设计模式之备忘录模式
设计模式·系统架构·软件工程·备忘录模式·架构设计
xxy!25 分钟前
Spring 框架中用到的设计模式
java·spring·设计模式
Leaf吧1 小时前
java 设计模式 原型模式
java·设计模式·原型模式
歡進1 小时前
🔥 每个故事都是一种设计模式
前端·javascript·设计模式
callJJ7 小时前
多线程编程的简单案例——单例模式[多线程编程篇(3)]
java·jvm·设计模式·面试·java-ee·软件工程·个人开发
努力的搬砖人.9 小时前
常见设计模式
设计模式
渊渟岳10 小时前
开源项目YtyMark文本编辑器--UI界面相关功能(关于设计模式的实战运用)
java·设计模式
A.lways14 小时前
【软考-系统架构设计师】设计模式三大类型解析
设计模式·系统架构