适配器模式:让不兼容接口协同工作

文章目录

    • [1. 适配器模式概述](#1. 适配器模式概述)
    • [2. 适配器模式的分类](#2. 适配器模式的分类)
      • [2.1 类适配器](#2.1 类适配器)
      • [2.2 对象适配器](#2.2 对象适配器)
    • [3. 适配器模式的结构](#3. 适配器模式的结构)
    • 4. C#实现适配器模式
      • [4.1 对象适配器实现](#4.1 对象适配器实现)
      • [4.2 类适配器实现](#4.2 类适配器实现)
    • [5. 适配器模式的实际应用场景](#5. 适配器模式的实际应用场景)
      • [5.1 第三方库集成](#5.1 第三方库集成)
      • [5.2 遗留系统集成](#5.2 遗留系统集成)
      • [5.3 系统重构与升级](#5.3 系统重构与升级)
      • [5.4 跨平台开发](#5.4 跨平台开发)
    • [6. 类适配器与对象适配器的对比](#6. 类适配器与对象适配器的对比)
      • [6.1 类适配器](#6.1 类适配器)
      • [6.2 对象适配器](#6.2 对象适配器)
    • [7. 适配器模式与其他模式的关系](#7. 适配器模式与其他模式的关系)
      • [7.1 适配器模式 vs 桥接模式](#7.1 适配器模式 vs 桥接模式)
      • [7.2 适配器模式 vs 装饰器模式](#7.2 适配器模式 vs 装饰器模式)
      • [7.3 适配器模式 vs 外观模式](#7.3 适配器模式 vs 外观模式)
    • [8. 适配器模式的优缺点](#8. 适配器模式的优缺点)
      • [8.1 优点](#8.1 优点)
      • [8.2 缺点](#8.2 缺点)
    • [9. .NET中的适配器模式应用](#9. .NET中的适配器模式应用)
      • [9.1 数据访问适配器](#9.1 数据访问适配器)
      • [9.2 IO流适配器](#9.2 IO流适配器)
    • [10. 总结](#10. 总结)

1. 适配器模式概述

适配器模式(Adapter Pattern)是一种结构型设计模式,它的主要目的是将一个类的接口转换成客户端所期望的另一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。简言之,适配器模式就是提供了一个中间层,让原本接口不兼容的类可以协同工作。

在日常生活中,适配器的例子比比皆是:

  • 电源适配器:将不同国家的电源插座标准转换为设备所需的电压和插座类型
  • 读卡器:将存储卡中的数据适配到电脑可识别的接口
  • 转接头:比如Type-C转HDMI接口的转接头

在软件开发中,当我们需要使用一个已存在的类,但是其接口与我们需要的不一致时,我们就可以使用适配器模式。

2. 适配器模式的分类

根据实现方式的不同,适配器模式可以分为两种主要类型:

2.1 类适配器

类适配器使用多重继承(在C#中是通过继承一个类并实现接口的方式)来达到适配的目的。

2.2 对象适配器

对象适配器使用组合关系,将被适配的对象作为适配器的一个成员变量。这种方式更加灵活,符合"组合优于继承"的设计原则。

3. 适配器模式的结构

依赖 实现 关联 Client method() <<interface>> Target request() Adapter request() Adaptee specificRequest()

适配器模式主要包含以下几个核心角色:

  1. 目标(Target):定义客户端使用的接口。

  2. 适配者(Adaptee):已存在的、具有特殊功能但接口不符合目标接口的类。

  3. 适配器(Adapter):实现目标接口并包含对适配者实例的引用,在目标接口的方法中调用适配者的相应方法。

  4. 客户端(Client):与目标接口交互的类。

以下是更详细的类图,展示了类适配器和对象适配器的区别:
依赖 实现 组合 继承 实现 Client Main() <<interface>> ITarget Request() Adaptee SpecificRequest() ObjectAdapter adaptee: Adaptee Request() ClassAdapter Request() SpecificRequest()

适配器模式的工作流程可以通过以下序列图来说明:
Client Adapter Adaptee 请求服务(Request) 转换并调用特定方法(SpecificRequest) 返回结果 转换并返回结果 Client Adapter Adaptee

从序列图中可以看出,客户端并不直接与适配者交互,而是通过适配器进行间接交互。适配器负责将客户端的请求转换为适配者能够理解的形式,并将适配者的响应转换回客户端期望的形式。

4. C#实现适配器模式

下面我们通过一个现实案例来演示适配器模式的实现。假设我们有一个第三方的媒体播放库,但它的接口与我们的应用程序所需的接口不兼容,我们需要创建一个适配器来解决这个问题。

4.1 对象适配器实现

首先,我们定义客户端期望的目标接口:

csharp 复制代码
/// <summary>
/// 目标接口:定义客户端使用的接口
/// </summary>
public interface IMediaPlayer
{
    void Play(string audioType, string fileName);
}

然后,我们有一个已经存在的适配者类(假设是第三方库):

csharp 复制代码
/// <summary>
/// 适配者类:已存在的视频播放器
/// </summary>
public class AdvancedMediaPlayer
{
    public void PlayVlc(string fileName)
    {
        Console.WriteLine($"播放VLC文件:{fileName}");
    }

    public void PlayMp4(string fileName)
    {
        Console.WriteLine($"播放MP4文件:{fileName}");
    }
}

现在,我们创建适配器类,将AdvancedMediaPlayer适配到IMediaPlayer接口:

csharp 复制代码
/// <summary>
/// 适配器类:将AdvancedMediaPlayer适配到IMediaPlayer
/// </summary>
public class MediaAdapter : IMediaPlayer
{
    private AdvancedMediaPlayer _advancedMediaPlayer;

    public MediaAdapter(string audioType)
    {
        _advancedMediaPlayer = new AdvancedMediaPlayer();
    }

    public void Play(string audioType, string fileName)
    {
        if (audioType.Equals("vlc", StringComparison.OrdinalIgnoreCase))
        {
            _advancedMediaPlayer.PlayVlc(fileName);
        }
        else if (audioType.Equals("mp4", StringComparison.OrdinalIgnoreCase))
        {
            _advancedMediaPlayer.PlayMp4(fileName);
        }
        else
        {
            Console.WriteLine($"不支持的媒体类型:{audioType}");
        }
    }
}

最后,创建客户端类:

csharp 复制代码
/// <summary>
/// 客户端类:使用IMediaPlayer接口播放不同类型的媒体文件
/// </summary>
public class AudioPlayer : IMediaPlayer
{
    public void Play(string audioType, string fileName)
    {
        // 内置支持播放mp3文件
        if (audioType.Equals("mp3", StringComparison.OrdinalIgnoreCase))
        {
            Console.WriteLine($"播放MP3文件:{fileName}");
        }
        // 通过适配器支持播放其他格式
        else if (audioType.Equals("vlc", StringComparison.OrdinalIgnoreCase) ||
                 audioType.Equals("mp4", StringComparison.OrdinalIgnoreCase))
        {
            MediaAdapter mediaAdapter = new MediaAdapter(audioType);
            mediaAdapter.Play(audioType, fileName);
        }
        else
        {
            Console.WriteLine($"不支持的媒体类型:{audioType}");
        }
    }
}

使用示例:

csharp 复制代码
static void Main(string[] args)
{
    AudioPlayer audioPlayer = new AudioPlayer();
    
    // 播放mp3文件(内置支持)
    audioPlayer.Play("mp3", "beyond the horizon.mp3");
    
    // 通过适配器播放vlc文件
    audioPlayer.Play("vlc", "far far away.vlc");
    
    // 通过适配器播放mp4文件
    audioPlayer.Play("mp4", "alone.mp4");
    
    // 尝试播放不支持的格式
    audioPlayer.Play("avi", "mind me.avi");
    
    Console.ReadLine();
}

输出结果:

复制代码
播放MP3文件:beyond the horizon.mp3
播放VLC文件:far far away.vlc
播放MP4文件:alone.mp4
不支持的媒体类型:avi

4.2 类适配器实现

在C#中,由于不支持多重继承,类适配器通常通过继承适配者类并实现目标接口来实现。下面是一个简单的例子:

csharp 复制代码
/// <summary>
/// 类适配器:通过继承AdvancedMediaPlayer并实现IMediaPlayer接口
/// </summary>
public class MediaClassAdapter : AdvancedMediaPlayer, IMediaPlayer
{
    public void Play(string audioType, string fileName)
    {
        if (audioType.Equals("vlc", StringComparison.OrdinalIgnoreCase))
        {
            PlayVlc(fileName);
        }
        else if (audioType.Equals("mp4", StringComparison.OrdinalIgnoreCase))
        {
            PlayMp4(fileName);
        }
        else
        {
            Console.WriteLine($"不支持的媒体类型:{audioType}");
        }
    }
}

5. 适配器模式的实际应用场景

适配器模式在实际软件开发中有广泛的应用:

5.1 第三方库集成

当需要集成第三方库,但其API与系统现有接口不兼容时,可以使用适配器模式创建一个中间层。

5.2 遗留系统集成

在企业应用中,经常需要将新系统与遗留系统集成,适配器模式可以帮助解决接口不兼容的问题。

5.3 系统重构与升级

在系统升级过程中,为了保持对现有代码的兼容,可以通过适配器模式提供向后兼容性。

5.4 跨平台开发

在跨平台开发中,不同平台可能有不同的API,适配器模式可以提供一个统一的接口。

6. 类适配器与对象适配器的对比

适配器模式 类适配器 对象适配器 通过多重继承/接口实现 通过组合/聚合关系实现 优点: 不需要额外对象 缺点: 耦合度高 优点: 灵活性高 缺点: 需要额外对象

6.1 类适配器

优点:

  • 适配器可以重写适配者的方法,提供更加灵活的适配
  • 不需要创建额外的对象,减少了内存开销

缺点:

  • 使用了继承,导致高耦合
  • 在C#等只支持单继承的语言中,一旦适配器类继承了适配者类,就不能再继承其他类

6.2 对象适配器

优点:

  • 使用组合代替继承,遵循"组合优于继承"的原则
  • 可以适配多个适配者类
  • 低耦合,适配者类的修改不会直接影响到适配器

缺点:

  • 需要创建额外的对象
  • 不能覆盖适配者类的行为

7. 适配器模式与其他模式的关系

7.1 适配器模式 vs 桥接模式

  • 适配器模式是事后补救,用于解决已有接口不兼容的问题
  • 桥接模式是事前预防,用于将抽象与实现分离,使它们可以独立变化

7.2 适配器模式 vs 装饰器模式

  • 适配器模式改变接口以匹配客户端的期望
  • 装饰器模式保持接口不变,但增加了对象的职责

7.3 适配器模式 vs 外观模式

  • 适配器模式使得两个现有的接口能够协同工作
  • 外观模式定义一个更高级的接口,简化了子系统的使用

8. 适配器模式的优缺点

8.1 优点

  1. 增加类的透明性:通过适配器,客户端可以调用同一接口,无需知道被适配者的存在。

  2. 提高类的复用性:将现有的类包装成目标接口,复用现有的功能。

  3. 灵活性和扩展性好:可以在不修改现有代码的情况下增加新的适配器,满足新的需求。

  4. 遵循开闭原则:可以引入新的适配器而无需修改现有代码,适配器本身也可以在不修改客户端代码的情况下替换。

8.2 缺点

  1. 增加系统的复杂性:引入适配器会增加系统的复杂性,因此只有在必要时才应使用。

  2. 可能需要更多的代码:需要编写额外的适配器类。

  3. 可能存在性能损失:适配器中的额外间接调用可能会导致一些性能损失。

9. .NET中的适配器模式应用

在.NET框架中,适配器模式有许多实际应用:

9.1 数据访问适配器

ADO.NET中的DataAdapter类就是一个典型的适配器模式应用。它将不同数据库提供者的特定操作适配到统一的接口,使得应用程序可以用一致的方式操作不同的数据库。

csharp 复制代码
// 创建SqlDataAdapter(适配SQL Server数据库)
SqlDataAdapter sqlAdapter = new SqlDataAdapter("SELECT * FROM Customers", connectionString);

// 创建OracleDataAdapter(适配Oracle数据库)
OracleDataAdapter oracleAdapter = new OracleDataAdapter("SELECT * FROM Customers", oracleConnectionString);

// 客户端代码可以以相同的方式使用不同的适配器
DataSet dataSet = new DataSet();
sqlAdapter.Fill(dataSet);  // 或 oracleAdapter.Fill(dataSet);

9.2 IO流适配器

.NET中的流适配器如StreamReaderStreamWriter也应用了适配器模式。它们将字节流转换为字符流,使得开发者可以更方便地处理文本数据。

csharp 复制代码
// 创建一个文件流
FileStream fileStream = new FileStream("file.txt", FileMode.Open);

// 使用StreamReader适配器将字节流转换为字符流
StreamReader reader = new StreamReader(fileStream);

// 现在可以按行读取文本了
string line;
while ((line = reader.ReadLine()) != null)
{
    Console.WriteLine(line);
}

10. 总结

适配器模式是一种非常实用的设计模式,它解决了接口不兼容的问题,使得原本不能一起工作的类可以协同工作。适配器模式主要有两种实现方式:类适配器和对象适配器,每种方式都有其适用场景。

在实际开发中,适配器模式常用于以下场景:

  • 集成第三方库
  • 系统重构或升级
  • 构建与多个外部系统交互的应用程序
  • 为遗留系统提供新的接口

通过合理使用适配器模式,可以提高代码的复用性、灵活性和可维护性,使系统更容易适应变化的需求。

相关推荐
杨充17 小时前
10.可测试性实战设计
设计模式·开源·代码规范
杨充17 小时前
9.重构十二式的实战
设计模式·开源·代码规范
杨充17 小时前
6.设计原则的全景图
设计模式·开源·全栈
杨充17 小时前
2.面向对象的特性
设计模式
杨充17 小时前
7.SOLID原则案例汇
设计模式·开源·全栈
杨充17 小时前
8.反模式与坏味道
设计模式·开源·代码规范
杨充17 小时前
3.接口vs抽象类比较
设计模式
咖啡八杯20 小时前
文法、BNF与AST
java·设计模式·解释器模式·ast·文法
咖啡八杯1 天前
GoF设计模式——解释器模式
java·后端·spring·设计模式