嵌入式编程之"重构"代码(C语言版本)
1、聊一聊为何要"重构code"?
自从发了前面几篇软件设计方面的文章以后,小伙伴们都私信给我问如何能够写出优秀的代码,于是脑海中闪现这个两个字-"重构","重构"这个名词应该在计算机应用程序中编程应用中已经写成了好几本书了,不过在嵌入式编程中可能我们提到的相对没那么多,重构其实从字面上理解就是重新构造的意思,在不改变对外表现的内在结构的优化。
在我们的项目代码开发和维护的过程中需要不断的增加或者修改代码,在没有一个全面的清晰的项目需求摆在程序员面前的时候,我们编写的代码往往容易忽略一些扩展预留的设计。当我们下一次需要进行改动的时候便会使得代码比较凌乱,这时候为了保证一份优秀的产品代码,必须要对当前代码进行整体或者局部的重构计划。
2、"重构code"一般过程
重构更多的是程序员的一种编程能力、抽象能力和预见能力的全面展现,我们在重构代码的时候一定觉得当前代码存在哪些缺陷,我们该对代码进行如何的修改才能对以后设计带来更多的便利,如移植性和扩展性等(也就是我们前面文章说的软件设计中的"SOLID"原则),然后通过我们熟练的编程能力把我们的设计转化为代码,并让其代码通过测试认可,该过程就是一个重构的过程(识别缺陷-->扩展设计-->转化设计-->保证测试通过),该过程对于较小的代码重构只要一个循环,对于较大修改则是一个循环的过程。
3、"重构code"中必备思路和技巧
重构对于计算机应用编程与嵌入式编程其实思路是差不多的,可能有些小伙伴会问哪些重构技术都是面向对象语言特性所适用的,对于C语言这种面向过程的语言可能不太使用吧。
其实个人觉得面向对象并没一种语言的专属特性,就像我们C++语言,也没有说有一种特性叫面向对象吧。面向对象更多的是是一种编程的思路和技巧,让我们的程序更加独立和可扩展。只是说用C进行面向对象编程我们会需要用额外的代码进行代码上的扩展等,后面我也会为大家带来C语言进行面向对象设计的文章。好了废话不多说速速看下面的重构技巧:
1)避免重复代码
大部分有一定代码量的程序员都会有这样的感觉,有些代码好像是同样的处理思路,可是我们往往没有去整合代码,而是导致代码越来越长,甚至到一个屏幕都无法显示完全。这个时候我觉得你可以停下来把代码重构一下了。比如说下面的例子:
下面的代码或许是大部分最开始写程序的样子:
#define TASK1_OVERTIME (10)
#define TASK2_OVERTIME (20)
/***********************************************
* Fuction: sTask1
* Descir : 未重构前的代码
* Author : (公众号:最后一个bug)
**********************************************/
void sTask1(void)
{
//接受超时计数变量
static int RecvOverTime = 0;
//查询队列是否有数据
if(sGetQueueUnit(&stQueue1) != 0)
{
//进行队列数据处理
sTask1QueueProcess(&stQueue1);
//清超时计数变量
RecvOverTime = 0;
}
else
{
//超时变量累计
RecvOverTime++;
//一旦超时即进行超时处理
if(RecvOverTime > TASK1_OVERTIME)
{
RecvOverTime = 0;
//任务1超时处理
sTask1OverTime();
}
}
}
/***********************************************
* Fuction: sTask2
* Descir : 未重构前的代码
* Author : (公众号:最后一个bug)
**********************************************/
void sTask2(void)
{
static int RecvOverTime = 0;
if(sGetQueueUnit(&stQueue2) != 0)
{
sTask2QueueProcess(&stQueue2);
RecvOverTime = 0;
}
else
{
RecvOverTime++;
if(RecvOverTime > TASK2_OVERTIME)
{
RecvOverTime = 0;
sTask2OverTime();
}
}
}
上面的两个任务处理几乎非常相识却分成了两块来进行处理和书写,如果以后还要再增加类似通信任务的处理,那估计又是好几行代码,这就是重复的代码,我们需要对其进行重构,下面我就简单的封装了一下数据部分,让代码变得更加清洁(大家可以对函数处理方法进行封装,具体方法可以参考上期文章<动态接口>),这样以后我们只需要定义数据结构体就可以增加一个通讯任务处理。
/***********************************************
* Fuction: 重复数据简单重构
* Author : (公众号:最后一个bug)
**********************************************/
typedef struct _tag_taskData
{
unsigned char ID; //任务ID,为了任务分支处理
sQuene stQuene; //消息队列
int recvOverTime; //接受超时累计变量
int time; //接受超时容忍时间
}sTaskData;
/***********************************************
* Fuction: 重复数据简单重构
* Author : (公众号:最后一个bug)
**********************************************/
void sTask(sTaskData *stTaskData)
{
if(sGetQueueUnit(&stTaskData->stQuene) != 0)
{
//根据任务ID进行分支处理
//该地方也可以用函数指针分支(具体见上期教程)
sTaskQueueProcess(&stTaskData);
//超时清零
stTaskData->recvOverTime = 0;
}
else
{
//超时累计
stTaskData->recvOverTime++;
//是否超时判断
if((stTaskData->recvOverTime) > stTaskData->time)
{
//清超时变量
stTaskData->recvOverTime = 0;
sTaskOverTime(&stTaskData);
}
}
}
2)达到杜绝注释
大部分人都会有这样的想法,一份代码注释越多越好,肯定很多的程序员形成了一种强迫症,需要为每个变量都定义好注释,一份代码下来注释的量比实际的产品代码还要多。
个人通过读一些高级程序员的代码,或者是一些开源的优秀代码和书籍,其实大部分都不提倡写过多的注释,因为我们的变量都有自己的名称,我们的行为也就是函数都有自己的名称,其实他们就是"最好的文档",那么有些人会问一大堆代码我怎么知道是实现什么功能?出现这样疑问的同志,可能一部分是由于起初开发的工程没有良好的编程风格,另一方面可能就是你对该项目的实现以及需求存在着认识上的缺陷。
一份良好风格的代码都是基于一定的思想,最多的就是分层的思想,函数一层一层的向底层推进,每个变量和函数的名称就已经可以书写成一篇详细的文档给你了,不过这样的前提是你有一个非常好的命名习惯和函数封装习惯。
遵循如下技巧即可:
1)函数变量等名称要具有可读性,便于理解,尽量不要缩写得太严重。
2)函数名称要揭示的是输出目的,而不是内部实现。如果目的命名特别长或者想要对这块代码进行注释,可能你就需要对函数进行继续底层封装,实现我们软件设计"SOLID原则"中的"责任单一原则"(该原则介绍可以看一下往期文章)。
3)一个函数的实现不能太长,阅码率会降低。
4)顶层的代码就像我们读文档一样顺利,完全不需要过多的注释。
下面就拿最简单的花式流水灯来跟大家简单讲下:
/***********************************************
* Fuction: sRunColorFlowLed
* Descri :简单的花样流水灯
* Author : (公众号:最后一个bug)
**********************************************/
void sRunColorFlowLed(void)
{
uint8_t i = 0;
uint8_t LEDStatue = 0x01;
//实现流水灯向左移动1s一次
for(i = 0 ;i < 7;i++)
{
LEDRegister = LEDStatue;
sDelay(cTimeDely1s);
LEDStatue = LEDStatue<<1;
}
//实现流水灯向右移动1s一次
LEDStatue = 0x80;
for(i = 0 ;i < 7;i++)
{
LEDRegister = LEDStatue;
sDelay(cTimeDely1s);
LEDStatue = LEDStatue>>1;
}
//实现流水灯全部闪烁1s一次
LEDStatue = 0xFF;
for(i = 0 ;i < 7;i++)
{
LEDRegister = LEDStatue;
sDelay(cTimeDely1s);
LEDStatue = ~LEDStatue;
}
}
上面这个花式流水灯可能大部分人不会这样写,我这里只是跟大家说明我们的函数及变量名称能够为我们构建文档我们不需要去写多余的注释,如下面代码所示,我们完全可不用注释太多,我们的函数名和变量名就能够很好的为我们解释实现过程。
/***********************************************
* Fuction: sContrlLedRightMove
* Author : (公众号:最后一个bug)
**********************************************/
void sContrlLedRightMove(void)
{
uint8_t i = 0;
uint8_t LEDStatue = 0x01;
for(i = 0 ;i < 7;i++)
{
LEDRegister = LEDStatue;
sDelay(cTimeDely1s);
LEDStatue = LEDStatue<<1;
}
}
/***********************************************
* Fuction: sContrlLedLeftMove
* Author : (公众号:最后一个bug)
**********************************************/
void sContrlLedLeftMove(void)
{
uint8_t i = 0;
uint8_t LEDStatue = 0x80;
for(i = 0 ;i < 7;i++)
{
LEDRegister = LEDStatue;
sDelay(cTimeDely1s);
LEDStatue = LEDStatue>>1;
}
}
/***********************************************
* Fuction: sContrlLedBILBIL
* Author : (公众号:最后一个bug)
**********************************************/
void sContrlLedBILBIL(void)
{
uint8_t i = 0;
uint8_t LEDStatue = 0xFF;
for(i = 0 ;i < 7;i++)
{
LEDRegister = LEDStatue;
sDelay(cTimeDely1s);
LEDStatue = ~LEDStatue;
}
}
/***********************************************
* Fuction: sRunColorFlowLed
* Descri :简单的花样流水灯
* Author : (公众号:最后一个bug)
**********************************************/
void sRunColorFlowLed(void)
{
sContrlLedRightMove();
sContrlLedLeftMove();
sContrlLedBILBIL();
}
3)用分层思想来写代码
大家应该都有学计算机网络课程,那么计算机网络经典的OSI七层模型就是最经典的分层思想,其实所谓的通讯也是由我们的代码组成的,说白了了就是告诉你实现这套模型就是采用分层的思想。(大家可以搜索一下网络模型简答的学习一下)其中书籍中提到该模型中仅仅是层与层之间交互和对话,底层是未知的,底层仅为底层提供服务。
那么对于我们嵌入式编程也是同样的处理办法,我们的代码一定是要同一层面的东西,比如说一个函数中有多个处理函数,那么这多个处理函数一定是同一层面的,否则需要对其进行移除或者封装,同时还要注意与平台有关的代码与平台无关的代码要分别放到不同的文件中,便于以后移植替换。(这部分具体的举例代码量比较大,可以网络搜索学习)
4)经典"重构"小技巧
1)对条件语句进行分离
很多程序员很喜欢if的条件判断里面一大堆的条件,这样是非常不便于阅读的,你需要对条件进行分类管理。例如如下代码:
/***********************************************
* Fuction: 重构前的函数
* Author : (公众号:最后一个bug)
**********************************************/
uint8_t IdentifyConditon(char var)
{
if(((var > 'A')&&(var < 'F'))&&\
((var > 'a')&&(var < 'f'))&&\
((var > 1)&&(var < 8)))
{
return TRUE;
}
return FALSE;
}
/***********************************************
* Fuction: 重构后的函数
* Author : (公众号:最后一个bug)
**********************************************/
uint8_t IdentifyConditon(char var)
{
if(MatchCaption(var))return TRUE;
if(MatchLowcase(var))return TRUE;
if(MatchFigure(var))return TRUE;
return FALSE;
}
2)参数太多,直接用结构体指针封装,这个就不举例子了,第一个代码也说明了这个问题,因为我们不断的封装,分支参数也在不断增加,所以统一打包指针传入;
3)变量初始化位置要统一,不要到处清零,这样程序对变量的行为无法把控。
4)条件编译尽量不要用太多,可以通过链接不同文件解决。
今天就讲这么多重构的技巧,夜已经深了,重构是一种行为,会有很多种形式,可能对于很多小伙伴们会问到,你虽然用于了良好的代码结构,不过程序在一层一层不断的花时间在调用函数岂不非常影响代码效率,我个人觉得这不是写乱代码的理由,现在我们编译器都非常智能,能都对其的代码进行大量的优化,函数的调用所带来的影响非常之小。不过凡是也不能太绝对,对于低端单片机,DSP做控制算法类还是需要注意函数的调用的,不能嵌套得太深。
好了,这里是公众号:"最后一个bug",后续我还会整理更多的编程经验和技巧跟大家分享,也希望大家分享转发!谢谢!
推荐阅读