GoF23:Adapter & Facade(适配器&外观)
1、适配器概述
生活中的适配器
问题
- 假设有一个插座(三孔)和一个插头(二头);
- 此时无法直接将二者连接使用。
解决
使用适配器:改变插座的接口,以符合插头的要求。
面向对象适配器
问题
- 假设有一个软件系统,系统接口与厂商提供的接口不匹配;
- 此时无法工作。
解决
-
不想改变现有代码(系统代码),而且无法改变厂商代码;
-
开发适配器类,将厂商的接口转换为所期望的接口。
2、case:Duck & Turkey
2.1、接口
- Duck:呱呱叫、飞行
- Turkey:咯咯叫、飞行
public interface Duck {
void quack();
void fly();
}
public interface Turkey {
void gobble();
void fly();
}
2.2、实现类
// 野鸭
public class MallardDuck implements Duck{
@Override
public void quack() {
System.out.println("野鸭,嘎嘎叫");
}
@Override
public void fly() {
System.out.println("野鸭,飞行");
}
}
// 野生火鸡
public class WildTurkey implements Turkey{
@Override
public void gobble() {
System.out.println("野生火鸡,咯咯叫");
}
@Override
public void fly() {
System.out.println("野生火鸡,飞行");
}
}
2.3、适配器
假设现在缺少鸭子对象,想用一些火鸡对象来冒充。因此写一个 “火鸡适配器”。
- 实现接口:想转换成的目标类型;
- 对象引用:实际调用该对象的方法(构造器注入);
- 实现方法:对外暴露的是接口方法,内部实际是调用被适配者的方法。
public class TurkeyAdapter implements Duck {
private Turkey turkey;
public TurkeyAdapter(Turkey turkey) {
this.turkey = turkey;
}
@Override
public void quack() {
turkey.gobble();
}
@Override
public void fly() {
System.out.println("第" + (i + 1) + "次飞行");
for (int i = 0; i < 3; i++) {
turkey.fly();
}
}
}
2.4、测试
-
代码
public class TurkeyAdapterTest { @Test public void test() { // 被适配者 WildTurkey turkey = new WildTurkey(); // 适配器,声明为目标类型 Duck turkeyAdapter = new TurkeyAdapter(turkey); turkeyAdapter.fly(); turkeyAdapter.quack(); } }
-
截图
3、适配器模式
4.1、定义
适配器模式:将一个类的接口,转换成客户期望的另一个接口。
- 创建适配器进行接口转换,使原本不兼容的接口兼容;
- 让客户从实现的接口解耦;
- 若接口改变,适配器可以封装改变的部分,而无需修改客户代码。
4.2、类图
- 客户
- 使用目标接口;
- 实际是调用适配器的方法,以发出请求。
- 适配器
- 实现目标接口、组合被适配接口;
- 将请求委托给被适配者的一个或多个方法。
- 被适配者
4.3、类、对象适配器
实际上,适配器有两类:类适配器、对象适配器。
类图对比
对比
类适配器 | 对象适配器 | |
---|---|---|
实现方式 | 多重继承 | 组合 |
被适配者 | 适配某个特定的类 | 适配某个类,及其任意子类 |
工作 | 不需要实现整个 adaptee,但在必要时可以覆盖 | 需要实现整个 adaptee |
使用方式 | 使用一个类适配器 | 使用一个对象适配器,并委托被适配者 |
4.4、分析
- 适配器与目标接口的大小成正比(因为要实现目标接口的所有方法)。
- 面向抽象编程:客户与接口绑定,而不是与实现绑定。
- 双向适配器:实现所涉及的两个接口,以支持两边的接口。
- 装饰者 vs 适配器 vs 外观模式
- 共同点:包装现有对象,无需修改现有代码;
- 装饰者:添加新行为;
- 适配器:转换接口;
- 外观:简化接口。
4、case:HomeTheater
4.1、家庭影院
简介
- 组成:爆米花机、灯光、屏幕、投影仪、音响、DVD播放器等;
- 涉及很多的接口和类,以及它们之间的交互。
观看步骤
以原生方式观看一部 DVD影片,步骤十分繁琐:
- 打开爆米花机
- 开始爆米花
- 灯光调暗
- 放下屏幕
- 打开投影仪
- 投影仪输入切换为DVD
- 投影仪设置为宽屏模式
- 打开音响
- 音响输入切换为DVD
- 音响设置为环绕立体声
- 音响音量调到中(5)
- 打开DVD播放器
- 播放DVD
对应代码
客户需要逐个调用以下代码。
popper.on();
popper.pop();
light.dim(10);
screen.down();
projector.on();
projector.setInput(dvd);
projector.wideScreenMode();
amp.on();
amp.setInput(dvd);
amp.setSurroundSound();
amp.setVolume(5);
dvdPlayer.on();
dvdPlayer.play(movie);
问题
- 看完电影,需要将所有设备关闭,难道需要反向把所有动作执行一遍?
- 如果不是看电影,而是看 CD 或广播,步骤也如此繁琐?
- 因此,需要升级系统,简化操作过程。(外观模式)
- 个人认为,也可以用命令模式的宏命令来优化设计。
4.2、家庭影院外观
- 设计一个外观接口,“封装”一个复杂的子系统,以简化接口;
- 如需使用子系统中的功能,仍可使用子系统的接口。
类图
- 外观类:HomeTheaterFacade
- 对外暴露几个简单方法;
- 将涉及的所有组件,视为一个子系统;
- 通过调用子系统,来实现外观类的方法;
- 客户:调用外观类提供的方法,而无需逐个调用子系统的方法。
4.3、代码实现
HomeTheaterFacade
- 成员变量:涉及的子系统组件(构造器注入);
- 方法:通过调用子系统,来实现外观类的方法。
public class HomeTheaterFacade {
private CornPopper popper;
private Light light;
private Screen screen;
private Projector projector;
private Amplifier amp;
private DvdPlayer player;
public HomeTheaterFacade(CornPopper popper, Light light, Screen screen, Projector projector, Amplifier amp, DvdPlayer player) {
this.popper = popper;
this.light = light;
this.screen = screen;
this.projector = projector;
this.amp = amp;
this.player = player;
}
public void watchMovie(String movie) {
System.out.println("Ready to watch movie:" + movie);
popper.on();
popper.pop();
light.dim(10);
screen.down();
projector.on();
projector.setInput("DVD");
projector.wideScreenMode();
amp.on();
amp.setInput("DVD");
amp.setSurroundSound();
amp.setVolume(5);
player.on();
player.play(movie);
}
public void endMovie(String movie) {
System.out.println("Ending......");
popper.off();
light.on();
screen.up();
projector.off();
amp.off();
player.pause();
player.off();
}
}
测试
-
代码
@Test public void test() { // 组件 CornPopper popper = new CornPopper(); Light light = new Light(); Screen screen = new Screen(); Projector projector = new Projector(); Amplifier amp = new Amplifier(); DvdPlayer player = new DvdPlayer(); // 外观 HomeTheaterFacade facade = new HomeTheaterFacade(popper, light, screen, projector, amp, player); facade.watchMovie("不能说的秘密"); System.out.println(); facade.endMovie(); }
-
结果
5、外观模式
5.1、定义
外观模式:提供了一个统一的接口,用来访问子系统中的一群接口。
- 外观定义了一个高层接口,让子系统更容易使用;
- OOP原则
- 迪米特法则:减少对象之间的交互,只与直接朋友交流;
- 开闭原则。
5.2、类图
5.3、思考
- 外观模式简化子系统,而不是 封装(encapsulation) 子系统。
- 提供简化接口的同时,保留系统完整的功能;
- 也就是说,可以调用外观类来完成请求。如需使用子系统,仍可单独使用子系统中的某个接口或类。
- 一个子系统可以有多个外观。
- 外观模式将 客户 与 子系统 解耦;
- 外观 vs 适配器
- 外观:简化接口
- 适配器:转换接口
6、小结
适配器
- 适配器模式:将一个类的接口,转换成客户期望的另一个接口;
- 客户:使用目标接口。实际是调用适配器的方法,以发出请求;
- 适配器:实现目标接口、组合被适配接口。将请求委托给被适配者;
- 被适配者
- 类适配器:多重继承,适配某个特定的类,不需要实现整个adaptee,使用时需要一个类适配器;
- 对象适配器:组合,适配某个类及其子类,需要实现整个adaptee,使用时需要一个对象适配器和被适配者。
- 双向适配器:实现所涉及的两个接口,以支持两边的接口。
外观
- 外观模式:提供了一个统一的接口,用来访问子系统中的一群接口;
- 外观定义了一个高层接口,让子系统更容易使用;
- OOP原则:迪米特法则、开闭原则
- 外观模式简化子系统,而不是 封装(encapsulation) 子系统。
- 一个子系统可以有多个外观。
对比
适配器 vs 装饰者 vs 外观
共同点:包装现有对象,无需修改现有代码。
- 装饰者:添加新行为;
- 适配器:转换接口;
- 外观:简化接口。