如何编写可重用代码——工程师原创应用笔记

发布时间:2020-06-28 阅读量:1106 来源: 我爱方案网 作者:

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


先描述一下我们要解决的问题。


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


wer.jpg


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


先看看app_set.h文件。


  1. //app_ser.h


  2. #ifndef _APP_SER_

  3. #define _APP_SER_


  4. //串口组的数量,如果需要更多的串口,则修改整个数字即可

  5. #define SER_NUM 2


  6. //每组串口对应的编号

  7. typedef enum

  8. {

  9.   SER0 = (unsigned char)0,

  10.   SER1,

  11.   SER2,

  12.   SER3,

  13.   SER4,

  14.   SER5

  15. }ser_num_t;


  16. //指向将数据写入串口硬件寄存器的函数,具体见后文

  17. typedef void (*hal_wt_data_t)(unsigned char dt);


  18. typedef struct

  19. {

  20.   unsigned int   len;

  21.   unsigned char  pos;

  22.   unsigned char  count;

  23.   unsigned char  *fifo;

  24.   unsigned char fifo_len;

  25. }tx_t;


  26. //特别注意:ser_t类型就是不同的串口组相同部分,具体应用见后文

  27. typedef struct

  28. {

  29.   tx_t tx;

  30.   hal_wt_data_t hal_wt_data;

  31. }ser_t;


  32. //发送十六进制数据,其中ser_num为串口编号,ptxd指向待发送数据的缓冲区,len表示待发送数据长度

  33. extern void serial_send_hex(ser_num_t ser_num, unsigned char *ptxd, unsigned char len);


  34. //发送字符串

  35. extern void serial_send_str(ser_num_t ser_num, unsigned char *ptxd);


  36. //初始化串口组,具体见后文

  37. extern void serial_init(ser_num_t ser_num, ser_t *p);


  38. //串口缓冲区数据发送管理,具体见后文

  39. extern void serial_send_manage(void );



  40. #endif

复制代码



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

  1. //app_ser.c


  2. #include "string.h"

  3. #include "app_ser.h"


  4. //定义串口组变量

  5. ser_t ser[SER_NUM];


  6. //判断串口缓冲区是否已经有数据在发送

  7. void serial_send_start(ser_num_t ser_num)

  8. {

  9.    ser_t *ps;

  10.    

  11.    ps = &ser[ser_num];

  12.    if(ps == 0)

  13.    {

  14.     return;

  15.    }

  16.    

  17.    if(ps->tx.len != 0)

  18.    {

  19.          return;

  20.    }

  21.    ps->tx.count = ps->tx.pos;

  22. }


  23. //发送十六进制数据,其中ser_num为串口组编号,ptxd为待发送数据缓冲区,len为待发送数据长度

  24. void serial_send_hex(ser_num_t ser_num, unsigned char *ptxd, unsigned char len)

  25. {

  26.    unsigned char i;

  27.    ser_t *ps;

  28.    

  29.    if(ser_num >= SER_NUM)

  30.    {

  31.          return;  

  32.    }

  33.    

  34.    serial_send_start(ser_num);

  35.    

  36.    ps = &ser[ser_num];

  37.    if(ps == 0)

  38.    {

  39.     return;

  40.    }

  41.    

  42.    for(i = 0 ; i < len; i++)

  43.    {

  44.      ps->tx.fifo[ps->tx.pos] = *ptxd++;

  45.         

  46.          if(++ps->tx.pos >= ps->tx.fifo_len)

  47.          {

  48.            ps->tx.pos = 0;         

  49.          }

  50.    }

  51.    ps->tx.len += len;

  52. }


  53. void serial_send_str(ser_num_t ser_num, unsigned char *ptxd)

  54. {

  55.   unsigned char len;

  56.   

  57.   len = strlen((char *)ptxs);

  58.   serial_send_hex(ser_num, ptxs, len);        

  59. }


  60. //串口缓冲区数据发送管理函数,这个函数需要放在一个定时器中,比如如果串口波特率是9600bps,

  61. //则发送一个字节所需要的时间至少1ms,那么这个函数必须要放在定时间隔超过1ms的定时器中调用

  62. void serial_send_manage(void )

  63. {

  64.   unsigned char i;         

  65.   ser_t *ps;

  66.          

  67.   for(i = 0; i < SER_NUM; i++)

  68.   {

  69.         ps = &ser[i];

  70.         if(ps != 0)

  71.         {

  72.           if(ps->tx.len)

  73.          {

  74.             ps->hal_wt_data(ps->tx.fifo[ps->tx.count]);

  75.          

  76.             if(++ps->tx.count >= ps->tx.fifo_len)

  77.             {

  78.               ps->tx.count = 0;         

  79.             }

  80.          

  81.             ps->tx.len--;

  82.          }         

  83.        }

  84.   }         

  85. }


  86. void serial_init(ser_num_t ser_num, ser_t *p)

  87. {

  88.      ser_t *ps;        

  89.         

  90.      if(ser_num >= SER_NUM)

  91.      {

  92.            return;  

  93.      }

  94.      ps = &ser[ser_num];

  95.     ps->tx.len = 0;

  96.     ps->tx.pos = 0;

  97.     ps->tx.count = 0;

  98.     ps->tx.fifo = p->tx.fifo;

  99.     ps->tx.fifo_len = p->tx.fifo_len ;

  100.     ps->hal_wt_data = p->hal_wt_data;

  101. }


复制代码


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


  1. //应用层代码

  2. #include "app_ser.h"


  3. //串口缓冲区长度

  4. #define SER0_FIFO_TX_LEN 50

  5. #define SER1_FIFO_TX_LEN 100


  6. //定义两个串口缓冲区

  7. unsigned char ser0[SER0_FIFO_TX_LEN];

  8. unsigned char ser1[SER1_FIFO_TX_LEN];


  9. unsigned char test1[10] = {0x01,0x02,0x03,0x04,0x05};

  10. unsigned char test2[10] = {0xaa,0xab,0xac,0xad,0xaf};



  11. void hal_uart0_set_data(unsigned char dt)

  12. {

  13.   SBUF0 = dt; //数据写入硬件串口寄存器        

  14. }


  15. void hal_uart1_set_data(unsigned char dt)

  16. {

  17.   SBUF1 = dt;//数据写入硬件串口寄存器        

  18. }


  19. main()

  20. {

  21.   ser_t s;        

  22.          

  23.   s.tx.fifo = ser0;

  24.   s.tx.fifo_len = SER0_FIFO_TX_LEN;

  25.   s.hal_wt_data = hal_uart0_set_data;

  26.   serial_init(SER0,&s);


  27.   s.tx.fifo = ser1;

  28.   s.tx.fifo_len = SER1_FIFO_TX_LEN;

  29.   s.hal_wt_data = hal_uart1_set_data;

  30.   serial_init(SER1,&s);

  31.   

  32.   //使用串口0发送test1前5个字节数据

  33.   serial_send_hex(SER0, test1, 5);

  34.   //使用串口1发送test2前5个字节数据

  35.   serial_send_hex(SER1, test2, 5);

  36.   

  37.   serial_send_str(SER0,"TEST1");

  38.   serial_send_str(SER1,"TEST2");

  39.   

  40.   while(1)

  41.   {

  42.         if(clk_2ms)

  43.         {

  44.           serial_send_manage();        

  45.           clk_2ms = 0;

  46.         }               


  47.   }

  48. }

复制代码


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


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


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


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


本文作者:会笑的星星是一名设计开发工程师,在平台上的用户名为“ 1585292050XQYe”,有多年的物联网安防产品设计开发经验。

相关资讯
无源晶振YSX321SL应用于高精度HUD平视显示系统YXC3225

在现代汽车行业中,HUD平视显示系统正日益成为驾驶员的得力助手,为驾驶员提供实时导航、车辆信息和警示等功能,使驾驶更加安全和便捷。在HUD平视显示系统中,高精度的晶振是确保系统稳定运行的关键要素。YSX321SL是一款优质的3225无源晶振,拥有多项卓越特性,使其成为HUD平视显示系统的首选。

拥有卓越性能的高精度超薄低功耗心电贴—YSX211SL

随着医疗技术的进步,心电监护设备在日常生活和医疗领域中起到了至关重要的作用。而无源晶振 YSX211SL 作为一种先进的心电贴产品,以其独特的优势在市场上备受瞩目。

可编程晶振选型应该注意事项

对于可编程晶振选型的话,需要根据企业的需求选择。在选择可编程晶振的时候注重晶振外观、晶振的频率、晶振的输出模式、晶振的型号等等,这些都是要注意的,尤其是晶振的频率和晶振输出模式以及晶振的型号都是需要注意的。

性能高的服务器—宽电压有源晶振YSO110TR 25MHZ,多种精度选择支持±10PPM—±30PPM

在现代科技发展中,服务器扮演着越来越重要的角色,为各种应用提供强大的计算和数据存储能力。而高品质的服务器组件是确保服务器稳定运行的关键。YSO110TR宽电压有源晶振,作为服务器的重要组成部分,具备多项优势,成为业界必备的可靠之选。

差分晶振怎么测量

其实对于差分晶振怎么测量方式有很多种,主要还是要看自己选择什么样的方式了,因为选择不同的测量方式步骤和操作方式是不同的。关于差分晶振怎么测量的方式,小扬给大家详细的分享一些吧!