1. 概念
- 适配器模式是一种结构型设计模式,它的主要目的是在不改变现有代码的情况下,使不兼容的接口之间能够正常工作。
2. 原理结构图
类适配器
对象适配器
原理结构图说明
- Target(目标接口):这是我们期望使用的接口,客户端代码将针对此接口进行编程。
- Adaptee(被适配者):已有的类或接口,它的功能需要被利用,但接口与目标接口不兼容。
- Adapter(适配器):这是模式的核心,它实现了目标接口并包含对被适配者类的引用。通过适配器,可以将被适配者的功能以目标接口的形式提供给客户端。
3. 代码示例
3.1 示例1
- 类适配器代码示例
- 假设有一个旧的类LegacyPrinter,它有一个print方法用于打印文本。现在,想要让这个打印机能够兼容新的打印接口NewPrintable,该接口有一个printWithFormat方法,它接受格式化的字符串进行打印。
// 新的打印接口
interface NewPrintable {
void printWithFormat(String formattedText);
}
// 旧的打印机类
class LegacyPrinter {
public void print(String text) {
System.out.println("Legacy Printer: " + text);
}
}
// 类适配器,继承自LegacyPrinter并实现NewPrintable接口
class LegacyPrinterAdapter extends LegacyPrinter implements NewPrintable {
@Override
public void printWithFormat(String formattedText) {
// 假设我们有一个方法可以将格式化文本转换为旧打印机能理解的格式
String convertedText = convertFormattedTextToLegacy(formattedText);
// 调用父类的print方法打印转换后的文本
super.print(convertedText);
}
// 假设的转换方法,实际应用中需要具体实现转换逻辑
private String convertFormattedTextToLegacy(String formattedText) {
// 这里只是简单返回,实际中需要进行格式转换
return formattedText;
}
}
public class Client {
public static void main(String[] args) {
// 创建类适配器实例
NewPrintable adapter = new LegacyPrinterAdapter();
// 使用新接口调用打印方法,实际上是调用了LegacyPrinter的print方法
adapter.printWithFormat("Formatted text for printing");
}
}
3.2 示例二
- 对象适配器代码示例
- 有一个旧的音乐播放器接口OldMusicPlayer,它有一个playOldFormat方法来播放老式的音乐格式。现在想要让这个播放器能够兼容新的音乐格式,但又不想直接修改旧的音乐播放器代码。可以通过对象适配器模式来创建一个新的接口NewMusicPlayer和一个适配器类MusicPlayerAdapter,使得旧的音乐播放器可以通过这个适配器来播放新格式的音乐。
// 旧的音乐播放器接口
interface OldMusicPlayer {
void playOldFormat(String fileName);
}
// 旧的音乐播放器实现类
class OldMusicPlayerImpl implements OldMusicPlayer {
@Override
public void playOldFormat(String fileName) {
System.out.println("Playing old format music: " + fileName);
}
}
// 新的音乐播放器接口
interface NewMusicPlayer {
void playNewFormat(String fileName);
}
// 音乐播放器适配器类
class MusicPlayerAdapter implements NewMusicPlayer {
private OldMusicPlayer oldPlayer;
public MusicPlayerAdapter(OldMusicPlayer oldPlayer) {
this.oldPlayer = oldPlayer;
}
@Override
public void playNewFormat(String fileName) {
// 假设有一个方法可以将新格式转换为旧格式,这里简化处理
String convertedFileName = convertNewFormatToOld(fileName);
// 调用旧播放器的playOldFormat方法来播放转换后的音乐
oldPlayer.playOldFormat(convertedFileName);
}
// 假设的转换方法,实际应用中需要具体实现转换逻辑
private String convertNewFormatToOld(String newFormatFileName) {
// 这里只是简单返回,实际中需要进行格式转换
return newFormatFileName;
}
}
public class Client {
public static void main(String[] args) {
// 创建旧的音乐播放器实例
OldMusicPlayer oldPlayer = new OldMusicPlayerImpl();
// 创建适配器实例,传入旧的音乐播放器
NewMusicPlayer adapter = new MusicPlayerAdapter(oldPlayer);
// 使用新接口调用播放方法,实际上是调用了旧播放器的playOldFormat方法
adapter.playNewFormat("new_music_file.mp4");
}
}
3.3 示例三(扩展)
- 双向适配器模式(Bidirectional Adapter Pattern)允许两个原本不兼容的接口在不修改它们的情况下进行通信。该模式通过创建一个适配器类,该类同时包含对目标接口和被适配接口的引用,使得适配者可以通过它调用目标接口中的方法,同时目标接口也可以通过它调用适配者类中的方法。这样,既可以将目标接口适配成被适配者,也可以将被适配者适配成目标接口,从而实现了两个接口之间的双向通信。
- 双向适配器代码示例
- 有两个不兼容的接口:OldMediaDevice和NewMediaDevice。想要让使用OldMediaDevice的客户端能够使用NewMediaDevice的功能,反之亦然。
// 旧的媒体设备接口
interface OldMediaDevice {
void playOldFormat();
void pauseOldFormat();
}
// 新的媒体设备接口
interface NewMediaDevice {
void playNewFormat();
void pauseNewFormat();
}
// OldMediaDevice的实现类
class OldMediaDeviceImpl implements OldMediaDevice {
@Override
public void playOldFormat() {
System.out.println("Playing old format media");
}
@Override
public void pauseOldFormat() {
System.out.println("Pausing old format media");
}
}
// NewMediaDevice的实现类
class NewMediaDeviceImpl implements NewMediaDevice {
@Override
public void playNewFormat() {
System.out.println("Playing new format media");
}
@Override
public void pauseNewFormat() {
System.out.println("Pausing new format media");
}
}
// 双向适配器
class MediaDeviceAdapter implements OldMediaDevice, NewMediaDevice {
private OldMediaDevice oldDevice;
private NewMediaDevice newDevice;
public MediaDeviceAdapter(OldMediaDevice oldDevice) {
this.oldDevice = oldDevice;
}
public MediaDeviceAdapter(NewMediaDevice newDevice) {
this.newDevice = newDevice;
}
// 实现OldMediaDevice接口的方法,转换为NewMediaDevice的调用
@Override
public void playOldFormat() {
if (newDevice != null) {
newDevice.playNewFormat();
}
}
@Override
public void pauseOldFormat() {
if (newDevice != null) {
newDevice.pauseNewFormat();
}
}
// 实现NewMediaDevice接口的方法,转换为OldMediaDevice的调用
@Override
public void playNewFormat() {
if (oldDevice != null) {
oldDevice.playOldFormat();
}
}
@Override
public void pauseNewFormat() {
if (oldDevice != null) {
oldDevice.pauseOldFormat();
}
}
}
public class Client {
public static void main(String[] args) {
// 创建OldMediaDevice的实现
OldMediaDevice oldDevice = new OldMediaDeviceImpl();
// 使用OldMediaDevice创建适配器,并作为NewMediaDevice使用
NewMediaDevice adapterAsNewDevice = new MediaDeviceAdapter(oldDevice);
adapterAsNewDevice.playNewFormat(); // 实际调用OldMediaDevice的playOldFormat
adapterAsNewDevice.pauseNewFormat(); // 实际调用OldMediaDevice的pauseOldFormat
// 创建NewMediaDevice的实现
NewMediaDevice newDevice = new NewMediaDeviceImpl();
// 使用NewMediaDevice创建适配器,并作为OldMediaDevice使用
OldMediaDevice adapterAsOldDevice = new MediaDeviceAdapter(newDevice);
adapterAsOldDevice.playOldFormat(); // 实际调用NewMediaDevice的playNewFormat
adapterAsOldDevice.pauseOldFormat(); // 实际调用NewMediaDevice的pauseNewFormat
}
}
4. 优缺点
- 主要作用
- 连接两个不兼容的接口
- 优点
- 提高代码复用性:通过使用适配器模式,可以重用已有的功能,无需修改源代码。
- 更好的扩展性:适配器模式可以使得原本不兼容的类协同工作,增加了系统的可扩展性。
- 解耦:适配器模式使得客户端与实现类的耦合度降低,提高了系统的灵活性和可维护性。
- 符合开闭原则:适配器模式对修改关闭,对扩展开放,即在不需要修改现有代码的情况下,通过增加新的适配器类来支持新的接口。
- 缺点
- 可能增加系统的复杂性:引入适配器模式可能会增加系统的类和接口的数量,使得系统变得更加复杂。
- 可能引入性能损耗:由于适配器模式涉及到接口转换,可能会引入一定的性能损耗。尤其是在处理大量数据时,这种损耗可能更加明显。
- 过度使用可能导致代码冗余:如果过度使用适配器模式,可能会导致代码冗余和重复。每个需要适配的接口都可能需要创建一个新的适配器类,这可能会使得代码库变得庞大而难以管理。
5. 应用场景
- 系统扩展:当系统需要引入新的类或接口进行扩展,而原有的客户端无法直接使用这些新的类或接口时,适配器模式可以将新的接口转换成客户端所期望的接口,使得客户端能够正常利用这些新扩展的功能。
- 接口转换:在不同的组件或系统间,可能采用不同的接口定义,而这些接口定义可能并不兼容。此时,适配器模式可以将一个类的接口转换成客户端所期望的另一种接口,使它们能够协同工作。
- 设计缺陷补救:适配器模式可以作为一种补偿措施,用来补救设计上的缺陷,尤其是在设计初期未能预见到接口不兼容问题时。
- 统一多个类的接口设计:某个功能的实现可能依赖于多个外部系统或类,而这些类或系统的接口可能各不相同。通过适配器模式,可以将这些不同的接口适配为统一的接口定义,使得代码逻辑可以复用。
- 兼容老版本接口:在进行版本升级时,对于需要废弃的接口,可以通过适配器模式暂时保留它们,并将内部实现逻辑委托给新的接口实现,从而实现接口的平滑过渡。
- 不同数据格式转换:在处理不同数据源或格式时,适配器模式可以用来转换数据格式,使其符合系统要求。
- 第三方库集成:当需要将第三方库集成到现有系统中,但两者的接口不匹配时,适配器模式可以用来实现平滑集成。
6. JDK中的使用
- IO类库:在Java的IO类库中,java.io.InputStreamReader、java.io.StringReader、java.io.OutputStreamWriter和java.io.ByteArrayInputStream等都是适配器模式的应用实例。这些类通过将一个不兼容的接口转换为另一个客户端期望的接口,从而使得原本不兼容的类可以一起工作。
- 并发包:在Java的并发包(JUC)中,FutureTask类也使用了适配器模式。它允许将Runnable或Callable任务包装成一个Future,这样可以使用Executor来执行这些任务,并提供了一种检查任务是否完成以及获取结果的方式。
- 集合框架:在Java的集合框架中,适配器模式也被用来提供不同类型集合之间的转换,例如Collections.list()方法可以将任何Collection转换为List接口的实例。
7. 注意事项
- 接口转换的目的:适配器模式主要用于解决接口不兼容的问题,而不是为了解决功能缺陷或逻辑问题。在使用前,应确保确实是接口问题导致了协同工作的困难。
- 不要过度使用:虽然适配器模式能解决接口不兼容的问题,但过度使用可能会导致代码复杂性的增加。在设计中应尽量避免引入不必要的适配器,仅在接口转换确实必要的情况下使用。
- 考虑性能损耗:适配器模式在接口转换的过程中可能会引入一定的性能损耗。在处理大量数据或性能敏感的场景中,应仔细评估适配器模式对性能的影响,并考虑是否有更优的解决方案。
- 考虑未来扩展性:适配器模式通常用于解决正在服役的项目中的问题,特别是在系统扩展和接口不兼容的情况下。因此,在设计适配器时,应考虑到未来的扩展性和维护性,确保适配器能够适应未来可能的变化。
- 仔细处理异常和错误:适配器模式可能涉及到多个组件或系统的交互,因此应仔细处理可能出现的异常和错误情况。确保适配器能够适当地传递或处理异常,以避免对整个系统造成影响。