基本概念

适配器模式

结构型设计模式,将一个类的接口转换成客户期望的另一个接口。适配器让原本接口不兼容的类可以合作。它通过中间层(适配器)实现不同接口的转换,类似于现实中的电源适配器(如Type-CUSB接口)

核心结构

  1. 目标接口(Target):客户端期望的接口;
  2. 适配者(``Adaptee`):需要被转换的接口,现有接口与目标接口不兼容;
  3. 适配器(Adapter):连接目标接口和适配者的中间层,实现接口转换。

两种实现方式

  1. 对象适配器(推荐):通过组合(聚合)适配者实现接口转换,符合OOP组合复用原则;
  2. 类适配器:通过继承适配者实现接口转换(Java中因单继承限制较少使用),不必重新实现整个被适配者,需要时也可覆盖被适配者的行为。

外观模式

结构型设计模式,为子系统的一组接口提供了一个统一的接口。外观模式定义了一个更高级别的接口,使得子系统更容易使用。

核心结构

  1. 外观类(Facade):提供简单统一的接口,封装子系统的复杂交互。
  2. 子系统类(Subsystem Classes):实现系统的核心功能,彼此可能有复杂的交互。
  3. 客户端(Client):通过外观类访问子系统功能。

示例说明

场景一:设计一个音频播放器系统,需要支持播放MP3格式(已有接口)和WAV格式(新接口),但播放器仅支持MP3接口。

  1. 定义目标接口(Target
1
2
3
4
5
6

// 目标接口 播放器期望的接口(支持MP3播放)
public interface MediaPlayer {
void playMp3(String fileName);
}

  1. 定义适配者(``Adaptee`)
1
2
3
4
5
6

// 目标接口:播放器期望的接口(支持MP3播放)
public interface MediaPlayer {
void playMp3(String fileName);
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14

// 适配者实现类
public class WavPlayer implements AdvancedMediaPlayer {
@Override
public void playWav(String fileName) {
System.out.println("播放WAV文件:" + fileName);
}

@Override
public void playMp4(String fileName) {
// 未实现
}
}

  1. 实现适配器(Adapter)对象适配器:将WavPlayer适配到MediaPlayer接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

public class MediaAdapter implements MediaPlayer {
private AdvancedMediaPlayer advancedPlayer;

// 通过构造函数传入适配者对象(组合方式)
public MediaAdapter(AdvancedMediaPlayer advancedPlayer) {
this.advancedPlayer = advancedPlayer;
}

@Override
public void playMp3(String fileName) {
// 若目标是MP3,直接调用原接口
System.out.println("MP3播放由原接口处理");
}

@Override
public void playWav(String fileName) {
// 转换接口:将playWav请求转发给适配者
advancedPlayer.playWav(fileName);
}
}

  1. 调用方使用, 播放器实现类(使用目标接口)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

public class AudioPlayer implements MediaPlayer {
private MediaAdapter mediaAdapter;

@Override
public void playMp3(String fileName) {
System.out.println("播放MP3文件:" + fileName);
}

@Override
public void playWav(String fileName) {
// 当需要播放WAV时,创建适配器处理
if (mediaAdapter == null) {
mediaAdapter = new MediaAdapter(new WavPlayer());
}
mediaAdapter.playWav(fileName);
}
}

  1. 测试验证:
1
2
3
4
5
6
7
8

// 客户端调用
public static void main(String[] args) {
AudioPlayer player = new AudioPlayer();
player.playMp3("music.mp3"); // 直接调用目标接口
player.playWav("sound.wav"); // 通过适配器转换接口
}

1
2
3
4
5

执行结果
播放MP3文件:music.mp3
播放WAV文件:sound.wav

客户端使用适配器的做法如下:

  • 客户通过使用目标接口,调用适配器的方法,对适配器做出请求。
  • 适配器使用被适配接口,把请求翻译成被适配者上的一个或多个调用。
  • 客户收到调用结果,但根本不知道是适配在进行转译工作。

其类图如下:

  1. 对象适配器
    对象适配器模式
  2. 类适配器
    类适配器模式

场景二:电脑开机过程涉及电源、CPU、内存、硬盘等多个组件的协同工作,使用外观模式可以将复杂流程封装为简单接口。

  1. 定义电脑组件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

// 子系统组件:电源
class Power {
public void on() {
System.out.println("电源已开启");
}

public void off() {
System.out.println("电源已关闭");
}
}

// 子系统组件:CPU
class CPU {
public void start() {
System.out.println("CPU初始化中...");
}

public void stop() {
System.out.println("CPU停止运行");
}
}

// 子系统组件:内存
class Memory {
public void load() {
System.out.println("加载系统到内存...");
}

public void release() {
System.out.println("释放内存资源");
}
}

// 子系统组件:硬盘
class HardDisk {
public void read() {
System.out.println("从硬盘读取系统数据...");
}
}

  1. 定义电脑外观类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

// 外观类:电脑
class ComputerFacade {
private Power power;
private CPU cpu;
private Memory memory;
private HardDisk hardDisk;

public ComputerFacade() {
power = new Power();
cpu = new CPU();
memory = new Memory();
hardDisk = new HardDisk();
}

// 封装复杂的开机流程
public void powerOn() {
power.on();
cpu.start();
memory.load();
hardDisk.read();
System.out.println("电脑开机完成!");
}

// 封装复杂的关机流程
public void powerOff() {
cpu.stop();
memory.release();
power.off();
System.out.println("电脑关机完成!");
}
}
  1. 测试验证
1
2
3
4
5
6
7
8
9
10
11

// 客户端测试
public class FacadePatternDemo {
public static void main(String[] args) {
// 通过外观类操作电脑,无需了解内部组件细节
ComputerFacade computer = new ComputerFacade();
computer.powerOn(); // 一键开机
System.out.println("========");
computer.powerOff(); // 一键关机
}
}

使用外观模式,我们创建一个类,这个类把属于某些子系统的一套复杂的类简化和统一。和许多模式不同,外观模式相当直接了当,没有难以理解的抽象,但这并不会让它的作用减弱;外观模式让我们避免客户端和子系统之间的紧耦合。
类图如下:
外观模式

还涉及到一新的面相对象原则: 【最少知识原则|墨忒耳法则】,只和你的密友谈话。
如何实现只和你的密友谈话,该原则提供类一个指南:对于任何对象,从该对象的任何方法,只调用属于以下范围的方法:

  • 对象自身
  • 作为参数传递给方法的类
  • 该方法创建或实例化的任何对象
  • 对象的任何组件
1
2
3
4
5
6
7
8
9
10
11

// 不遵守原则: 从气象站获取了Thermometer对象,调用了该对象的方法
public float getTemp() {
Thermometer thermometer = station.getThermometer();
return thermometer.getTemperature();
}

// 遵守原则: 给station添加了一个方法,向温度器发起请求,减少依赖的类数量
public float getTemp(){
return station.getTemperature();
}

总结

适配器模式

优点

  • 耦合性低:客户端与适配者解耦,无需修改原有代码;
  • 复用性强:适配者可被多个适配器复用,拓展新接口成本低;
  • 开闭原则:适配器封装变化,无需修改目标接口和适配者,可以新增适配器实现扩展;

缺点

  • 多层封装开销:过多适配器可能增加系统复杂度;
  • 接口转换限制:无法转换适配者未提供的方法(仅能适配现有接口),实现适配器的接口和需要支持的接口大小成正比,若过大是否可以考虑重写客户端的接口调用,或者直接封装一个类,在类中封装所有变化;

使用场景

  • 系统兼容:新旧系统接口不兼容时,通过适配器过渡;
  • 第三方库集成:集成外部库时,将其接口转换为系统兼容的格式;
  • 接口统一:将多个不同接口统一为标准接口(如JavaEnumerationIterator);

在JDK中的应用

  • java.io.InputStreamReader:将InputStream(字节流)适配为Reader(字符流);
  • java.util.Arrays.asList():将数组适配为List接口;
  • Servlet中的适配器:HttpServletAdapter将标准Servlet适配为HttpServlet接口;

外观模式

优点

  • 简化接口:为复杂子系统提供统一入口,客户端无需了解内部细节;
  • 降低耦合:客户端与子系统解耦,子系统内部变化不影响客户端;
  • 层次分明:分离了高层接口和底层实现,符合“最少知识法则”;
  • 保护子系统:外观类可以控制对底层组件的访问,避免客户端直接操作导致混乱;

缺点

  • 复杂度增加:应用“最少知识原则”会导致需要编写更多的“包装者”类来处理对其他组件的调用,导致系统复杂度和开发成本的增加,运行时性能下降;

使用场景

  • 复杂系统的简化接口:如软件框架为用户提供简化的API(如Spring框架对底层技术的封装);
  • 新旧系统兼容:为旧系统提供新的统一接口,便于新功能集成;
  • 模块解耦:作为模块间的接口,降低模块间的依赖;
  • 分布式系统:为分布式服务提供统一的调用入口;

区别

  • 适配器模式:适配器模式转换接口格式,外观模式简化接口复杂度,外观和适配器都可以包装多个类,但外观的意图是简化,而适配器的意图是转化接口为不同的接口;
  • 中介者模式:中介者模式处理组件间的交互,外观模式封装子系统为单一接口;
  • 单例模式:外观类通常设计为单例,确保全局唯一入口;