Adapter 适配器模式简介与 C# 示例【结构型1】【设计模式来了_6】
阅读原文时间:2023年08月30日阅读:1

1、什么是适配器模式?

一句话解释:

两个无关联的类,通过实现同一接口或继承对方得到新的适配器类,新的适配器类中通过实现原本类的操作,可达到进行相同的操作的目的。

适配器模式(Apapter Pattern)是一种结构型设计模式,用于将一个类的实现转换成客户端所期望的另一个类,这个类中的操作和目标类具有相同的操作,以达到两个不相关的类可以互操作的目的。

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

一个比喻:(两位来自不同方言区的学生)

  分别来自广州和海口的两位同学,如果都各自说自己的家乡话粤语和海南话,则根本无法沟通,此时普通话就是一个适配器,将两种方言翻译成普通话来进行适配,这样就可以顺畅沟通了。

2、优缺点和使用场景

优缺点:

  • 通过适配器模式,可以使两个不兼容的接口协同工作,避免了修改现有代码的需要。

  • 提高了代码的复用性和灵活性,因为适配器可以重复使用,并且可以在不同的场景中使用。

  • 降低了系统的耦合度,适配器模式允许系统中的各个组件相互独立地演化。

  • 由于引入了适配器类,可能造成系统的复杂度增加。

  • 在某些情况下,可能需要创建多个适配器来满足不同的客户端需求。

适用场景:

  • 当需要使用一个已经存在的类,但其接口与你的需求不兼容时,可以使用适配器模式。
  • 当想要创建一个可复用的类,该类与一些不相关或不可预见的类进行交互时,适配器模式也是很有用的。
  • 当希望通过一个统一的接口与多个类进行交互时,适配器模式可以提供一个统一的接口。
  • 当需要在不破坏现有代码结构的情况下,对已有的类进行功能扩展时,可以考虑使用适配器模式。

实际使用场景举例:

  • 旧系统与新系统的兼容:当需要将一个旧系统的接口适配成一个新系统可以使用的接口时,适配器模式非常有用。通过创建一个适配器类,可以使新系统能够无缝地与老旧系统进行通信,而不需要修改新系统的代码。
  • 第三方组件的集成:当我们需要使用某个第三方组件,但其提供的接口与我们的系统需求不一致时,可以使用适配器模式。适配器可以将第三方组件的接口转换为符合我们系统需求的接口形式,从而能够顺利地集成到我们的系统中。
  • 多个类库之间的互操作:当我们需要在多个类库之间进行互操作,但它们之间的接口不兼容时,适配器模式可以起到桥梁的作用。通过创建适配器类,将不同类库的接口转换为统一的接口,实现它们之间的互操作性。
  • 接口的标准化:当系统中存在多个类似但接口不同的组件时,可以使用适配器模式将它们的接口标准化。通过适配器,这些组件可以统一使用相同的接口,从而提高系统的一致性和可维护性。
  • 对已有类的功能扩展:当我们需要对一个已有的类进行功能扩展时,可以使用适配器模式。通过创建适配器类,将新功能与原有类进行适配,使得原有类能够使用新功能,同时不影响原有代码的稳定性。

下面是一个使用适配器设计模式的示例,假设我们有一个电源插座接口 ISocket,其中定义了供电方法 SupplyPower():

class Program // 测试
{
    static void Main(string[] args)
    {
        // 国标插座
        ChinaSocket chinaSocket = new ChinaSocket();
        ChinaSocketAdapter chinaAdapter = new ChinaSocketAdapter(chinaSocket);
        Laptop laptop1 = new Laptop(chinaAdapter); // 笔记本1 通过国标版插座充电
        laptop1.Charge();
        // 美版插座
        USASocket usaSocket = new USASocket();
        USASocketAdapter usaAdapter = new USASocketAdapter(usaSocket);
        Laptop laptop2 = new Laptop(usaAdapter); // 同一类型的笔记本2 通过美标版插座充电
        laptop2.Charge();
        Console.ReadKey();
    }
}
// 电源插座接口
public interface ISocket
{
    void SupplyPower();
}
// 国标版插口
public class ChinaSocket
{
    public string name { get; set; }
    public void Charge()
    {
        Console.WriteLine("开始充电!");
    }
}
// 美版插口
public class USASocket
{
    public string name { get; set; }
    public void PowerUp()
    {
        Console.WriteLine("Start charging!");
    }
}
// 国标版的适配器,实现统一的电源插座接口
public class ChinaSocketAdapter : ISocket
{
    private readonly ChinaSocket _chinaSocket;
    public ChinaSocketAdapter(ChinaSocket chinaSocket)
    {
        _chinaSocket = chinaSocket;
    }
    public void SupplyPower() // 国标版实现充电方法,调用特有的 Charge() 方法
    {
        _chinaSocket.Charge();
    }
}
// 美版插座适配器,实现统一的插座接口
public class USASocketAdapter : ISocket
{
    private readonly USASocket _usaSocket;
    public USASocketAdapter(USASocket usaSocket)
    {
        _usaSocket = usaSocket;
    }
    public void SupplyPower() // 美版实现充电方法,调用特有的 PowerUp() 方法
    {
        _usaSocket.PowerUp();
    }
}
// 笔记本电脑类实现,统一充电方法 Charge()
public class Laptop
{
    private readonly ISocket _socket;
    public Laptop(ISocket socket)
    {
        _socket = socket;
    }
    public void Charge()
    {
        _socket.SupplyPower();
    }
}

最终,无论是国标还是美版的插座,均调用 Charge() 方法进行充电,充当适配器角色的就是电源插座接口 IScoket。

如下简单画一下上一章节示例代码对应的类图:(美版相似就省略了,只显示国标版)

Client:给 Laptop 声明插座并完成充电动作。

Laptop:定义统一充电接口的模板,供适配器实现。

ChinaSocketAdapter:对 ChinaSocket 类和 Laptop 进行适配。

ChinaSocket:已存在的类,此类需要适配。

IDataAdapter 接口的实现类通常是数据库访问器,它们提供了一种与特定数据库类型无关的方式来访问数据。

下面是接口 IDataAdapter 的源码:

// System.Data.Common, Version=7.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
// System.Data.IDataAdapter
using System.Data;
using System.Diagnostics.CodeAnalysis;

public interface IDataAdapter
{
    MissingMappingAction MissingMappingAction { get; set; }

    MissingSchemaAction MissingSchemaAction { get; set; }

    ITableMappingCollection TableMappings { get; }

    [RequiresUnreferencedCode("IDataReader's (built from adapter commands) schema table types cannot be statically analyzed.")]
    DataTable[] FillSchema(DataSet dataSet, SchemaType schemaType);

    int Fill(DataSet dataSet);

    IDataParameter[] GetFillParameters();

    [RequiresUnreferencedCode("IDataReader's (built from adapter commands) schema table types cannot be statically analyzed.")]
    int Update(DataSet dataSet);
}

具体的数据库,均实现了 IDataAdapter 接口:

// SQL Server
public sealed class SqlDataAdapter : DbDataAdapter, IDataAdapter, IDbDataAdapter, ICloneable
{ ... }
// Oracle
[Designer("Oracle.VsDevTools.OracleVSGDataAdapterWizard, Oracle.VsDevTools, Version=4.122.1.0, Culture=neutral, PublicKeyToken=89b483f429c47342, processorArchitecture=MSIL", typeof(IDesigner))]
[ToolboxBitmap(typeof(resfinder), "Oracle.ManagedDataAccess.src.Client.Icons.OracleDataAdapterToolBox_hc.bmp")]
[DefaultEvent("RowUpdated")]
public sealed class OracleDataAdapter : DbDataAdapter, IDbDataAdapter, IDataAdapter
{ ... }
// MySQL
[DesignerCategory("Code")]
[Designer("MySql.Data.MySqlClient.Design.MySqlDataAdapterDesigner,MySqlClient.Design")]
public sealed class MySqlDataAdapter : DbDataAdapter, IDbDataAdapter, IDataAdapter
{ ... }

通过使用 IDataAdapter 接口,我们可以编写与特定数据库类型无关的代码,而只需要关心数据集的结构和操作。这样,我们就可以更轻松地更换或扩展我们的数据源,而不需要修改应用程序的代码。同时,IDataAdapter 接口还支持数据映射和数据验证等高级功能,使得我们可以更好地处理复杂的数据操作。

桥接模式(Bridge)的结构与对象适配器类似,但是 Bridge 模式的出发点不同。Bridge 的目的是将接口部分和实现部分分离,从而可以对它们较为容易也相对独立地加以改变。而 Adapter  则意味着改变一个已有对象的接口。

装饰模式(Decorator)增强了其他对象的功能而同时又不改变它的接口,因此 Decorator 对应用程序的透明性比适配器要好。结果是 Decorator 支持递归组合,而纯粹使用适配器是不可能实现这一点的。

代理模式(Proxy)在不改变它的接口的条件下,为另一个对象定义了一个代理。