如何编写可重用代码

[复制链接]
1093|8
手机看帖
扫描二维码
随时随地手机跟帖
会笑的星星|  楼主 | 2020-4-7 11:40 | 显示全部楼层 |阅读模式
本帖最后由 会笑的星星 于 2020-4-18 10:12 编辑

编写可重用的代码对于程序员而言的重要性是不言自明的,关键在于如何编写可重用的代码,说实话,这并不简单,但我依然尝试讲讲。要让代码具有良好的可重用性,我们必须要对代码做适当的抽象,即把代码的共同部分抽象出来,把变化分离出去。这里,我还是以串口打印为例,来说明如何编写可重用代码,先描述一下我们要解决的问题。

编写一个串口打印模块,要求该串口模块能提供两个面向应用层的接口,一个是打印字符串,另一个是打印十六进制数据。并要求串口模块具有良好的扩展性与重用性,能方便的扩展为多组串口且发送数据的相关接口能共用,如下图所示。

如何编写可重用代码.png

我们先来分析一下这个问题。按要求,虽然我们需要扩展多组串口,但是由于每组串口有相同的部分,即针对串口缓冲区的操作方式以及把数据写入到硬件串口寄存器中。不同的部分则是不同组的串口对应不同的串口缓冲区。基于串口模块的这些特点,我们可以把相同部分给抽象出来,不同部分分离出去,具体的做法如下。

先看看app_set.h文件。

//app_ser.h

#ifndef _APP_SER_
#define _APP_SER_

//串口组的数量,如果需要更多的串口,则修改整个数字即可
#define SER_NUM 2

//每组串口对应的编号
typedef enum
{
  SER0 = (unsigned char)0,
  SER1,
  SER2,
  SER3,
  SER4,
  SER5
}ser_num_t;

//指向将数据写入串口硬件寄存器的函数,具体见后文
typedef void (*hal_wt_data_t)(unsigned char dt);

typedef struct
{
  unsigned int   len;
  unsigned char  pos;
  unsigned char  count;
  unsigned char  *fifo;
  unsigned char fifo_len;
}tx_t;

//特别注意:ser_t类型就是不同的串口组相同部分,具体应用见后文
typedef struct
{
  tx_t tx;
  hal_wt_data_t hal_wt_data;
}ser_t;

//发送十六进制数据,其中ser_num为串口编号,ptxd指向待发送数据的缓冲区,len表示待发送数据长度
extern void serial_send_hex(ser_num_t ser_num, unsigned char *ptxd, unsigned char len);

//发送字符串
extern void serial_send_str(ser_num_t ser_num, unsigned char *ptxd);

//初始化串口组,具体见后文
extern void serial_init(ser_num_t ser_num, ser_t *p);

//串口缓冲区数据发送管理,具体见后文
extern void serial_send_manage(void );


#endif

对于app_ser.h中的代码,要留意ser_t类型,因为这个类型抽象了多组串口之间的共同行为。具体的使用可见app_ser.c文件。这个文件中的函数功能以及原理之前在"串口模块"那篇**中说过,这里只简单的说一下功能,要理解具体的含义,可能需要先去看看那篇**,我把**的链接放在末尾,有需要的可以去看看。

//app_ser.c

#include "string.h"
#include "app_ser.h"

//定义串口组变量
ser_t ser[SER_NUM];

//判断串口缓冲区是否已经有数据在发送
void serial_send_start(ser_num_t ser_num)
{
   ser_t *ps;
   
   ps = &ser[ser_num];
   if(ps == 0)
   {
    return;
   }
   
   if(ps->tx.len != 0)
   {
         return;
   }
   ps->tx.count = ps->tx.pos;
}

//发送十六进制数据,其中ser_num为串口组编号,ptxd为待发送数据缓冲区,len为待发送数据长度
void serial_send_hex(ser_num_t ser_num, unsigned char *ptxd, unsigned char len)
{
   unsigned char i;
   ser_t *ps;
   
   if(ser_num >= SER_NUM)
   {
         return;  
   }
   
   serial_send_start(ser_num);
   
   ps = &ser[ser_num];
   if(ps == 0)
   {
    return;
   }
   
   for(i = 0 ; i < len; i++)
   {
     ps->tx.fifo[ps->tx.pos] = *ptxd++;
        
         if(++ps->tx.pos >= ps->tx.fifo_len)
         {
           ps->tx.pos = 0;         
         }
   }
   ps->tx.len += len;
}

void serial_send_str(ser_num_t ser_num, unsigned char *ptxd)
{
  unsigned char len;
  
  len = strlen((char *)ptxs);
  serial_send_hex(ser_num, ptxs, len);        
}

//串口缓冲区数据发送管理函数,这个函数需要放在一个定时器中,比如如果串口波特率是9600bps,
//则发送一个字节所需要的时间至少1ms,那么这个函数必须要放在定时间隔超过1ms的定时器中调用
void serial_send_manage(void )
{
  unsigned char i;         
  ser_t *ps;
         
  for(i = 0; i < SER_NUM; i++)
  {
        ps = &ser[i];
        if(ps != 0)
        {
          if(ps->tx.len)
         {
            ps->hal_wt_data(ps->tx.fifo[ps->tx.count]);
         
            if(++ps->tx.count >= ps->tx.fifo_len)
            {
              ps->tx.count = 0;         
            }
         
            ps->tx.len--;
         }         
       }
  }         
}

void serial_init(ser_num_t ser_num, ser_t *p)
{
     ser_t *ps;        
        
     if(ser_num >= SER_NUM)
     {
           return;  
     }
     ps = &ser[ser_num];
    ps->tx.len = 0;
    ps->tx.pos = 0;
    ps->tx.count = 0;
    ps->tx.fifo = p->tx.fifo;
    ps->tx.fifo_len = p->tx.fifo_len ;
    ps->hal_wt_data = p->hal_wt_data;
}

从app_ser.c中可以看出来,不论是定义了几组串口,每组串口的操作方式都是一样的,使用不同组的串口发送接口都是serial_send_str()以及serial_send_hex()。唯一不同的是具体的串口数据缓冲区以及相关的参数不一样,对于不一样的部分,我们通过serial_init()接口让应用者去设置,从而把变化分离出去,具体应用见下面的代码。

//应用层代码
#include "app_ser.h"

//串口缓冲区长度
#define SER0_FIFO_TX_LEN 50
#define SER1_FIFO_TX_LEN 100

//定义两个串口缓冲区
unsigned char ser0[SER0_FIFO_TX_LEN];
unsigned char ser1[SER1_FIFO_TX_LEN];

unsigned char test1[10] = {0x01,0x02,0x03,0x04,0x05};
unsigned char test2[10] = {0xaa,0xab,0xac,0xad,0xaf};


void hal_uart0_set_data(unsigned char dt)
{
  SBUF0 = dt; //数据写入硬件串口寄存器        
}

void hal_uart1_set_data(unsigned char dt)
{
  SBUF1 = dt;//数据写入硬件串口寄存器        
}

main()
{
  ser_t s;        
         
  s.tx.fifo = ser0;
  s.tx.fifo_len = SER0_FIFO_TX_LEN;
  s.hal_wt_data = hal_uart0_set_data;
  serial_init(SER0,&s);

  s.tx.fifo = ser1;
  s.tx.fifo_len = SER1_FIFO_TX_LEN;
  s.hal_wt_data = hal_uart1_set_data;
  serial_init(SER1,&s);
  
  //使用串口0发送test1前5个字节数据
  serial_send_hex(SER0, test1, 5);
  //使用串口1发送test2前5个字节数据
  serial_send_hex(SER1, test2, 5);
  
  serial_send_str(SER0,"TEST1");
  serial_send_str(SER1,"TEST2");
  
  while(1)
  {
        if(clk_2ms)
        {
          serial_send_manage();        
          clk_2ms = 0;
        }               

  }
}

综合上述的代码,我们可以看到,SER_NUM设置串口组数量,通过公共接口serial_send_hex()、serial_send_str()发送了相关的数据,这样就解决了我们开头描述的问题。

事实上,如果你学过面向对象的编程语言,你肯定知道基类是派生类的功能抽象。你会发现这里的ser_t类型类似于基类,ser数组则相当于该类型的一个派生类,每个派生类(ser[0]、ser[1])由于继承了相同的基类,所以具有相同的成员,这使得不同的串口组具有相同的操作方式。不同的派生类中的成员虽然一致,但是成员的参数却各不相同,这确保了不同的串口组能完成各自的功能,这实际上就类似于面向对象中的一个重要概念 --- 多态(同名函数,但功能却可以各不一样,比如这个例子中hal_wt_data函数指针就可以表现为多态特性)。通过多态,我们可以实现代码的重用性与扩展性,就如我们上述代码展示的那样。

抽象出代码的公共部分,把变化分离出去,这是编写可重用代码的一个重要原则。当我们为了完成某些功能而出现很多雷同的代码时,这说明我们的代码可重用性不高,也提醒我们可能代码需要做进一步的抽象分离了。

原则很简单,但是要在实践中应用却不简单。在我们编程之初,可能并没有很好的抽象意识(这在实际中也常常发生,因为前期编程迫于时间压力以及编程细节的纠缠),很多时候我们需要依靠后续(这时的项目可能都做完了)的代码重构来提高我们对代码的抽象能力。任重道远,一起努力吧。


参考:
串口模块 --- 讲三层架构前的铺垫
https://bbs.21ic.com/icview-2892524-1-1.html

程序的抽象
https://bbs.21ic.com/icview-2892498-1-1.html


使用特权

评论回复

相关帖子

会笑的星星|  楼主 | 2020-4-7 11:41 | 显示全部楼层

使用特权

评论回复
叶春勇| | 2020-4-7 11:51 | 显示全部楼层
好贴,顶着。你这个串口多呀。数据结构应该把串口接收buf,发送buf抽象成一种高级串口。

使用特权

评论回复
会笑的星星|  楼主 | 2020-4-7 12:00 | 显示全部楼层
叶春勇 发表于 2020-4-7 11:51
好贴,顶着。你这个串口多呀。数据结构应该把串口接收buf,发送buf抽象成一种高级串口。 ...

嗯,这里只是一个例子,怕搞的太复杂没人看啊

使用特权

评论回复
叶春勇| | 2020-4-7 12:46 | 显示全部楼层
会笑的星星 发表于 2020-4-7 12:00
嗯,这里只是一个例子,怕搞的太复杂没人看啊

你加个stm32就有人看了。不加的话,权当笔记。子曾经曰过:温故而知新。

使用特权

评论回复
dsyq| | 2020-4-7 17:29 | 显示全部楼层
先顶,晚上看!

使用特权

评论回复
elife| | 2020-4-8 13:07 | 显示全部楼层
函数的重入,应该是变量局部化,也就是不能使用全局变量。

使用特权

评论回复
andy520520| | 2020-12-4 15:09 | 显示全部楼层
你说的概念都搞错了,什么叫可重用?

所以函数都可重用,应该是可重入

使用局部变量就可重入函数了

使用特权

评论回复
andy520520| | 2020-12-6 10:10 | 显示全部楼层
你这个也就玩玩可以
for 用来发送,你不怕挂逼?!  ,  你的fifo 在哪里?

for(i = 0 ; i < len; i++)
   {
     ps->tx.fifo[ps->tx.pos] = *ptxd++;
        
         if(++ps->tx.pos >= ps->tx.fifo_len)
         {
           ps->tx.pos = 0;         
         }
   }

数据结构应该这么玩:

typedef struct _usart
{
    uint8_t usart_buf[FIFO_SIZE];
    uint8_t read;
    uint8_t write;
}usart_fifo_t;


extern idata usart_fifo_t  rx0,tx0;
extern idata usart_fifo_t  rx1,tx1;

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

31

主题

96

帖子

15

粉丝