目录
模块化
模块化是指将功能相关的代码和数据组织成独立的模块,以便于开发的任务分割与安排、独立测试、后期维护以及后续别的项目有相同功能时的可移植,大大缩短开发时间,避免重复造轮子。当你模块化到了极致,简单的功能重复项目可以做到1-2天上交测试。
总言之,模块化可以提高代码的可重用性和可测试性,并简化系统的复杂性与提高系统的隐秘性。
先举个代码栗子,简单了解下“模块”,如下低配版按键功能模块示意代码:
#include "delay.h" /* 示意延时头文件 */
#include "readIO.h" /* 示意读取KEY状态头文件 */
#include "led.h" /* 示意KEY触发的处理函数头文件 */
/*按键功能示意代码*/
void KEY_Scan(void)
{
if(0 == ReadKEY())
{
Delayms(20); /* 仅仅示意,同学们别再用堵塞延时啦,学学状态机 */
if(0 == ReadKEY())
{
LED(OPEN); /* 实现按键短按亮灯 */
}
}
}
这是一个关于按键功能的模块,是一个未经过解耦的按键模块,里面还能分割成按键扫描功能模块、IO状态读取模块、按键触发的其他功能模块。
再举个栗子:每个大模块还能继续分割成多个小模块,比如(1+2)为大模块,把“1”与“2”分割出来成为小模块,大模块中主要实现“+”这个功能,以及如何把“1”与“2”甚至“3”“4”“9”的功能接入进来。如用户交互大模块,其中的用户输入可能是按键、触摸屏、通讯协议等,反馈可能是显示图片到OLED屏、串口屏、打印文字信息等等。由用户输入进而实现反馈就是“+”,按键、触摸屏、通讯协议、显示图片到OLED屏、串口屏、打印文字信息就是“1”“2”“3”...
再举个栗子:如一个温控设备,可以分成数据采集模块、用户交互模块、温度控制模块等大模块。这些大模块都可以分派给独立的工程师去进行开发,每位工程师只需了解自己负责模块的功能与接口需求,无需知道这些模块最终被用在哪,当然这是基于系统的隐秘性的。如果你是独立开发或是项目架构师,你要尽可能的去分析每个模块将会用在哪,需要提供什么功能什么接口来提高可重用性。什么需要公开什么需要私有,这里就涉及到了下面讲到的“封装”了。
解耦
解耦是解除耦合的简称,是指将代码中的不相关部分分离开来,以减少模块之间的依赖性,从而提高代码的可维护性、可兼容性和可扩展性。解耦可以通过使用接口、事件驱动编程和依赖注入等技术来实现。
总言之,始终贯彻着“事不关己高高挂起”的核心思想。
何为依赖性?有大部分同学的按键扫描驱动写法都是直接读取GPIO电平的。啊?不是这样写那怎么写啊?看到这里是不是就依赖着“直接读取GPIO电平”了,而且还需要在按键扫描驱动里添加关于“读取GPIO电平”的库函数头文件。
何为耦合?上面说的“直接读取GPIO电平”以及按键触发了短按、短按抬起等等时需要处理的事件,比如需要实现短按亮灯、短按抬起灭灯等,则还需要把关于控制灯功能的头文件添加进来。哇塞,大杂烩,全都到碗里来。
何为接口技术实现解耦?如下升级版按键功能模块示意代码:
/****************************************外部接口****************************************/
#include "delay.h" /* 示意延时头文件 */
#include "readIO.h" /* 示意读取KEY状态头文件 */
#include "handle.h" /* 示意KEY触发的处理函数头文件 */
static void KEY_Delayms(unsigned short ms)
{
//添加延时函数
}
static unsigned char ReadKEY0(void)
{
//添加读取KEY0状态的函数
}
static void KEY0_Handle(void)
{
//添加读取KEY0触发的处理函数
}
/****************************************外部接口 End****************************************/
/*按键功能示意代码*/
void KEY_Scan(void)
{
if(0 == ReadKEY0())
{
KEY_Delayms(20); /* 仅仅示意,同学们别再用堵塞延时啦,学学状态机 */
if(0 == ReadKEY0())
{
KEY0_Handle();
}
}
}
这种接口技术,仅仅在按键功能模块文件里进行解耦,但并不是真正完全解耦。优点是代码上没那么臃肿,把按键驱动功能与外部接口功能分隔开来。
为了实现真正解耦,有请函数指针上场,如下初级版单按键扫描驱动示意代码:
typedef void (*KEY_Delay)(unsigned char ms); /* 延时函数函数指针类型 */
typedef unsigned char (*ReadKEY)(void); /* 读取按键状态函数函数指针类型 */
typedef void (*KEY_Handle)(unsigned char keyState); /* 按键处理函数函数指针类型 */
static void KEY_Delay_NULL(unsigned char ms){} /* 延时函数类型的空函数 */
static unsigned char ReadKEY_NULL(void){} /* 读取按键状态函数类型的空函数 */
static void KEY_Handle_NULL(unsigned char keyState){} /* 按键处理函数类型的空函数 */
static KEY_Delay KEY_Delayms = KEY_Delay_NULL; /**< 延时函数 防止为野指针 */
static ReadKEY ReadKEY0 = ReadKEY_NULL; /**< 读取按键状态函数 防止为野指针 */
static KEY_Handle KEY0_Handle = KEY_Handle_NULL; /**< 按键处理函数 防止为野指针 */
/*延时函数的注册函数*/
unsigned char Register_KEY_Delay(KEY_Delay function)
{
if(KEY_Delay_NULL == KEY_Delayms)
{
KEY_Delayms = function;
return 0; /* 首次注册 */
}
KEY_Delayms = function;
return 1; /* 非首次注册 */
}
/*读取按键状态函数的注册函数*/
unsigned char Register_ReadKEY(ReadKEY function)
{
if(ReadKEY_NULL == ReadKEY0)
{
ReadKEY0 = function;
return 0; /* 首次注册 */
}
ReadKEY0 = function;
return 1; /* 非首次注册 */
}
/*按键处理函数的注册函数*/
unsigned char Register_KEY_Handle(KEY_Handle function)
{
if(KEY_Handle_NULL == KEY0_Handle)
{
KEY0_Handle = function;
return 0; /* 首次注册 */
}
KEY0_Handle = function;
return 1; /* 非首次注册 */
}
/*按键扫描示意代码*/
void KEY_Scan(void)
{
if(0 == ReadKEY0()) /* 低电平有效 */
{
KEY_Delayms(20); /* 仅仅示意,同学们别再用堵塞延时啦,学学状态机 */
if(0 == ReadKEY0())
{
KEY0_Handle(CLICK); /* 短按按下 */
}
}
}
由此写法可以看出,这个按键扫描的驱动文件里干净了许多,无需依赖别的头文件,仅有按键扫描的功能,其他部分由上层调用者去注册即可。这时就会有人说,这写法不是看起来更加复杂和繁琐吗?我知道你很急但你先别急,这仅仅只是初级版,我们了解完解耦之后,现在来看看下面的“封装”。
封装
封装是指将代码和数据结构隐藏在一个模块或类中,需要通过提供公共接口来访问和操作这些数据结构和功能。封装可以提高代码的安全性和保密性,降低调用者对代码内部功能具体如何实现的要求,调用者只需关注封装的功能和接口即可。
举个最简单的栗子,函数封装:
C文件内容
/*冒泡排序*/
void Bubble_Sort(unsigned char* pBuf, unsigned short bufSize, unsigned char flag)
{
//...具体就不实现了哈
}
H文件内容
/*声明冒泡排序*/
void Bubble_Sort(unsigned char* pBuf, unsigned short bufSize, unsigned char flag);
冒泡排序函数封装好了,调用者只需了解接口怎么用即可,并不需要知道内部如何排的序。
现在以按键扫描驱动再举个大栗子,里面有面向对象与状态机结合的思想,如下为进阶版按键扫描驱动:
C文件内容
#include "KEY_Driver.h"
/*按键对象创建*/
void KEY_Create(_STR_KEY* str, unsigned char keyName, \
unsigned char (*ReadKEY)(void), \
void (*KEY_Handle)(unsigned char keyState))
{
str->keyName = keyName;
str->keyState = KEY_STEP_WAIT;
str->count = 0;
//...
str->ReadKEY = ReadKEY;
str->KEY_Handle = KEY_Handle;
}
/*按键扫描*/
void KEY_Scan(_STR_KEY* str)
{
switch(str->CheckStep)
{
case KEY_STEP_WAIT:/*等待按下*/
if(0 == str->ReadInputDataBit()) /* 电平有效 */
{
str->DownCount = str->Click_CountVal; /* 赋予短按按下计数值 */
str->CheckStep = KEY_STEP_CLICK; /* 检测状态切换:检测按下 */
}
break;
case KEY_STEP_CLICK:/*检测按下*/
str->DownCount--;
if(str->DownCount == 0) /* 短按按下计数已过(消抖) */
{
if(0 == str->ReadInputDataBit()) /* 电平仍然有效 */
{
str->State = KEY_STATE_CLICK; /* 状态切换:短按 */
str->ActionFunc(str->ID, str->State); /* 执行函数 */
//...
str->CheckStep = KEY_STEP_LONG; /* 检测状态切换:检测长按 */
}
else /* 无效 */
{
str->CheckStep = KEY_STEP_WAIT; /* 检测状态切换:等待按下 */
}
}
break;
//...
}
}
H文件(KEY_Driver.h)
/**按键状态*/
typedef enum{
KEY_STEP_WAIT,
KEY_STATE_CLICK, /**< 短按 */
KEY_STATE_CLICK_UP, /**< 短按抬起 */
//...
}_enum_KEY_STATE;
/**按键对象*/
typedef struct STR_KEY{
unsigned char keyName; /**< 按键ID */
unsigned char keyState; /**< 按键状态 */
unsigned short count; /**< 计数器 */
//...
unsigned char (*ReadKEY)(void); /**< 读取按键状态函数函数指针 */
void (*KEY_Handle)(unsigned char keyState); /**< 按键执行函数函数指针 */
}_STR_KEY;
/*按键对象创建*/
void KEY_Create(_STR_KEY* str, unsigned char keyName, \
unsigned char (*ReadKEY)(void), \
void (*KEY_Handle)(unsigned char keyState));
/*按键扫描*/
void KEY_Scan(_STR_KEY* str);
这个版本的按键驱动是已经完全解耦了,扫描功能是实现了,也封装好了扫描功能,但是我们细看H文件,我们能看到“按键对象”的结构体全部成员(虽然我省略了一部分)。看到就看到啊,怎么了?正因为看到了,我可以根据这个结构体去反向分析代码实现,使得保密性下降。我还可以直接修改结构体成员的值,使得内部代码的运行逻辑乱套甚至崩溃跑飞,使得安全性下降。
那么还需要对结构体进行隐藏,说到结构体隐藏我知道的有两种方式,我们后面再讲。
接着再讲讲源文件,如果直接把源文件发给调用者,调用者可以直接查看并且修改源码,这是非常影响代码的保密性与安全性,当然开源代码就另当别论了。如果调用者在不了解源码的实现原理的情况下,随意修改会导致源码损坏无法运行的后果。所以有的代码还会再进一步封装生成静态库或动态链接库;
分享先到这里,希望能给大家带来启发与帮助。如果对内容存在疑问或想法,欢迎在评论区留言,我会积极回复大家的问题。在我的“经验分享”专栏中,还有很多实用的经验,欢迎一起探讨、一起学习。