背景
适配器模式(Adapter Pattern)是作为两个不兼容的接口之间的桥梁。这种类型的设计模式属于结构型模式,它结合了两个独立接口的功能。此模式应用到C语言中,跟装饰者和代理这两种模式很接近,所以这里把这三个放一起讲,这三种模式在C语言里经常用到,叫做接口封装。
名词释义
适配器别名:包装器。适配器最早出现在电工学里,有些国家用的是110V市电,而有些则是220V,但对于笔记本电脑来讲不可能兼容这两种电源,也不可能说因为要统一使用笔记本电脑,把各个国家的电网统一规格,那不现实,于是就诞生了适配器这种东西,把110V或220V的电转成笔记本电脑可以用的电压。用在软件上也一样,如果当前系统无法兼容使用一个类,可以通过新建一个适配器类对其包装成系统可用的接口。
装饰者,在使用接口时,保留原本功能的同时,再添加一些其他功能,这就是装饰者。就比如买一套房子,刚买来的时候,同一个小区的房子可能都长一样的,但每个家庭装修的风格不同,给房子赋予了不同的功能,这些房子都有居住的属性,但有人作为家,有人作为公司,有人当作出租屋。
代理,顾名思义,使用者不直接使用各种类型的接口,而是通过一个中间人的角色去使用各种业务。就比如律师,一般人不可能去熟读法律条文,于是就诞生了律师这个职业,以此来代理法律相关的业务,律师就是处理法律业务的代理人。
C语言应用
在C语言中一般是用在函数接口上。当你想使用一个函数,而这个函数传参多了几个你并不关心的参数时,可以使用适配器把接口重新包装一下,只留下需要的参数。那对于这三者之间有什么区别呢,这里大致梳理了一下差异点:
模式 | 差异点 |
---|---|
适配器 | 接口不一致,功能一样 |
装饰者 | 接口一致,但在原本基础上添加功能 |
代理 | 接口功能一致,替换不同的实现 |
例子
- 适配器
举个栗子,STM32F1的LL库中,设置串口波特率的接口需要传3个参数,设置波特率的接口如下:
/**
* @brief Configure USART BRR register for achieving expected Baud Rate value.
* @note Compute and set USARTDIV value in BRR Register (full BRR content)
* according to used Peripheral Clock, Oversampling mode, and expected Baud Rate values
* @note Peripheral clock and Baud rate values provided as function parameters should be valid
* (Baud rate value != 0)
* @rmtoll BRR BRR LL_USART_SetBaudRate
* @param USARTx USART Instance
* @param PeriphClk Peripheral Clock
* @param BaudRate Baud Rate
* @retval None
*/
__STATIC_INLINE void LL_USART_SetBaudRate(USART_TypeDef *USARTx, uint32_t PeriphClk, uint32_t BaudRate)
{
USARTx->BRR = (uint16_t)(__LL_USART_DIV_SAMPLING16(PeriphClk, BaudRate));
}
但对于使用者来讲,完全没有必要知道当前时钟频率要设置多少,只需要关注当前设置哪个串口波特率值为多少即可。所以这里的接口可以简化,即对用户层进行接口封装,屏蔽掉部分信息。这就是适配器最简单的应用。
void SetUartBaudRate(USART_TypeDef *USARTx, uint32_t BaudRate)
{
/* 假设当前时钟频率为1MHz,这里用获取系统时钟的接口获取频率会更通用些。 */
LL_USART_SetBaudRate(USARTx, 1000000, BaudRate);
}
- 装饰者
还是以设置波特率为例,因为当前库里提供的接口就只有设置串口波特率的作用,如果现在要实现设置波特率后,在下次掉电上电后,还可以保持当前的波特率值,那就需要在设置波特率的同时,把设置的值存入Flash或EEPROM等可掉电保存的介质中。为了让用户每次设置时会保存,我们可以把这个功能封装在设置波特率的接口中。我们在前面的接口基础上进行添加。
extern void EEPROM_Write(uint32_t addr,
uint8_t *data,
uint32_t len);
void BSP_Uart_SetBaudRate(USART_TypeDef *USARTx, uint32_t BaudRate)
{
/* 保存波特率值 */
EEPROM_Write(0, BaudRate, sizeof(BaudRate));
/* 设置波特率 */
SetUartBaudRate(USARTx, BaudRate);
}
- 代理
当做一些通信相关的应用时,其实应用层并不需要去关心底层是通过什么方式进行通信的,只需要知道要发送什么数据,然后什么时候去获取数据。所以对于通信,最基本的可以抽象出两个接口,发送和接收。对于底层,则需要根据其物理特性去实现收跟发这两个接口。以IIC和串口为例,可以有如下操作。
#include <string.h>
#include <stdint.h>
/* 定义代理接口 */
struct tagCommAPI
{
void (*Send)(uint8_t *, uint32_t);
void (*Recv)(uint8_t **, uint32_t);
}CommAPI;
/* 串口的接口实现 */
void Uart_Send(uint8_t *data, uint32_t len)
{
/* 串口发送数据操作 */
}
void Uart_Recv(uint8_t **data, uint32_t len)
{
/* 串口接收数据操作 */
}
/* IIC的接口实现 */
void IIC_Send(uint8_t *data, uint32_t len)
{
/* IIC发送数据操作 */
}
void IIC_Recv(uint8_t **data, uint32_t len)
{
/* IIC接收数据操作 */
}
uint8_t buff[] = "Hello,world!";
int main(void)
{
/* 初始化为IIC操作 */
CommAPI.Send = IIC_Send;
CommAPI.Recv = IIC_Recv;
/* 使用IIC进行收发 */
CommAPI.Send(buff, strlen(buff));
CommAPI.Recv(&buff, 12);
/* 初始化为串口操作 */
CommAPI.Send = Uart_Send;
CommAPI.Recv = Uart_Recv;
/* 使用Uart进行收发 */
CommAPI.Send(buff, strlen(buff));
CommAPI.Recv(&buff, 12);
return 0;
}
实际应用中典型的用法,以FreeModbus为例,里面把RTU和ASCII的驱动操作抽象成几个接口,选用不同类型时,切换接口的实现,以此来实现RTU和ASCII的切换。以下为FreeModbus源码的一部分,可以参考观摩一下。
/* ----------------------- Prototypes 0-------------------------------------*/
typedef void ( *pvMBFrameStart ) ( void );
typedef void ( *pvMBFrameStop ) ( void );
typedef eMBErrorCode( *peMBFrameReceive ) ( UCHAR * pucRcvAddress,
UCHAR ** pucFrame,
USHORT * pusLength );
typedef eMBErrorCode( *peMBFrameSend ) ( UCHAR slaveAddress,
const UCHAR * pucFrame,
USHORT usLength );
typedef void( *pvMBFrameClose ) ( void );
/* Functions pointer which are initialized in eMBInit( ). Depending on the
* mode (RTU or ASCII) the are set to the correct implementations.
* Using for Modbus Slave
*/
static peMBFrameSend peMBFrameSendCur;
static pvMBFrameStart pvMBFrameStartCur;
static pvMBFrameStop pvMBFrameStopCur;
static peMBFrameReceive peMBFrameReceiveCur;
static pvMBFrameClose pvMBFrameCloseCur;
/* Callback functions required by the porting layer. They are called when
* an external event has happend which includes a timeout or the reception
* or transmission of a character.
* Using for Modbus Slave
*/
BOOL( *pxMBFrameCBByteReceived ) ( void );
BOOL( *pxMBFrameCBTransmitterEmpty ) ( void );
BOOL( *pxMBPortCBTimerExpired ) ( void );
BOOL( *pxMBFrameCBReceiveFSMCur ) ( void );
BOOL( *pxMBFrameCBTransmitFSMCur ) ( void );
/* ----------------------- Start implementation -----------------------------*/
eMBErrorCode
eMBInit( eMBMode eMode, UCHAR ucSlaveAddress, UCHAR ucPort, ULONG ulBaudRate, eMBParity eParity )
{
eMBErrorCode eStatus = MB_ENOERR;
/* check preconditions */
if( ( ucSlaveAddress == MB_ADDRESS_BROADCAST ) ||
( ucSlaveAddress < MB_ADDRESS_MIN ) || ( ucSlaveAddress > MB_ADDRESS_MAX ) )
{
eStatus = MB_EINVAL;
}
else
{
ucMBAddress = ucSlaveAddress;
switch ( eMode )
{
#if MB_SLAVE_RTU_ENABLED > 0
case MB_RTU:
pvMBFrameStartCur = eMBRTUStart;
pvMBFrameStopCur = eMBRTUStop;
peMBFrameSendCur = eMBRTUSend;
peMBFrameReceiveCur = eMBRTUReceive;
pvMBFrameCloseCur = MB_PORT_HAS_CLOSE ? vMBPortClose : NULL;
pxMBFrameCBByteReceived = xMBRTUReceiveFSM;
pxMBFrameCBTransmitterEmpty = xMBRTUTransmitFSM;
pxMBPortCBTimerExpired = xMBRTUTimerT35Expired;
eStatus = eMBRTUInit( ucMBAddress, ucPort, ulBaudRate, eParity );
break;
#endif
#if MB_SLAVE_ASCII_ENABLED > 0
case MB_ASCII:
pvMBFrameStartCur = eMBASCIIStart;
pvMBFrameStopCur = eMBASCIIStop;
peMBFrameSendCur = eMBASCIISend;
peMBFrameReceiveCur = eMBASCIIReceive;
pvMBFrameCloseCur = MB_PORT_HAS_CLOSE ? vMBPortClose : NULL;
pxMBFrameCBByteReceived = xMBASCIIReceiveFSM;
pxMBFrameCBTransmitterEmpty = xMBASCIITransmitFSM;
pxMBPortCBTimerExpired = xMBASCIITimerT1SExpired;
eStatus = eMBASCIIInit( ucMBAddress, ucPort, ulBaudRate, eParity );
break;
#endif
default:
eStatus = MB_EINVAL;
break;
}
if( eStatus == MB_ENOERR )
{
if( !xMBPortEventInit( ) )
{
/* port dependent event module initalization failed. */
eStatus = MB_EPORTERR;
}
else
{
eMBCurrentMode = eMode;
eMBState = STATE_DISABLED;
}
}
}
return eStatus;
}
适用范围
- 在对接双方短期内难以调整接口的情况,一般在日常维护型的项目中会较多使用。
- 在使用第三方API时,可以通过适配器对第三方接口进行转换适配自己的系统。
优势
- 快速对接两个不同接口。
- 修改一个模块接口,不会直接影响到其他使用它的接口。
- 对于代理模式来讲,可以很容易地替换不同的功能接口。
劣势
- 适配器和装饰器过多的使用会造成系统整体混乱,长期方案还是需要通过调整接口进行合理对接。
- 代理的使用可以很好地规范后面开发的接口,但同时也导致接口不够灵活,如果后期存在新的开发接口当前代理接口无法满足时,可能会导致涉及大部分代码的重构。