适配器模式

Metadata

title: 适配器模式
date: 2022-12-14 15:38
tags:
  - 行动阶段/完成
  - 主题场景/设计
  - 笔记空间/KnowladgeSpace/ProgramSpace/ProjectSpace
  - 细化主题/设计模式/结构模式/适配器模式
categories:
  - 设计
keywords:
  - 设计模式/结构模式/适配器模式
description: 适配器模式(Adapter Pattern)是作为两个不兼容的接口之间的桥梁。这种类型的设计模式属于结构型模式,它结合了两个独立接口的功能。

适配器模式(Adapter Pattern)是作为两个不兼容的接口之间的桥梁。这种类型的设计模式属于结构型模式,它结合了两个独立接口的功能。
这种模式涉及到一个单一的类,该类负责加入独立的或不兼容的接口功能。举个真实的例子,读卡器是作为内存卡和笔记本之间的适配器。您将内存卡插入读卡器,再将读卡器插入笔记本,这样就可以通过笔记本来读取内存卡。
我们通过下面的实例来演示适配器模式的使用。其中,音频播放器设备只能播放 mp3 文件,通过使用一个更高级的音频播放器来播放 vlc 和 mp4 文件。

简介

意图:
将一个类的接口转换成客户希望的另外一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。

主要解决:
主要解决在软件系统中,常常要将一些 “现存的对象” 放到新的环境中,而新环境要求的接口是现对象不能满足的。

何时使用:

  1. 系统需要使用现有的类,而此类的接口不符合系统的需要。
  2. 想要建立一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作,这些源类不一定有一致的接口。
  3. 通过接口转换,将一个类插入另一个类系中。(比如老虎和飞禽,现在多了一个飞虎,在不增加实体的需求下,增加一个适配器,在里面包容一个虎对象,实现飞的接口。)

如何解决:
继承或依赖(推荐)。

关键代码:
适配器继承或依赖已有的对象,实现想要的目标接口。

应用实例:

  1. 美国电器 110V,中国 220V,就要有一个适配器将 110V 转化为 220V。
  2. JAVA JDK 1.1 提供了 Enumeration 接口,而在 1.2 中提供了 Iterator 接口,想要使用 1.2 的 JDK,则要将以前系统的 Enumeration 接口转化为 Iterator 接口,这时就需要适配器模式。
  3. 在 LINUX 上运行 WINDOWS 程序。
  4. JAVA 中的 jdbc。

优点:

  1. 可以让任何两个没有关联的类一起运行。
  2. 提高了类的复用。
  3. 增加了类的透明度。
  4. 灵活性好。

缺点:

  1. 过多地使用适配器,会让系统非常零乱,不易整体进行把握。比如,明明看到调用的是 A 接口,其实内部被适配成了 B 接口的实现,一个系统如果太多出现这种情况,无异于一场灾难。因此如果不是很有必要,可以不使用适配器,而是直接对系统进行重构。
  2. 由于 JAVA 至多继承一个类,所以至多只能适配一个适配者类,而且目标类必须是抽象类。

使用场景:
有动机地修改一个正常运行的系统的接口,这时应该考虑使用适配器模式。

注意事项:
适配器不是在详细设计时添加的,而是解决正在服役的项目的问题。

模式结构

对象适配器

  1. 客户端 (Client) 是包含当前程序业务逻辑的类。
  2. 客户端接口 (Client Interface) 描述了其他类与客户端代码合作时必须遵循的协议。
  3. 服务 (Service) 中有一些功能类 (通常来自第三方或遗留系统)。 客户端与其接口不兼容, 因此无法直接调用其功能。
  4. 适配器 (Adapter) 是一个可以同时与客户端和服务交互的类: 它在实现客户端接口的同时封装了服务对象。 适配器接受客户端通过适配器接口发起的调用, 并将其转换为适用于被封装服务对象的调用。
  5. 客户端代码只需通过接口与适配器交互即可, 无需与具体的适配器类耦合。 因此, 你可以向程序中添加新类型的适配器而无需修改已有代码。 这在服务类的接口被更改或替换时很有用: 你无需修改客户端代码就可以创建新的适配器类。

类适配器

这一实现使用了继承机制: 适配器同时继承两个对象的接口。 请注意, 这种方式仅能在支持多重继承的编程语言中实现, 例如 C++。

  1. 类适配器不需要封装任何对象, 因为它同时继承了客户端和服务的行为。 适配功能在重写的方法中完成。 最后生成的适配器可替代已有的客户端类进行使用。

适合应用场景

title: 当你希望使用某个类, 但是其接口与其他代码不兼容时, 可以使用适配器类。

适配器模式允许你创建一个中间层类, 其可作为代码与遗留类、 第三方类或提供怪异接口的类之间的转换器。
title: 如果您需要复用这样一些类, 他们处于同一个继承体系, 并且他们又有了额外的一些共同的方法, 但是这些共同的方法不是所有在这一继承体系中的子类所具有的共性。

你可以扩展每个子类, 将缺少的功能添加到新的子类中。 但是, 你必须在所有新子类中重复添加这些代码, 这样会使得代码有坏味道。

将缺失功能添加到一个适配器类中是一种优雅得多的解决方案。 然后你可以将缺少功能的对象封装在适配器中, 从而动态地获取所需功能。 如要这一点正常运作, 目标类必须要有通用接口, 适配器的成员变量应当遵循该通用接口。 这种方式同装饰模式非常相似。

实现方式

  1. 确保至少有两个类的接口不兼容:
    • 一个无法修改 (通常是第三方、 遗留系统或者存在众多已有依赖的类) 的功能性服务类。
    • 一个或多个将受益于使用服务类的客户端类。
  2. 声明客户端接口, 描述客户端如何与服务交互。
  3. 创建遵循客户端接口的适配器类。 所有方法暂时都为空。
  4. 在适配器类中添加一个成员变量用于保存对于服务对象的引用。 通常情况下会通过构造函数对该成员变量进行初始化, 但有时在调用其方法时将该变量传递给适配器会更方便。
  5. 依次实现适配器类客户端接口的所有方法。 适配器会将实际工作委派给服务对象, 自身只负责接口或数据格式的转换。
  6. 客户端必须通过客户端接口使用适配器。 这样一来, 你就可以在不影响客户端代码的情况下修改或扩展适配器。

示例

我们有一个 MediaPlayer 接口和一个实现了 MediaPlayer 接口的实体类 AudioPlayer。默认情况下,AudioPlayer 可以播放 mp3 格式的音频文件。

我们还有另一个接口 AdvancedMediaPlayer 和实现了 AdvancedMediaPlayer 接口的实体类。该类可以播放 vlc 和 mp4 格式的文件。

我们想要让 AudioPlayer 播放其他格式的音频文件。为了实现这个功能,我们需要创建一个实现了 MediaPlayer 接口的适配器类 MediaAdapter,并使用 AdvancedMediaPlayer 对象来播放所需的格式。

AudioPlayer 使用适配器类 MediaAdapter 传递所需的音频类型,不需要知道能播放所需格式音频的实际类。AdapterPatternDemo 类使用 AudioPlayer 类来播放各种格式。

类适配器:

因为 Adapter 类既继承了 Adaptee (被适配类),也实现了 Target 接口(因为 Java 不支持多继承,所以这样来实现)所以被称为类适配器

实例 (以插网线上网举例):

1)首先我们拥有一根网线, 他有上网的功能,但是它的接口与电脑不匹配

//要适配的类:网线
public class Adaptee {
    //功能:上网
    public void request(){
        System.out.println("链接网线上网");
    }
}

2)因此我们定义了一个 usb 接口,也就是上面提到的目标接口(Target)

//接口转换器的抽象实现
public interface NetToUsb {
    //作用:处理请求,网线=>usb
    public void handleRequest();
}
  1. 定义一个适配器继承着网线,连接着 usb 接口
//真正的适配器,余姚链接usb,连接网线
public class Adapter extends Adaptee implements NetToUsb {

    @Override
    public void handleRequest() {
        super.request();//可以上网了
    }
}

4)上网的具体实现

//客户端类:想上网,插不上网线
public class Computer {
    //电脑需要连接上转接器才可以上网
    public void net(NetToUsb adapter){
        //上网的具体实现:找一个转接头
        adapter.handleRequest();
    }

    public static void main(String[] args) {
        //电脑,适配器,网线
        Computer computer = new Computer();//电脑
        Adapter adapter = new Adapter();//转接器
        computer.net(adapter);//电脑直接连接转接器就可以
    }
}

对象适配器:

不是使用多继承或继承再实现的方式,而是使用直接关联,或者称为委托的方式

  1. 更改适配器为如下代码
public class Adapter2 implements NetToUsb {
    private Adaptee adaptee;
    public Adapter2(Adaptee adaptee){
        this.adaptee = adaptee;
    }
    @Override
    public void handleRequest() {
     adaptee.request();;//可以上网了
    }
}

2)此时的客户端中,转接器需要分别连接网线和电脑

//客户端类:想上网,插不上网线
public class Computer {
    //电脑需要连接上转接器才可以上网
    public void net(NetToUsb adapter){
        //上网的具体实现:找一个转接头
        adapter.handleRequest();
    }

    public static void main(String[] args) {
        //电脑,适配器,网线
        Computer computer = new Computer();//电脑
        Adaptee adaptee = new Adaptee();//网线
        Adapter2 adapter2 = new Adapter2(adaptee);//转接器
        computer.net(adapter2);
    }
}

优缺点

title: 优点

-  单一职责原则你可以将接口或数据转换代码从程序主要业务逻辑中分离。
- 开闭原则。 只要客户端代码通过客户端接口与适配器进行交互, 你就能在不修改现有客户端代码的情况下在程序中添加新类型的适配器。

缺点

  • 代码整体复杂度增加, 因为你需要新增一系列接口和类。 有时直接更改服务类使其与其他代码兼容会更简单。

与其他模式的关系

[[桥接模式]]通常会于开发前期进行设计, 使你能够将程序的各个部分独立开来以便开发。 另一方面, [[适配器模式]]通常在已有程序中使用, 让相互不兼容的类能很好地合作。

[[适配器模式]]可以对已有对象的接口进行修改, [[装饰模式]]则能在不改变对象接口的前提下强化对象功能。 此外, 装饰还支持递归组合, 适配器则无法实现。

[[适配器模式]]能为被封装对象提供不同的接口, [[代理模式]]能为对象提供相同的接口, [[装饰模式]]则能为对象提供加强的接口。

[[外观模式]]为现有对象定义了一个新接口, [[适配器模式]]则会试图运用已有的接口。 适配器通常只封装一个对象, 外观通常会作用于整个对象子系统上。

[[桥接模式]]、 [[状态模式]]和[[策略模式]] (在某种程度上包括[[适配器模式]]) 模式的接口非常相似。 实际上, 它们都基于[[组合模式]]——即将工作委派给其他对象, 不过也各自解决了不同的问题。 模式并不只是以特定方式组织代码的配方, 你还可以使用它们来和其他开发者讨论模式所解决的问题。