GoF23:Adapter & Facade(适配器&外观)

GoF23

1、适配器概述

生活中的适配器

问题

  • 假设有一个插座(三孔)和一个插头(二头)
  • 此时无法直接将二者连接使用。

image-20220202200015118

解决

使用适配器:改变插座的接口,以符合插头的要求。

image-20220202195746605

面向对象适配器

问题

  • 假设有一个软件系统,系统接口厂商提供的接口不匹配;
  • 此时无法工作。

image-20220202200605044

解决

  • 不想改变现有代码(系统代码),而且无法改变厂商代码;

  • 开发适配器类,将厂商的接口转换为所期望的接口。

    image-20220202202157562

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();
        }
    }
    
  • 截图

    image-20220202215920782

3、适配器模式

4.1、定义

适配器模式将一个类的接口,转换成客户期望的另一个接口

  • 创建适配器进行接口转换,使原本不兼容的接口兼容;
  • 让客户从实现的接口解耦;
  • 若接口改变,适配器可以封装改变的部分,而无需修改客户代码。

4.2、类图

  • 客户
    1. 使用目标接口;
    2. 实际是调用适配器的方法,以发出请求。
  • 适配器
    1. 实现目标接口、组合被适配接口;
    2. 将请求委托给被适配者的一个或多个方法。
  • 被适配者

image-20220203000414831

4.3、类、对象适配器

实际上,适配器有两类:类适配器、对象适配器。

类图对比

image-20220203002430642

对比

类适配器 对象适配器
实现方式 多重继承 组合
被适配者 适配某个特定的类 适配某个类,及其任意子类
工作 不需要实现整个 adaptee,但在必要时可以覆盖 需要实现整个 adaptee
使用方式 使用一个类适配器 使用一个对象适配器,并委托被适配者

4.4、分析

  1. 适配器与目标接口的大小成正比(因为要实现目标接口的所有方法)。
  2. 面向抽象编程:客户与接口绑定,而不是与实现绑定。
  3. 双向适配器:实现所涉及的两个接口,以支持两边的接口。
  4. 装饰者 vs 适配器 vs 外观模式
    • 共同点:包装现有对象,无需修改现有代码;
    • 装饰者:添加新行为;
    • 适配器:转换接口;
    • 外观:简化接口。

4、case:HomeTheater

4.1、家庭影院

简介

  • 组成:爆米花机、灯光、屏幕、投影仪、音响、DVD播放器等;
  • 涉及很多的接口和类,以及它们之间的交互。

image-20220203173453648

观看步骤

以原生方式观看一部 DVD影片,步骤十分繁琐:

  1. 打开爆米花机
  2. 开始爆米花
  3. 灯光调暗
  4. 放下屏幕
  5. 打开投影仪
  6. 投影仪输入切换为DVD
  7. 投影仪设置为宽屏模式
  8. 打开音响
  9. 音响输入切换为DVD
  10. 音响设置为环绕立体声
  11. 音响音量调到中(5)
  12. 打开DVD播放器
  13. 播放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、家庭影院外观

  1. 设计一个外观接口,“封装”一个复杂的子系统,以简化接口
  2. 如需使用子系统中的功能,仍可使用子系统的接口

类图

  1. 外观类:HomeTheaterFacade
    • 对外暴露几个简单方法;
    • 将涉及的所有组件,视为一个子系统;
    • 通过调用子系统,来实现外观类的方法;
  2. 客户:调用外观类提供的方法,而无需逐个调用子系统的方法。

image-20220203180354184

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();
    }
    
  • 结果

    image-20220203184154523

5、外观模式

5.1、定义

外观模式提供了一个统一的接口,用来访问子系统中的一群接口

  • 外观定义了一个高层接口,让子系统更容易使用;
  • OOP原则
    • 迪米特法则:减少对象之间的交互,只与直接朋友交流;
    • 开闭原则

5.2、类图

image-20220203184609293

5.3、思考

  1. 外观模式简化子系统而不是 封装(encapsulation) 子系统。
    • 提供简化接口的同时,保留系统完整的功能;
    • 也就是说,可以调用外观类来完成请求。如需使用子系统,仍可单独使用子系统中的某个接口或类
  2. 一个子系统可以有多个外观
  3. 外观模式将 客户子系统 解耦
  4. 外观 vs 适配器
    • 外观:简化接口
    • 适配器:转换接口

6、小结

适配器

  1. 适配器模式将一个类的接口,转换成客户期望的另一个接口
    • 客户:使用目标接口。实际是调用适配器的方法,以发出请求;
    • 适配器:实现目标接口、组合被适配接口。将请求委托给被适配者;
    • 被适配者
  2. 类适配器:多重继承,适配某个特定的类,不需要实现整个adaptee,使用时需要一个类适配器;
  3. 对象适配器:组合,适配某个类及其子类,需要实现整个adaptee,使用时需要一个对象适配器和被适配者。
  4. 双向适配器:实现所涉及的两个接口,以支持两边的接口。

外观

  1. 外观模式提供了一个统一的接口,用来访问子系统中的一群接口
  2. 外观定义了一个高层接口,让子系统更容易使用;
  3. OOP原则:迪米特法则、开闭原则
  4. 外观模式简化子系统,而不是 封装(encapsulation) 子系统。
  5. 一个子系统可以有多个外观。

对比

适配器 vs 装饰者 vs 外观

共同点:包装现有对象,无需修改现有代码。

  • 装饰者:添加新行为;
  • 适配器:转换接口;
  • 外观:简化接口。
posted @ 2022-01-28 15:24  Jaywee  阅读(48)  评论(0编辑  收藏  举报

👇