结构型模式:适配器模式
什么是适配器模式?
适配器模式(Adapter Pattern)是一种常用的结构型设计模式,它的主要作用是将一个类的接口转换成客户端期望的另一个接口。就像现实生活中的各种转接头一样,适配器模式使得原本因接口不兼容而无法一起工作的类能够协同合作。
想象一下,你有一个美国制造的电器,插头是两孔扁头,但你在中国旅行,插座是三孔。这时,你需要一个电源转换器(适配器)来解决这个问题。在软件设计中,适配器模式正是解决这类"接口不匹配"问题的优雅解决方案。
适配器模式也常被称为包装器(Wrapper)模式,因为它就像一个包装纸,将原本不兼容的接口"包装"起来,使其能与目标接口兼容。
适配器模式的类型
适配器模式主要有两种实现方式:
1. 类适配器
类适配器通过多重继承(在Java中通过继承被适配类并实现目标接口)实现适配。类适配器使用的是继承机制。
2. 对象适配器
对象适配器通过组合方式实现适配,即在适配器中持有被适配对象的实例。对象适配器使用的是组合机制。
适配器模式的结构
下面是适配器模式的UML类图,它清晰地展示了这种设计模式的结构:
在这个结构中,Target(目标接口)是客户端所期望的接口,Adaptee(被适配者)是需要被适配的类或接口,而Adapter(适配器)则是将Adaptee转换成Target的类。
适配器模式的基本实现
对象适配器模式实现
对象适配器使用组合方式,将被适配的类的实例包装在适配器中:
// 目标接口:客户端期望使用的接口
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;}// 实现目标接口的方法,在内部调用被适配对象的方法@Overridepublic 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()
方法时,适配器会将调用转发给Adaptee
的specificRequest()
方法,从而实现接口的适配。这样,客户端就能够通过目标接口间接使用被适配类的功能,而不必关心它们之间的接口差异。
类适配器模式实现
类适配器使用继承方式,同时继承被适配类并实现目标接口:
// 目标接口:客户端期望使用的接口
public interface Target {void request(); // 客户端期望调用的方法
}// 被适配的类:已经存在的、接口不兼容的类
public class Adaptee {// 被适配类的方法与目标接口不兼容public void specificRequest() {System.out.println("适配者的特殊请求方法");}
}// 类适配器:通过继承被适配类并实现目标接口
public class ClassAdapter extends Adaptee implements Target {// 实现目标接口的方法@Overridepublic 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()
方法时,适配器可以直接调用继承自Adaptee
的specificRequest()
方法。类适配器的优点是实现更加简洁,但由于Java只支持单继承,这种方式会受到继承体系的限制,灵活性不如对象适配器。
实际应用示例:电源适配器
让我们用一个现实世界中的例子——电源适配器——来展示适配器模式的应用:
// 美国标准电源接口(110V)
interface USPowerSource {void supplyPowerAt110V(); // 提供110V电力的方法
}// 欧洲标准电源接口(220V)
interface EUPowerSource {void supplyPowerAt220V(); // 提供220V电力的方法
}// 美国电源实现
class USPowerSupply implements USPowerSource {@Overridepublic void supplyPowerAt110V() {System.out.println("提供110V的电力");}
}// 欧洲电源实现
class EUPowerSupply implements EUPowerSource {@Overridepublic 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;}@Overridepublic 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;}// 实现欧洲电源接口方法@Overridepublic 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();}
}
运行结果:
=== 在美国使用欧洲电器 ===
欧洲设备启动中...
适配器转换中: 110V -> 220V
提供110V的电力
电压转换完成,输出220V
欧洲设备工作正常!=== 在欧洲使用欧洲电器(无需适配器)===
欧洲设备启动中...
提供220V的电力
欧洲设备工作正常!
这个例子模拟了现实世界中的电源适配器场景。我们有美国标准的110V电源和欧洲标准的220V电源,而欧洲电子设备需要220V电源才能正常工作。当我们在美国(只有110V电源)使用欧洲设备时,需要一个电源适配器来进行转换。适配器PowerAdapter
在内部调用美国电源的方法,然后进行必要的转换,最终提供欧洲设备所需的220V电源。这样,欧洲设备就可以通过适配器在美国使用了。而在欧洲使用欧洲设备时,由于电源标准匹配,就不需要适配器了。
这个例子非常直观地展示了适配器的作用:让不兼容的接口(110V和220V)能够协同工作,就像现实中的电源转换器一样。
实际应用示例:旧系统集成
在企业应用中,系统集成是适配器模式的一个典型应用场景。下面我们来看一个旧系统集成的例子:
// 旧的用户信息系统接口
class LegacyUserSystem {// 旧系统返回格式化的字符串public String fetchUserData(String userId) {// 模拟从旧系统获取用户数据,格式为:USER:ID:姓名:性别:年龄:地址return String.format("USER:%s:张三:男:30:北京", userId);}
}// 新系统期望的用户模型
class User {private String id; // 用户IDprivate 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方法,方便输出用户信息@Overridepublic 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;}// 实现新接口的获取用户方法@Overridepublic 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);}// 实现新接口的保存用户方法@Overridepublic 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的InputStreamReader
和OutputStreamWriter
类就是典型的适配器模式应用。InputStreamReader
将字节流(InputStream)适配为字符流(Reader),解决了字节与字符的转换问题。同样,OutputStreamWriter
将字节输出流(OutputStream)适配为字符输出流(Writer)。这样,开发者就可以用统一的字符流接口处理不同编码的输入输出,而不必关心底层的字节处理细节。
Arrays.asList()
方法也是一个适配器的例子,它将数组适配为List集合,使数组可以使用集合的方法。通过这个适配器,我们可以将一个固定长度的数组转换为一个List接口的对象,从而能够使用集合框架提供的丰富功能。
另外,Collections.list()
将旧式的Enumeration适配为现代的List集合,这是为了兼容早期Java版本的代码而设计的适配器。Java XML绑定API中的XmlAdapter
则是在XML数据与Java对象之间进行转换的适配器,它使得XML序列化和反序列化过程更加灵活可控。
适配器模式的优缺点
优点
优点 | 说明 |
---|---|
增加了类的透明性 | 客户端通过目标接口与适配器交互,不需要了解适配器背后的实现细节 |
提高了类的复用性 | 通过适配器,原本不兼容的类可以在新环境中得到复用 |
灵活性和可扩展性 | 可以引入更多适配器支持更多类型的适配者,系统更易于扩展 |
遵循开闭原则 | 无需修改现有代码,通过添加适配器来满足新需求 |
结构清晰 | 适配器的职责明确,系统结构清晰易于理解和维护 |
缺点
缺点 | 说明 |
---|---|
增加系统复杂度 | 引入适配器会增加系统中的类和间接层,使系统略微复杂化 |
可能需要修改多个适配器 | 当适配者接口发生变化时,所有相关适配器可能都需要更新 |
可能导致性能损失 | 通过中间层转换可能带来轻微的性能损失 |
调试复杂度增加 | 当出现问题时,可能需要调试适配层而非业务层,增加排错难度 |
最后的一丢丢总结
适配器模式是一种强大的结构型设计模式,它能够将不兼容的接口转换成客户端期望的接口,让原本无法一起工作的类能够协同工作。通过适配器模式,我们可以集成新系统和遗留系统,重用现有的类,使第三方库和现有系统无缝协作,以及在不修改现有代码的情况下满足新的接口需求。
适配器模式有两种主要实现方式:类适配器(通过继承)和对象适配器(通过组合)。在实际应用中,对象适配器更为常用,因为它更加灵活且符合"组合优于继承"的设计原则。虽然适配器模式增加了一定的间接性和复杂性,但它提供的接口转换能力使得系统更加灵活、可扩展,特别是在系统集成和演化过程中,适配器模式能够发挥重要作用。
当你面临接口不兼容的问题,或需要集成多个系统时,不妨考虑使用适配器模式。它就像现实生活中的转接头一样,能够让不兼容的部分和谐地工作在一起,让系统更加灵活和可维护。