揭秘设计模式:一个转接头,竟然能让你的代码更优雅?聊聊适配器模式
你有没有过这样的经历?买了新的电脑,接口和旧显示器不兼容;或者出国旅行,手机充电器插不进当地的插座。这时候,一个转接头就能解决所有烦恼。
在软件开发的世界里,我们也经常遇到类似的问题:你手头有一个功能强大的类,却无法直接被你的新系统调用,因为它俩的接口“说不通”。你是否曾为此犯愁,是修改老代码,还是为新系统妥协?
别急,答案就在一个被誉为“代码界转接头”的设计模式——适配器模式Adapter Pattern里。它能让你在不改动任何老代码的前提下,让两个原本不兼容的接口,完美地协同工作。
什么是适配器模式?
简单来说,适配器模式就是将一个类的接口转换成客户希望的另一个接口,让原本因为接口不兼容而不能一起工作的类,能够和谐共处。
它包含三个核心角色:
- 目标接口 (Target): 这是我们希望使用的接口,也就是我们想要“适配”到的那个规范。
- 被适配者 (Adaptee): 这是那个“不兼容”的类,它就是我们想要利用但又无法直接使用的对象。
- 适配器 (Adapter): 这就是那个“转接头”,它实现了目标接口,但内部持有被适配者的实例,负责将调用者对目标接口的请求,转换成对被适配者方法的调用。
类图
为了更直观地理解这些类的关系,我们可以用一个类图来表示。

关系解析:
WavAdapter实现了MediaPlayer接口。这表明WavAdapter能够被当作一个MediaPlayer来使用。WavAdapter持有一个WavPlayer的实例(通过has a关系)。这是适配器模式的核心,适配器通过这种方式来调用被适配者的方法。AudioPlayer使用MediaPlayer接口。这意味着AudioPlayer的代码只与MediaPlayer接口打交道,而不知道具体的实现类是WavAdapter还是其他播放器。
文末有编码参考
适配器模式在主流框架中的应用
理论听起来很棒,但在每天的业务开发中,适配器模式到底能帮我们解决什么问题呢?除了新老系统集成、对接第三方服务这类场景,其实你每天都在使用的Spring 框架中,适配器模式无处不在。
最经典的例子就是 Spring MVC 的处理器适配器。
我们都知道,Spring MVC 的核心是 DispatcherServlet。当一个 HTTP 请求进来时,它需要找到一个合适的处理器(Controller)来处理这个请求。但是,程序员写 Controller 的方式可能多种多样:
- 有些 Controller 可能实现了特定的接口,比如
Controller。 - 有些 Controller 使用了
@Controller和@RequestMapping注解。 - 还有些可能使用其他自定义的处理器。
如果 DispatcherServlet 要兼容所有这些不同的处理器,它的代码会变得非常复杂,而且每当有新的处理器类型出现,都需要修改它的核心代码。
Spring 的解决方案就是使用处理器适配器 (HandlerAdapter) 。
- 目标接口 (Target): Spring 定义了一个
HandlerAdapter接口,它只有一个核心方法handle()。DispatcherServlet只与这个接口打交道。 - 被适配者 (Adaptee): 就是我们自己编写的各种类型的 Controller。
- 适配器 (Adapter): Spring 提供了多种适配器实现。例如,
SimpleControllerHandlerAdapter用来处理实现了Controller接口的处理器;RequestMappingHandlerAdapter用来处理那些使用了@RequestMapping注解的处理器。
DispatcherServlet 收到请求后,会遍历所有的 HandlerAdapter,询问它们“你能不能处理这个请求对应的处理器?”一旦找到合适的适配器,它就会将请求“转接”给这个适配器,由适配器去调用我们 Controller 中的方法。
这个设计完美地体现了适配器模式的精髓:将不兼容的接口(各种类型的 Controller)转换成统一的接口(HandlerAdapter) 。这使得 Spring 的核心代码 DispatcherServlet 变得极其简单和灵活,可以轻松支持各种新的处理器类型,而无需修改其内部实现。
java编码
为了更好地理解,我们来看一个具体的例子。
假设我们正在开发一个音频播放器,目前只支持 MP3 格式。我们有一个 MediaPlayer 接口,定义了统一的播放规范。
// 目标接口
public interface MediaPlayer {
void play(String audioType, String fileName);
}
现在,我们拿到了一个新需求,需要播放 WAV 文件。我们有一个现成的 WavPlayer 类,但它的接口和我们的 MediaPlayer 不兼容。
// 被适配者:WavPlayer 有自己的播放方法
public class WavPlayer {
public void playWav(String fileName) {
System.out.println("Playing WAV file: " + fileName);
}
}
为了让我们的主程序 AudioPlayer 能够像播放 MP3 一样播放 WAV,我们不能直接修改 WavPlayer 或 MediaPlayer。最好的做法,就是创建一个“转接头”——WavAdapter。
这个适配器需要做两件事:
- 实现我们的
MediaPlayer接口,以便让AudioPlayer能够识别它。 - 持有一个
WavPlayer实例,以便在需要时调用它的playWav方法。
这就是我们的适配器代码:
// 适配器
public class WavAdapter implements MediaPlayer {
private WavPlayer wavPlayer;
public WavAdapter(WavPlayer wavPlayer) {
this.wavPlayer = wavPlayer;
}
@Override
public void play(String audioType, String fileName) {
if (audioType.equalsIgnoreCase("wav")) {
// 在这里,适配器将 play 方法的调用,转换成了对 playWav 的调用
this.wavPlayer.playWav(fileName);
} else {
System.out.println("Invalid audio type: " + audioType);
}
}
}
现在,我们的主程序 AudioPlayer 就可以像下面这样使用了。它只关心 MediaPlayer 这个接口,而将具体的适配工作交给了 WavAdapter。
public class AudioPlayer {
public void play(String audioType, String fileName) {
if (audioType.equalsIgnoreCase("mp3")) {
System.out.println("Playing MP3 file: " + fileName);
} else if (audioType.equalsIgnoreCase("wav")) {
// 通过适配器,我们像播放MP3一样播放WAV
MediaPlayer mediaPlayer = new WavAdapter(new WavPlayer());
mediaPlayer.play(audioType, fileName);
} else {
System.out.println("Invalid audio type: " + audioType);
}
}
public static void main(String[] args) {
AudioPlayer audioPlayer = new AudioPlayer();
audioPlayer.play("mp3", "beyond_the_horizon.mp3");
audioPlayer.play("wav", "my_new_song.wav");
}
}
适配器模式的真正价值
适配器模式的真正力量在于,它提供了一种优雅的方式来处理接口不兼容的问题,让我们能够:
- 实现开闭原则: 当需要支持新的服务时,我们只需要新增一个适配器,而不需要修改任何已有的核心代码。这使得系统对扩展是开放的,对修改是封闭的。
- 实现责任分离: 每个类都只做自己的事。
WavPlayer只管播放 WAV,AudioPlayer只管调用MediaPlayer接口。而适配器,则只负责接口的转换。这让代码结构清晰,易于维护。 - 新老系统无缝对接: 它让那些原本无法协作的模块,能够通过一个“转接头”和谐共处。
所以,下次当你面对两个不兼容的接口时,不妨停下来想一想:我是否需要一个“转接头”来让它们握手言和?


被折叠的 条评论
为什么被折叠?



