前面几篇我们讲的是创建型模式。它们解决的问题主要是:对象该怎么创建一整套对象该怎么统一创建复杂对象该怎么一步步构建模板对象该怎么通过复制快速生成变体但真实工程里,光把对象创建出来还不够。更常见的问题是:对象虽然已经有了,但接口对不上。比如车企软件里很常见的场景:新平台统一使用ITransport接口,但老模块还是SendFrame()/RecvFrame()上层诊断框架希望调用统一的Connect()/Send()/Receive(),但不同供应商 SDK 的接口名字、参数、返回值完全不一样新系统要求所有设备都实现统一控制接口,但某个老设备库只有一套历史 API业务层想统一处理 CAN、DoIP、串口、仿真通信对象,但这些对象原本长得都不一样这时问题就变了。我们不再关心:这个对象怎么创建?而是关心:这个已经存在的类,怎么接入我现在这套接口体系?这就是适配器模式要解决的问题。一、先从一个车企场景说起假设我们现在在做一个统一诊断通信框架。上层业务希望依赖统一接口:classITransport{public:virtual~ITransport()=default;virtualboolConnect()=0;virtualboolSend(conststd::vectoruint8_tdata)=0;virtualstd::vectoruint8_tReceive()=0;};这样上层代码就可以不关心底层到底是:CANDoIP串口仿真通道只要拿到ITransport就能工作。但问题来了。系统里已经有一个老的 CAN 通信库,接口长这样:classLegacyCanChannel{public:boolOpenChannel(intchannelId);intWriteFrame(constuint8_t*data,size_t len);intReadFrame(uint8_t*buffer,size_t maxLen);};你会发现,它和新接口根本对不上。新接口是:Connect() Send(vectoruint8_t) Receive()老接口是:OpenChannel(channelId) WriteFrame(data, len) ReadFrame(buffer, maxLen)这时你有几种选择。第一,直接修改老库。但很多时候做不到:老库是第三方提供的老库已经在多个系统里使用改老库风险太高老库代码权限不在你手里第二,在业务代码里到处写转换逻辑。比如:if(useLegacyCan){legacyCan.OpenChannel(1);legacyCan.WriteFrame(...);}elseif(useDoip){doip.Connect(...);doip.Send(...);}这段代码也能跑。但问题很明显。第一,业务代码开始知道底层各种细节。第二,接口转换逻辑散落在各处。第三,新增一种通信方式时,到处都要改。第四,统一抽象名义上存在,实际上业务层还是被具体实现绑住了。所以这里真正的问题不是“没有通信对象”。而是:已有对象能不能在不改业务代码、不改老库的前提下,被接到新接口体系里?适配器模式,就是为了解决这个问题。二、为什么“接口不兼容”是工程里特别常见的问题?很多人第一次学适配器模式时,会觉得:不就是换个函数名吗?真实工程远不止这么简单。1. 历史系统和新系统演进节奏不同一个系统往往不是从零重写。它通常是:老模块继续跑新模块逐步替换新框架引入统一抽象老接口暂时不能动所以接口不兼容几乎是必然的。2. 第三方供应商接口风格不一致比如两个供应商都提供雷达 SDK。A 供应商接口可能是:Init() Start() GetObjects() Stop()B 供应商接口可能是:OpenDevice() ReadTargets() CloseDevice()它们都能完成目标识别。但上层业务不应该跟着供应商风格一起变化。这时就很适合做一层适配。3. 新架构通常会引入统一抽象比如你设计了一套统一接口:ITransportISensorIBrakeControllerIVehicleStateReader这些抽象的价值就在于:上层不依赖具体实现。但如果系统里已经有大量历史实现,就会遇到一个现实问题:这些老实现并不是按你的新抽象写的。这时你不能指望所有历史代码都重构一遍。适配器就是一条现实可走的路。4. 协议、数据格式、调用方式都可能不同接口不兼容不只是方法名不同。还可能包括:参数类型不同返回值不同调用顺序不同同步/异步模型不同异常/错误码风格不同数据单位不同数据结构不同比如新接口传std::vectoruint8_t,老接口要的是裸指针和长度。这时光靠重命名是解决不了问题的。需要一层明确的转换逻辑。三、适配器模式到底是什么?适配器模式可以这样理解:把一个已有类的接口转换成客户端希望的另一个接口,使原本因为接口不兼容而不能一起工作的类可以协同工作。再说得直白一点:调用方只认识新接口,适配器负责把调用翻译成老对象能听懂的方式。它关注的不是:创建哪个对象复杂对象怎么构建用哪个模板复制对象它关注的是:两边都已有了,但接口对不上,怎么接起来。所以适配器模式是典型的结构型模式。它解决的是对象之间的“连接关系”。四、适配器模式解决的核心问题适配器模式最核心的价值有三个。1. 让调用方依赖统一抽象业务层希望看到的是:transport-Connect();transport-Send(data);autoresp=transport-Receive();它不希望知道:老库叫OpenChannel某个 SDK 要传裸指针某个接口返回的是int某个通道要先Initialize()再Activate()适配器可以把这些差异藏起来。2. 复用已有实现,而不是强行重写很多老类其实并不差。它们只是接口风格不符合新架构。如果为了“接口统一”就把老代码重写一遍,成本和风险都很高。适配器让你可以:保留老实现,重接一层新接口。3. 把转换逻辑集中管理没有适配器时,业务层可能到处写转换:legacy.WriteFrame(data.data(),data.size());或者:autocount=legacy.ReadFrame(buffer,1024);returnstd::vectoruint8_t(buffer,buffer+count);这些逻辑如果散落在多个地方,很快就乱。适配器把它们集中起来,职责更清楚。五、适配器模式的核心角色适配器模式通常有几个角色。1. Target:目标接口也就是调用方真正希望依赖的接口。比如:ITransport ISensor IDeviceController IVehicleStateReader调用方只面向 Target 编程。2. Adaptee:被适配者也就是已经存在、但接口不兼容的类。比如:LegacyCanChannel VendorRadarSdk OldBrakeModule ThirdPartyDoipClient这些类往往不能直接改,或者不值得改。3. Adapter:适配器适配器实现 Target 接口,并在内部持有或继承 Adaptee,把调用翻译过去。它的职责就是:把调用方的语言,翻译成老对象能听懂的语言。4. Client:调用方调用方只依赖 Target,不直接碰 Adaptee。这正是适配器存在的意义。六、适配器模式的结构可以先用一个简化结构理解:调用方 Client ↓ 依赖 目标接口 Target ↑ 实现 适配器 Adapter ↓ 调用 被适配者 Adaptee用 UML 简化表示:
适配器模式:新老接口不兼容时怎么优雅接起来
前面几篇我们讲的是创建型模式。它们解决的问题主要是:对象该怎么创建一整套对象该怎么统一创建复杂对象该怎么一步步构建模板对象该怎么通过复制快速生成变体但真实工程里,光把对象创建出来还不够。更常见的问题是:对象虽然已经有了,但接口对不上。比如车企软件里很常见的场景:新平台统一使用ITransport接口,但老模块还是SendFrame()/RecvFrame()上层诊断框架希望调用统一的Connect()/Send()/Receive(),但不同供应商 SDK 的接口名字、参数、返回值完全不一样新系统要求所有设备都实现统一控制接口,但某个老设备库只有一套历史 API业务层想统一处理 CAN、DoIP、串口、仿真通信对象,但这些对象原本长得都不一样这时问题就变了。我们不再关心:这个对象怎么创建?而是关心:这个已经存在的类,怎么接入我现在这套接口体系?这就是适配器模式要解决的问题。一、先从一个车企场景说起假设我们现在在做一个统一诊断通信框架。上层业务希望依赖统一接口:classITransport{public:virtual~ITransport()=default;virtualboolConnect()=0;virtualboolSend(conststd::vectoruint8_tdata)=0;virtualstd::vectoruint8_tReceive()=0;};这样上层代码就可以不关心底层到底是:CANDoIP串口仿真通道只要拿到ITransport就能工作。但问题来了。系统里已经有一个老的 CAN 通信库,接口长这样:classLegacyCanChannel{public:boolOpenChannel(intchannelId);intWriteFrame(constuint8_t*data,size_t len);intReadFrame(uint8_t*buffer,size_t maxLen);};你会发现,它和新接口根本对不上。新接口是:Connect() Send(vectoruint8_t) Receive()老接口是:OpenChannel(channelId) WriteFrame(data, len) ReadFrame(buffer, maxLen)这时你有几种选择。第一,直接修改老库。但很多时候做不到:老库是第三方提供的老库已经在多个系统里使用改老库风险太高老库代码权限不在你手里第二,在业务代码里到处写转换逻辑。比如:if(useLegacyCan){legacyCan.OpenChannel(1);legacyCan.WriteFrame(...);}elseif(useDoip){doip.Connect(...);doip.Send(...);}这段代码也能跑。但问题很明显。第一,业务代码开始知道底层各种细节。第二,接口转换逻辑散落在各处。第三,新增一种通信方式时,到处都要改。第四,统一抽象名义上存在,实际上业务层还是被具体实现绑住了。所以这里真正的问题不是“没有通信对象”。而是:已有对象能不能在不改业务代码、不改老库的前提下,被接到新接口体系里?适配器模式,就是为了解决这个问题。二、为什么“接口不兼容”是工程里特别常见的问题?很多人第一次学适配器模式时,会觉得:不就是换个函数名吗?真实工程远不止这么简单。1. 历史系统和新系统演进节奏不同一个系统往往不是从零重写。它通常是:老模块继续跑新模块逐步替换新框架引入统一抽象老接口暂时不能动所以接口不兼容几乎是必然的。2. 第三方供应商接口风格不一致比如两个供应商都提供雷达 SDK。A 供应商接口可能是:Init() Start() GetObjects() Stop()B 供应商接口可能是:OpenDevice() ReadTargets() CloseDevice()它们都能完成目标识别。但上层业务不应该跟着供应商风格一起变化。这时就很适合做一层适配。3. 新架构通常会引入统一抽象比如你设计了一套统一接口:ITransportISensorIBrakeControllerIVehicleStateReader这些抽象的价值就在于:上层不依赖具体实现。但如果系统里已经有大量历史实现,就会遇到一个现实问题:这些老实现并不是按你的新抽象写的。这时你不能指望所有历史代码都重构一遍。适配器就是一条现实可走的路。4. 协议、数据格式、调用方式都可能不同接口不兼容不只是方法名不同。还可能包括:参数类型不同返回值不同调用顺序不同同步/异步模型不同异常/错误码风格不同数据单位不同数据结构不同比如新接口传std::vectoruint8_t,老接口要的是裸指针和长度。这时光靠重命名是解决不了问题的。需要一层明确的转换逻辑。三、适配器模式到底是什么?适配器模式可以这样理解:把一个已有类的接口转换成客户端希望的另一个接口,使原本因为接口不兼容而不能一起工作的类可以协同工作。再说得直白一点:调用方只认识新接口,适配器负责把调用翻译成老对象能听懂的方式。它关注的不是:创建哪个对象复杂对象怎么构建用哪个模板复制对象它关注的是:两边都已有了,但接口对不上,怎么接起来。所以适配器模式是典型的结构型模式。它解决的是对象之间的“连接关系”。四、适配器模式解决的核心问题适配器模式最核心的价值有三个。1. 让调用方依赖统一抽象业务层希望看到的是:transport-Connect();transport-Send(data);autoresp=transport-Receive();它不希望知道:老库叫OpenChannel某个 SDK 要传裸指针某个接口返回的是int某个通道要先Initialize()再Activate()适配器可以把这些差异藏起来。2. 复用已有实现,而不是强行重写很多老类其实并不差。它们只是接口风格不符合新架构。如果为了“接口统一”就把老代码重写一遍,成本和风险都很高。适配器让你可以:保留老实现,重接一层新接口。3. 把转换逻辑集中管理没有适配器时,业务层可能到处写转换:legacy.WriteFrame(data.data(),data.size());或者:autocount=legacy.ReadFrame(buffer,1024);returnstd::vectoruint8_t(buffer,buffer+count);这些逻辑如果散落在多个地方,很快就乱。适配器把它们集中起来,职责更清楚。五、适配器模式的核心角色适配器模式通常有几个角色。1. Target:目标接口也就是调用方真正希望依赖的接口。比如:ITransport ISensor IDeviceController IVehicleStateReader调用方只面向 Target 编程。2. Adaptee:被适配者也就是已经存在、但接口不兼容的类。比如:LegacyCanChannel VendorRadarSdk OldBrakeModule ThirdPartyDoipClient这些类往往不能直接改,或者不值得改。3. Adapter:适配器适配器实现 Target 接口,并在内部持有或继承 Adaptee,把调用翻译过去。它的职责就是:把调用方的语言,翻译成老对象能听懂的语言。4. Client:调用方调用方只依赖 Target,不直接碰 Adaptee。这正是适配器存在的意义。六、适配器模式的结构可以先用一个简化结构理解:调用方 Client ↓ 依赖 目标接口 Target ↑ 实现 适配器 Adapter ↓ 调用 被适配者 Adaptee用 UML 简化表示: