【知识分享】C语言中的设计模式——适配器、装饰者和代理

【知识分享】C语言中的设计模式——适配器、装饰者和代理,第1张

背景

    适配器模式(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 
#include 

/* 定义代理接口 */
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;
}
适用范围
  1. 在对接双方短期内难以调整接口的情况,一般在日常维护型的项目中会较多使用。
  2. 在使用第三方API时,可以通过适配器对第三方接口进行转换适配自己的系统。
优势
  1. 快速对接两个不同接口。
  2. 修改一个模块接口,不会直接影响到其他使用它的接口。
  3. 对于代理模式来讲,可以很容易地替换不同的功能接口。
劣势
  1. 适配器和装饰器过多的使用会造成系统整体混乱,长期方案还是需要通过调整接口进行合理对接。
  2. 代理的使用可以很好地规范后面开发的接口,但同时也导致接口不够灵活,如果后期存在新的开发接口当前代理接口无法满足时,可能会导致涉及大部分代码的重构。

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/langs/2889633.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-09-14
下一篇 2022-09-14

发表评论

登录后才能评论

评论列表(0条)

保存