1.前言
之前,一直在跟大伙分享怎么去玩蓝牙模块,怎么去玩wifi模块,怎么去玩json,然后有很多小伙伴就留言各种问题或者说直接怼他的代码过来让我看,然后我就一脸懵逼(代码中到处各种abcd变量,各种一个方法几百行,也没有什么注释,我心中一万只万马奔腾)。所以就有了这次的主题,代码规范(当然,这是我自己的代码规范经验,只是借鉴经验),教了大家怎么去做东西,反而忽略了最基本的东西。
特别是出来工作之后,我就觉得代码规范比做需求业务还要重要。
首先,方便他人。一个项目往往是由多人一起完成的,代码规范,会让大家交流起来更加简单有效,别人接手你的代码也比较容易。
其次,方便扩展。一个好的项目,代码还是得具有一定的扩展性,很多时候项目是迭代开发的,分层抽象还是很重要。
最后,方便自己。很多开发人员都会有这种感觉,自己写的代码,过一段时间回来再看的时候,就会有点懵逼的感觉(往往就是因为代码不够规范,也没什么注释,连自己都看不懂了)。
这次计划分三篇来分享,尽量概括常见的规范点,主题分为三部分:
- 工程代码规范
- 函数方法规范
- 兼容性规范
2.工程代码规范
工程代码规范,主要是针对整个项目来规范。
2.1 文件命名
2.1.1 错误写法
- 博主还记得大学的时候非常喜欢用abcdxy这些单一的字母来命名文件,隔一段时间再去查看代码,有点懵逼。
2.1.2 推荐写法
- 驼峰式大小写法(每个单词的首字符是大写,其余用小学),比如蓝牙灯项目,博主喜欢命名为BlueToothLedPro,8266灯项目,博主喜欢命名为ESP8266LedPro,简单明了。
2.2 文件注释
博主经常看到这样的现象——很多童鞋发过来的主工程代码基本上都是没有注释的。
2.2.1 错误演示
比如一个蓝牙灯项目,不合理代码基本上是这样的:
#include
#include
............
是不是非常懵逼,完全不知道这个项目是做什么的?
2.2.2 推荐写法
参考写法如下:
/**
* 日期:2017/09/25
* 功能:wifi+weather+oled 8266端
* 作者:单片机菜鸟
* 16X16点阵显示 取模方式 阴码+逐行式+顺向
**/
#include
#include
.............
一般规范的写法就是三个w(who作者,what time什么时间写的,doing what 这段代码主要是什么功能,当然如果有版本迭代的,也请加入版本说明)。
这样别人其实不用深入看你的代码就知道你这个文件的代码主要有什么用,完成什么功能。
2.3 常量
常量,基本上都是一些固定的配置数据(波特率,数组大小等)或者常用不变的字符串,我推荐用全大写字母写法。请看代码:
const unsigned long BAUD_RATE = 115200; // serial connection speed
const unsigned long HTTP_TIMEOUT = 5000; // max respone time from server
const size_t MAX_CONTENT_SIZE = 1000; // max size of the HTTP response
const char* host = "api.seniverse.com";
const char* APIKEY = "wcmquevztdy1jpca"; //API KEY
const char* city = "guangzhou";
const char* language = "en";//zh-Hans 简体中文 会显示乱码
推荐全大写写法的原因就是我们的变量基本上都是小写居多,而为了区分常量,就全大写最为稳妥。
当然我这里还是有问题,有大写,也有小写,作为错误示范了,尽量全大写规范。
2.4 变量
很多人初学者习惯写法都是abdcxyi这些常见字母去命名变量,比如:
int a = 0;
char b;
int i = 0;//循环经常见到这个
我个人推荐的写法就是前缀m+名称。
比如用户数据,就可以命名为 mUserData。
比如最近时间,就可以命名为mLastTime。
int mUserData;//用户数据
int mLastTime;//上一次时间
3.函数方法规范
函数方法是一个功能的描述,接下来我会主要分几个方面讲解我的函数规范。
3.1 函数方法注释
这是最重要的一点,首先你最起码得让别人知道这个函数功能是什么,需要传入什么参数,如果有返回值,返回什么内容。
/**
* @desc 发送请求指令
* @param host 请求后台接口
* @param cityid 城市id
* @param apiKey 请求后台接口需要识别的密钥
* @return 请求是否成功
*/
bool sendRequest(const char* host, const char* cityid, const char* apiKey) {
// We now create a URI for the request
//心知天气
String GetUrl = "/v3/weather/now.json?key=";
GetUrl += apiKey;
GetUrl += "&location=";
GetUrl += city;
GetUrl += "&language=";
GetUrl += language;
// This will send the request to the server
client.print(String("GET ") + GetUrl + " HTTP/1.1\r\n" +
"Host: " + host + "\r\n" +
"Connection: close\r\n\r\n");
DebugPrintln("create a request:");
DebugPrintln(String("GET ") + GetUrl + " HTTP/1.1\r\n" +
"Host: " + host + "\r\n" +
"Connection: close\r\n");
delay(1000);
return true;
}
- desc 表示方法功能描述
- param 表示输入参数描述
- return 有返回值的话,返回值描述
3.2 函数方法体
一般来说,一个方法体功能越简单越好,这是设计原则里面的单一原则,尽量去细化。
我经常看到很多人写的函数方法,都是从头到尾一口气把所有功能的写进去了,估计有个几百行。
我的建议就是一个方法体尽量控制代码在几十行左右,一个电脑屏幕就能看完。实在代码太多了,考虑函数方法拆分。
3.3 函数拆分
函数拆分的前提是函数完成单一的功能。比如我比较喜欢这样的写法:
/**
* @desc 方法功能描述
* @param param 参数
* @return 无
*/
void handleFunction(param) {
handleFunctionA(param);
handleFunctionB(param);
handleFunctionC(param);
}
/**
* @desc 方法A功能描述
* @param param 参数
* @return 无
*/
void handleFunctionA(param) {
}
/**
* @desc 方法B功能描述
* @param param 参数
* @return 无
*/
void handleFunctionB(param) {
}
/**
* @desc 方法C功能描述
* @param param 参数
* @return 无
*/
void handleFunctionC(param) {
}
每个功能是一个小方法,然后一个大方法去调用这些小方法完成所需功能。比如wifi模块获取天气信息这个功能,我们的方法就可以分为这样:
/**
* @desc 获取天气信息
* @param param 参数
* @return 无
*/
void getWeather(param) {
sendRequest(param);
readReponseContent(param);
parseReponseContent(param);
}
/**
* @desc 发送请求
* @param param 参数
* @return 无
*/
void sendRequest(param) {
}
/**
* @desc 读取响应数据
* @param param 参数
* @return 无
*/
void readReponseContent(param) {
}
/**
* @desc 解析响应数据
* @param param 参数
* @return 无
*/
void parseReponseContent(param) {
}
大方法负责管理整个获取天气流程,小方法负责各自的业务,包括发起请求,获取请求响应数据,解析响应数据。
重点说一下,拆分方法的前提是功能的细分。
3.4 函数参数
可能很多人不会在意这一点,反正是参数,只需要不断往里面加就可以了。
其实参数在5个以内还好,超过5个之后你就会发现方法名字后面连着一坨长长的东西。
这还不是问题所在,那么问题来了。假设你这个方法在很多地方都调用了(工具类方法经常会这样,就是那么浪),那么你就得考虑假设我要加多几个参数,是不是意味着我每个调用的地方都得改一下(当然,你可能会说,我可以重新写一个新方法,当然你喜欢也可以了,但是刚好这个方法是需要全局改动的,那么你就准备gg)。
对于这种没有超过5个参数的,我的做法还是直接放在方法里面。
对于超过5个参数的,我建议可以使用一个结构体变量。废话少说,看我例子。
/**
* @desc 方法功能描述
* @param param1 参数1
* @param param2 参数2
* @param param3 参数3
* @param param4 参数4
* @param param5 参数5
* @return 无
*/
void handleFunctionA(param1,param2,param3,param4,param5){
}
/**
* @desc 方法B输入参数
*/
struct FunctionBParams {
param1;//参数1
param2;//参数2
param3;//参数3
param4;//参数4
param5;//参数5
param6;//参数6
};
/**
* @desc 方法功能描述
* @param FunctionBParams 参数
* @return 无
*/
void handleFunctionB(FunctionBParams param){
}
这样就对参数做了一个扩展,不需要改动原来的调用逻辑。
3.5 if else or switch case 选择困难症
我的建议就是三个以内的判断用 if else,超过三个之后的判断就用switch case(因为往往超过三个的后面还会陆续有判断)。
而且,出现频率比较高的条件优先排在前面,减少判断次数。
其次,建议case 后面的常量不要是 0 1 2, 最好还是定义好具体的常量含义,这样比较清晰,避免使用魔法数字
4.兼容性规范
兼容性讲究的是多平台性,也就是时常说的一套代码多个平台(arduino)上运行。
我们都知道,arduino有uno mega mini等,每一款都有它自己规定好的硬件资源,要想一套代码能在多个平台上运行,那就得意味着你的代码得适应多平台。
解决以上的问题,关键还是利用好预编译指令 #define #ifdef #endif。
预编译命令的好处在于根据你的配置来把不同的代码段编译进最终的代码中去,可以实现我们要求的多平台性。
接下来我截取了一下我在wifi lamp里面的代码:
/**
* 日期:2017/09/14
* 功能:wifi lamp arduino端
* 作者:单片机菜鸟
**/
#include
#include
const unsigned long BAUD_RATE = 115200; // serial connection speed
const size_t MAX_CONTENT_SIZE = 50;
const size_t t_bright=1,t_color=2,t_frequency=3,t_switch=4;
//#define UNO //uncomment this line when you use it with UNO board
#define MEGA //uncomment this line when you use it with MEGA board
#ifdef UNO
SoftwareSerial mySerial(10,11);
#endif
#ifdef UNO
#define WifiSerial Serial
#define MyDebugSerial mySerial
#endif
#ifdef MEGA
#define WifiSerial Serial1
#define MyDebugSerial Serial
#endif
//该条语句用于使能DEBUG输出信息,屏蔽掉就不会输出debug调试信息
#define DEBUG
//该条语句用于使能是共阴RGB 屏蔽掉就是共阳RGB
//#define COMMON_GND
#ifdef DEBUG
#define DBGLN(message) MyDebugSerial.println(message)
#else
#define DBGLN(message)
#endif
#ifdef UNO
#define PIN_RED 3 //red 引脚
#define PIN_GREEN 5 //green 引脚
#define PIN_BLUE 6 //blue 引脚
#define PIN_ENABLE 9 //使能引脚 pwm控制亮度
#else
#define PIN_RED 2
#define PIN_GREEN 3
#define PIN_BLUE 4
#define PIN_ENABLE 5
#endif
里面做了一个开关,主要是兼容UNO和MEGA两个平台,实现一键切换。
然后在使用的时候就不用考虑是哪一个平台。
然后就是使用DEBUG功能,以及不删掉调试代码的情况下去掉调试信息(把define debug那句代码注释掉)。
接下来,因为我在做RGB灯的时候也考虑共阴共阳的问题,那么怎么去考虑兼容性呢,还是利用强大的预编译三剑客命令。
/**
* 控制RGB颜色
*/
void colorRGB(int red, int green, int blue){
#ifdef COMMON_GND
analogWrite(PIN_RED,constrain(red,0,255));
analogWrite(PIN_GREEN,constrain(green,0,255));
analogWrite(PIN_BLUE,constrain(blue,0,255));
#else
analogWrite(PIN_RED,constrain(255-red,0,255));
analogWrite(PIN_GREEN,constrain(255-green,0,255));
analogWrite(PIN_BLUE,constrain(255-blue,0,255));
#endif
}
RGB代码中兼容共阴共阳,然后设置一个开关切换就可以了,预编译的好处就是根据你的配置来编译出你要基于某个平台的代码而实现多平台。
5.总结
代码规范,就跟写文章一样,同样的题材,不一样的写作方式,得到的分数也是不一样的。虽然篇章简洁,但是内容还是需要仔细斟酌的。