浅谈项目开发中的模块化、解耦、封装

目录

模块化

解耦

封装


模块化

        模块化是指将功能相关的代码和数据组织成独立的模块,以便于开发的任务分割与安排、独立测试、后期维护以及后续别的项目有相同功能时的可移植,大大缩短开发时间,避免重复造轮子。当你模块化到了极致,简单的功能重复项目可以做到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文件,我们能看到“按键对象”的结构体全部成员(虽然我省略了一部分)。看到就看到啊,怎么了?正因为看到了,我可以根据这个结构体去反向分析代码实现,使得保密性下降。我还可以直接修改结构体成员的值,使得内部代码的运行逻辑乱套甚至崩溃跑飞,使得安全性下降。

        那么还需要对结构体进行隐藏,说到结构体隐藏我知道的有两种方式,我们后面再讲。

        接着再讲讲源文件,如果直接把源文件发给调用者,调用者可以直接查看并且修改源码,这是非常影响代码的保密性与安全性,当然开源代码就另当别论了。如果调用者在不了解源码的实现原理的情况下,随意修改会导致源码损坏无法运行的后果。所以有的代码还会再进一步封装生成静态库或动态链接库;


        分享先到这里,希望能给大家带来启发与帮助。如果对内容存在疑问或想法,欢迎在评论区留言,我会积极回复大家的问题。在我的“经验分享”专栏中,还有很多实用的经验,欢迎一起探讨、一起学习。

  • 5
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论
Java实现模块解耦可以采用以下几种方式: 1. 接口设计:模块之间的依赖关系通过接口来实现,每个模块只依赖于接口而不依赖于具体的实现类。这样,在模块之间进行交互时就只需要使用接口,而不需要直接依赖于其他模块的具体实现。 2. 依赖注入:通过依赖注入(Dependency Injection)的方式,将模块之间的依赖关系从代码解耦出来。依赖注入可以通过构造函数、Setter方法或者注解等方式实现。 3. 事件驱动:通过事件机制将模块之间的依赖关系解耦。每个模块都可以发出事件,其他模块可以监听这些事件并做出相应的响应。 4. 消息队列:通过消息队列的方式将模块之间的依赖关系解耦。每个模块可以向消息队列发送消息,其他模块可以从消息队列获取消息并做出相应的响应。 Java模块化开发的通用设计指南如下: 1. 模块划分:将系统划分为多个模块,每个模块具有独立的功能和职责。 2. 接口定义:为每个模块定义接口,使得模块之间的依赖关系通过接口实现。 3. 依赖管理:通过依赖管理工具(如Maven、Gradle等)管理模块之间的依赖关系,确保依赖关系的正确性和稳定性。 4. 单一职责:每个模块只负责一项功能,确保模块的内聚性和可维护性。 5. 松耦合设计:通过接口、依赖注入、事件驱动、消息队列等方式实现模块之间的松耦合设计,使得系统更加灵活和可扩展。 6. 模块测试:为每个模块编写测试用例,确保模块的正确性和稳定性。 7. 模块文档:为每个模块编写文档,包括模块的接口、依赖关系、使用方法等内容,方便其他人理解和使用模块。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小星星星球

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值