适配器模式
目录
1 概述
💡在软件开发中,基本上任何问题都可以通过增加一个中间层来解决。适配器模式其实就是一个中间层。综上,适配器模式起着转化/委托的作用,将一种接口转化为另一种符合需求的接口。1
- 适配器模式是一种结构型设计模式。结构型设计模式关注如何组合类和对象以形成更大的结构,旨在简化设计并提高系统的灵活性和可复用性。它分为类结构型模式(通过继承)和对象结构型模式(通过组合或聚合)。
- 适配器模式可以将一个类的接口和另一个类的接口匹配起来,从而使原本因接口不匹配的类可以协同工作,并且无须修改原来的适配者接口和抽象目标类接口。
- 在需要集成旧代码、统一接口或处理不同数据源时。通过适配器模式,可以实现代码的复用、解耦和灵活性。在实际开发中,合理使用适配器模式可以显著提高代码的可维护性和扩展性。
2 主要角色
- 目标接口(Target)
- 这是客户端所期待的接口,定义了客户端需要的操作。
- 适配器(Adapter)
- 适配器的主要职责是将现有类的接口转换为目标接口所期望的格式。
- 适配器充当了目标接口和现有类之间的转换器。它实现了目标接口,并在内部使用现有类的实例来完成实际的操作。
- 现有类(Adaptee)
- 现有类是需要被适配的类,它已经存在并且具有某些有用的功能,但其接口与客户端所期望的目标接口不兼容。
3 适配器模式的两种实现方式
根据适配器类与适配者类的关系不同,适配器模式可分为对象适配器模式
和类适配器模式
两种。
在类适配器模式中,适配器与适配者之间是继承(或实现)关系。
在对象适配器模式中,适配器与适配者之间是关联关系;
在实际开发中,对象适配器模式的使用频率更高。
3.1 类适配器
- 实现:通过继承现有类(Adaptee)并实现目标接口(Target)来实现适配。适配器与适配者之间是继承(或实现)关系。
- 优点:可以重写现有类的方法来改变其行为,
- 缺点:由于继承关系,适配器类会继承现有类的所有方法,可能会导致类的膨胀。并且很多语言并不支持多继承,一个类只能继承一个父类,也就只能适配一个Adaptee。
- 限制:这种方式在某些编程语言中可能会受到多重继承的限制。
3.1.1 示例
- Target
// 目标接口 public interface IMediaPlayer { void Play(string audioType, string fileName); }
- Adaptee
// 现有类 public class AdvancedMediaPlayer { public void PlayVlc(string fileName) { // 播放VLC文件的逻辑 } public void PlayMp4(string fileName) { // 播放MP4文件的逻辑 } }
- Adapter
// 类适配器 public class MediaAdapter : AdvancedMediaPlayer, IMediaPlayer { private string audioType; public MediaAdapter(string audioType) { this.audioType = audioType; } public void Play(string audioType, string fileName) { if (audioType.Equals("vlc", StringComparison.OrdinalIgnoreCase)) { PlayVlc(fileName); } else if (audioType.Equals("mp4", StringComparison.OrdinalIgnoreCase)) { PlayMp4(fileName); } } }
- Client
var vlcmediaPlayer = new MediaAdapter("vlc"); vlcmediaPlayer.Play("SampleVideo.vlc"); var mp4MediaPlayer = new MediaAdapter("mp4"); mp4MediaPlayer.Play("SampleVideo.mp4");
3.1.2 Mermaid图
3.2 对象适配器
- 实现:通过在适配器类内部持有一个现有类(Adaptee)的实例,并实现目标接口(Target)来实现适配。适配器和现有类是关联关系。
- 优点:更加灵活,因为它不依赖于继承,可以适配多个不同的现有类;可以很容易地更换现有类的实现;
- 缺点:无法重写现有类的方法
3.2.1 示例
- Target
// 目标接口 public interface ITarget { void Request(); }
- Adaptee
// 现有类 public class Adaptee { public void SpecificRequest() { Console.WriteLine("Adaptee.SpecificRequest()"); } }
- Adapter
// 对象适配器 public class Adapter : ITarget { private Adaptee adaptee; public Adapter(Adaptee adaptee) { this.adaptee = adaptee; } public void Request() { adaptee.SpecificRequest(); } }
- Client
var adaptee = new Adaptee(); var objectAdapter = new Adapter(adaptee); objectAdapter.Request();
3.2.2 MerMaid类图
3.3 双向适配器模式
- 目标:双向适配器模式允许两个不兼容的接口相互适配,使得它们可以互相调用对方的方法。
- 实现:在适配器类中,同时包含对目标类和适配者类的引用,适配者可以通过它调用目标类中的方法,目标类也可以通过它调用适配者类中的方法。
- 优点:可以实现两个系统的双向兼容
- 缺点:适配器类的实现可能会比较复杂,需要同时处理两个接口的适配逻辑。
- 场景:适用于需要双向交互的场景,例如在两个不同的系统或模块之间进行数据交换和通信时,可以让它们无缝地协同工作。
3.3.1 示例
假设有一个智能窗帘和一个智能温控器,
智能窗帘可以通过调用OpenCurtain
和CloseCurtain
方法来控制开合,
智能温控器可以通过调用IncreaseTemperature
和DecreaseTemperature
方法来调节温度。
现希望智能窗帘能够根据温度自动开合,同时智能温控器也能根据窗帘的状态调整温度,以实现更智能的家居环境控制。
- Adaptee
接口实现
// 智能窗帘接口 public interface ISmartCurtain { void OpenCurtain(); void CloseCurtain(); } // 智能温控器接口 public interface ISmartThermostat { void IncreaseTemperature(); void DecreaseTemperature(); }
具体实现
// 智能窗帘实现类 public class SmartCurtain : ISmartCurtain { public void OpenCurtain() { Console.WriteLine("智能窗帘打开"); } public void CloseCurtain() { Console.WriteLine("智能窗帘关闭"); } } // 智能温控器实现类 public class SmartThermostat : ISmartThermostat { public void IncreaseTemperature() { Console.WriteLine("智能温控器升温"); } public void DecreaseTemperature() { Console.WriteLine("智能温控器降温"); } }
- Adapter
public class SmartHomeAdapter : ISmartCurtain, ISmartThermostat { private ISmartCurtain smartCurtain; private ISmartThermostat smartThermostat; public ISmartCurtain SmartCurtain { set => smartCurtain = value; } public ISmartThermostat SmartThermostat { set => smartThermostat = value; } public void OpenCurtain() { Console.Write("根据温度自动 "); smartCurtain.OpenCurtain(); smartThermostat.DecreaseTemperature(); // 窗帘打开时,适当降温 } public void CloseCurtain() { Console.Write("根据温度自动 "); smartCurtain.CloseCurtain(); smartThermostat.IncreaseTemperature(); // 窗帘关闭时,适当升温 } public void IncreaseTemperature() { Console.Write("根据窗帘状态自动 "); smartThermostat.IncreaseTemperature(); smartCurtain.CloseCurtain(); // 升温时,关闭窗帘以保持温度 } public void DecreaseTemperature() { Console.Write("根据窗帘状态自动 "); smartThermostat.DecreaseTemperature(); smartCurtain.OpenCurtain(); // 降温时,打开窗帘以促进空气流通 } }
- Client
public class Program { public static void Main() { ISmartCurtain smartCurtain = new SmartCurtain(); ISmartThermostat smartThermostat = new SmartThermostat(); SmartHomeAdapter adapter = new SmartHomeAdapter(); adapter.SmartCurtain = smartCurtain; adapter.SmartThermostat = smartThermostat; // 根据温度自动控制窗帘和温控器 ISmartCurtain curtainAdapter = adapter; curtainAdapter.OpenCurtain(); // 根据温度自动打开窗帘并适当降温 curtainAdapter.CloseCurtain(); // 根据温度自动关闭窗帘并适当升温 // 根据窗帘状态自动调整温度 ISmartThermostat thermostatAdapter = adapter; thermostatAdapter.IncreaseTemperature(); // 根据窗帘状态自动升温并关闭窗帘 thermostatAdapter.DecreaseTemperature(); // 根据窗帘状态自动降温并打开窗帘 } }
3.3.2 MerMaid 类图
3.4 缺省适配器模式
- 目标:适用于接口中有多个方法,但客户端只需要使用其中部分方法的情况。
- 实现:通过创建一个抽象类来实现接口,并为接口中的每个方法提供一个默认的空实现,具体子类只需重写需要使用的方法,而不需要实现接口中的所有方法。
- 这种模式适用于接口中有多个方法,但大多数方法在某些情况下不需要实现的场景。它可以简化适配器类的实现,避免了实现大量空方法的繁琐。
- 优点:可以减少代码冗余,提高开发效率
- 缺点:可能会隐藏一些需要实现的方法,导致子类开发者忘记实现某些重要的方法。
3.4.1 示例
- Target
// 目标接口 public interface IDevice { void PowerOn(); void PowerOff(); void Reset(); void Upgrade(); }
- Adapter
// 缺省适配器 public abstract class DefaultDeviceAdapter : IDevice { public virtual void PowerOn() { // 默认空实现 } public virtual void PowerOff() { // 默认空实现 } public virtual void Reset() { // 默认空实现 } public virtual void Upgrade() { // 默认空实现 } } // 具体适配器类 public class ConcreteDeviceAdapter : DefaultDeviceAdapter { public override void PowerOn() { Console.WriteLine("设备开机"); } public override void PowerOff() { Console.WriteLine("设备关机"); } }
- Client
IDevice device = new ConcreteDeviceAdapter(); device.PowerOn(); // 输出:设备开机 device.PowerOff(); // 输出:设备关机
3.4.2 Mermaid类图
4 适用场景
适配器模式主要用于解决接口不兼容的问题,使得原本无法协同工作的类或系统能够顺利交互。以下是适配器模式适用的主要场景:
- 兼容性问题导向
- 接口不匹配是核心痛点:无论是第三方库、旧代码、外部系统,还是不同硬件设备等,场景中都存在接口不一致的问题。
- 涉及不同系统或组件的交互:场景都围绕着不同系统、组件或模块之间的协同工作。
- 复用与保留需求
- 对已有资源的复用:在复用旧代码、逐步重构系统等场景中,强调对已有代码或模块的再利用。
- 保留旧接口兼容性:在接口升级场景中,需要考虑到旧版本客户端的兼容性。适配器可以作为中间层,将新接口的特性适配到旧接口上,确保旧客户端仍能正常调用服务。
- 统一与转换功能
- 统一接口标准:多个类的接口统一、跨平台开发中API接口统一等场景,目的是将分散的、不一致的接口整合为一个统一的标准。这样可以简化系统的调用逻辑,提高代码的可维护性和可扩展性。
- 数据格式转换:支持多种数据格式的场景中,适配器承担着数据格式转换的任务。
具体场景示例
-
集成第三方库或组件:适配器模式用于解决系统与第三方库接口不兼容的问题,如支付网关接口转换。
-
复用旧代码:适配器模式允许在不修改旧代码的情况下,将其接口适配到新系统需求,如旧日志记录类适配新接口。
-
统一多个类的接口:适配器模式可将多个功能相似但接口不一致的类统一为一致接口,如多种数据库操作接口统一。
-
与外部系统交互:适配器模式用于与接口不兼容的外部系统交互,如物联网应用中不同硬件设备协议的转换。
-
测试驱动开发(TDD):适配器模式可模拟不兼容的依赖项行为,如测试中模拟外部服务的行为。
例如
OrderProcessor
,它依赖于一个外部服务PaymentService
来处理支付。PaymentService
是一个第三方服务,它的接口可能比较复杂,或者它的调用可能会产生副作用(如真实的支付操作)。在测试
OrderProcessor
时,并不希望真正调用PaymentService
,此时就可以使用适配器模式,写一个Mock测试实现IPaymentService
,模拟支持成功的行为,,以便专注于测试OrderProcessor
的逻辑。 -
支持多种数据格式:适配器模式可将不同数据格式转换为统一接口,如从JSON、XML、数据库等数据源读取数据。
-
逐步重构系统:适配器模式用于新旧模块接口不一致时的协同工作,如单体应用拆分为微服务时的接口适配。
-
跨平台开发:适配器模式用于统一不同平台的API接口,如跨平台文件操作工具的开发。
-
硬件抽象:适配器模式用于统一多种硬件设备的接口,如支持多种品牌打印机的驱动系统。
-
接口升级:适配器模式用于在接口升级时保留旧接口的兼容性,如API升级时适配旧版本客户端。
5 适配器模式的优缺点
5.1 优点
- 提高类的复用性
- 现有类可以被复用,而不需要修改其源代码。适配器模式通过创建一个新的适配器类来适配现有类,使得现有类可以在不同的系统或模块中被复用。
- 降低类之间的耦合度
- 客户端与现有类之间的耦合度降低。客户端只需要与适配器的目标接口交互,而不需要直接与现有类的接口打交道。这样,即使现有类的实现发生变化,只要适配器的目标接口保持不变,客户端就不需要修改。
- 增加系统的灵活性和扩展性
- 系统可以更容易地添加新的适配器来适配新的现有类,而不需要修改客户端代码。例如,原本只支持手机号登录,现需要支持微信、邮箱、GitHub等多种登录方式,虽然登录形式丰富,但是登录后的处理逻辑可以不必改,只需要引用适配器模式,使其兼容并支持多种登录模式。
5.2 缺点
- 增加系统的复杂性
- 适配器模式会增加系统的类的数量。每个适配器都是一个单独的类,这可能会使系统的结构变得复杂。
- 可能会导致性能问题
- 适配器模式可能会引入额外的性能开销。因为适配器需要在客户端和现有类之间进行转换,这可能会导致一些额外的函数调用和数据转换操作。例如,在一个性能敏感的实时系统中,适配器的转换操作可能会对系统的性能产生影响。如果适配器的实现不够高效,可能会导致系统的响应时间变长,从而影响用户体验。
6 .NET 中的适配器模式案例
6.1 DataAdapter
:
- 在 ADO.NET 中,
DataAdapter
(如SqlDataAdapter
、OleDbDataAdapter
)是一个典型的适配器模式应用。 - 它将不同数据库(如 SQL Server、Oracle、Access)的操作接口适配到统一的
DataSet
接口。
public void DataAdapterDemoMethod()
{
string connectionString = "connection_string";
string query = "SELECT * FROM Customers";
using (SqlConnection connection = new SqlConnection(connectionString))
{
SqlDataAdapter adapter = new SqlDataAdapter(query, connection);
DataSet dataSet = new DataSet();
// 填充 DataSet
adapter.Fill(dataSet, "Customers");
// 访问 DataSet 中的数据
foreach (DataRow row in dataSet.Tables["Customers"].Rows)
{
Console.WriteLine(row["CustomerName"]);
}
}
}
6.2 HttpClient
和HttpMessageHandler
:
- 在 .NET 的 HTTP 客户端库中,
HttpClient
使用HttpMessageHandler
来处理 HTTP 请求。 - 你可以通过适配器模式自定义
HttpMessageHandler
,以适配不同的 HTTP 请求处理逻辑。
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
class CustomHandler : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
// 在发送请求之前可以添加自定义逻辑
Console.WriteLine("Request sent to: " + request.RequestUri);
// 继续处理请求
HttpResponseMessage response = await base.SendAsync(request, cancellationToken);
// 在收到响应之后可以添加自定义逻辑
Console.WriteLine("Response received with status code: " + response.StatusCode);
return response;
}
}
class Program
{
static async Task Main()
{
HttpClient client = new HttpClient(new CustomHandler());
HttpResponseMessage response = await client.GetAsync("https://jsonplaceholder.typicode.com/posts/1");
if (response.IsSuccessStatusCode)
{
string content = await response.Content.ReadAsStringAsync();
Console.WriteLine(content);
}
}
}
7 适配器模式与其它模式的区别
这一部分等更新了其他设计模式的内容,再进行更新。
8 总结
适配器模式的核心思想是将一个接口转换为另一个接口,使得原本不兼容的接口能够协同工作。它的主要优点是:
- 解耦: 将客户端代码与具体实现解耦。
- 复用: 可以复用现有的类或组件,而无需修改其代码。
- 灵活性: 可以轻松支持新的接口或实现。
适配器模式也不应滥用。如果接口本身设计合理,或者可以通过重构直接统一接口,那么就不需要使用适配器模式。
9 参考
- 《设计模式的艺术》——刘伟
《设计模式就该这样学: 基于经典框架源码和真实业务场景》——谭勇德 ↩︎