系统模块化设计
将系统中有关联的部分组合在一起,构成具有特定功能的子系统。划分
模块的内部组成具有较强的耦合性,模块本身具有一定的通用性。
不同的模块间可以进行相互组合与依赖,进而构成不同的产品。
模块化设计:
结构化设计 面向对象设计
示例:
Module Demo
interface123 int function(); class0 class1 class2
接口比模块先实现,用来各个模块交互。
各个模块间需要相互依赖,进而完成产品功能。
根据依赖关系能够将模块分为不同的层。
模块间的分层:硬件层,系统层,平台层,框架层,应用层
平台层 heap module timer mudule memory module
框架层 fsm module communication module
应用层 session(汇画) module privilege module user module
模块的分级(更细的设计粒度):
同一层中的模块根据依赖关系能够继续分级
平台层:两级
框架层
应用层:两级
分层和分级的意义:
模块间的依赖关系决定了初始化的前后顺序。
被依赖的模块必须先初始化(底层先于上层初始化)
如:硬件层模块先于系统层模块初始化
框架层模块先于应用层模块初始化。
系统架构示例一:图
依赖关系
系统内核--平台层(android runtime)--框架层--应用层
示例2
硬件层(显示器,键盘,收银箱,POS机,打印机)--操作系统(驱动)--平台层(java虚拟机)--框架层(数据库模块,网络通信模块,设备控制模块)--应用层(用户界面,扩展模块)
设计时需要思考的问题:
如何在代码中定义模块?如何定义的层级关系(依赖关系)?如何确定模块的初始化顺序?
模块的定义:
typedef enum{
MODULE_MODULE, //for module management
MODULE_INTERRUPT, //for interrupt management
MODULE_DEVICE, //for device management
...}module_t;
模块的描述及组织方式
type struct{
dll_node_t node_; //链表结点
const char *p_name_; //模块名
module_callback_t callback_; //模块回调函数:接收系统消息
bool is_registered_; //注册标记
}module_init_t;
注册到数据结构中:
level0-->module1->module2
level1-->module3->module4->module5
level2->module6
第一级依赖于第二级.....
层级关系的定义:每一层分为七级
typename enum{
LEVEL_FIRST,
//...
//for platform layer
PLATFORM_LEVEL0,
//...
PLATFORM_LEVEL7,
//for framework layer
FRAMEWORK_LEVEL0,
//...
FRAMEWORK_LEVEL7
...
LEVEL_COUNT,
LEVEL_LAST=(LEVEL_COUNT-1)
}init_level_t;
状态设计: 用回调函数改状态
-system_up()->initialzing->up--system_down()->down->destroying
typedef enum{
STARE_INITIALIZING,
STATE_UP,
STATE_DOWN,
STATE_DESTROYING
}system_state_t;
模块初始化:
modele1--module_register()-->module manager
modele2--module_register()-->module manager
modele2<---callback()--module manager回调
模块的销毁:
modele2--module_register()-->module manager(system_down())
最后初始化的最先终止,最先初始化的最后终止。
实现要点:
每一个模块对应一个ID和一个结构体变量(module_init_t)
模块需要注册后才能被初始化(module_register)
模块提供了一个回调函数(module_callback_t)用于接收事件。
所有的模块根据层级关系组织与不同链表中:同一个链表中的模块没有依赖关系,整个系统从底层(最上层)的模块开始进行初始化(销毁)。
启动-模块注册-发送消息-system_down()
system up:如何在代码中反映层级关系:将模块对应的变量放到相应的链表中去,然后遍历的时候从上到下遍历链表中的结点,遍历之后,对结点中的回调函数进行调用,回调函数收到系统初始化以及启动的消息后,就进行具体的模块初始化工作了。
system down:返过来遍历:
模块设计是要遵循强内聚弱耦合的原则。
模块之间可以相互依赖,并进行模块层级的划分。
模块管理是为了系统中各个模块的有序启动和停。
模块设计时需要考虑资源的分配和释放问题。
模块代码优化22-1.
22:异常设计
在开发中,不可避免的需要进行异常处理。
函数调用时的异常:并不是函数设计上的逻辑错误。而是可预见的非正常功能的执行分支。
异常处理的意义:
软件开发过程中,大多数情况下都在处理异常情况。
异常不是错误,但是可能导致程序无法正常执行。
异常处理直接决定软件产品的鲁棒性和稳定性。
项目中的异常设计:
实现表达异常的通用方法(异常如何表示?)
设定异常报告的方法(发生什么异常,如何知道)
指定统一处理异常的原则(怎么处理异常)
在c语言中通过错误码对异常进行表示:
优势:错误码的 定义简单,使用方便。
劣势:同一个错误码可能表示不同的含义
异常表示的通用设计方法:采用整数分区域的方式对异常进行表示。
32位整数:最高为标识,1为错误,15位模块标识,16位错误标识。
注意:一般而言,错误码最高位为1.即:所有错误码为负数。
错误码类型操作:
ERROR_MARK //0x8000000 错误码最高位为1
ERROR_BEGIN(_module_id) //根据模块ID计算起始错误号
ERROR_T(module_error) //根据错误号生成错误号
MODULE_ERROR(_error_t)// 获取错误码中的模块内错误号(底16位)
MODULE_ID(_error_t) //获取错误码中的模块ID(高15位)
代码:
异常的报告:
通常情况下,系统日志的是异常报告的主要方式。
注意:异常报告并不是异常处理。
异常报告用于记录异常的发生,异常处理用于阻止导致的程序奔溃。
异常报告与异常处理示例(整体框架)
异常报告与异常处理示例(异常报告)
异常报告与异常处理示例(异常处理)
工程开发时的一些建议:
尽量在异常发生的地方报告异常:有助于事后找到发生时的函数调用路径。
尽量在上层函数中统一处理异常:集中处理异常有助于提高代码的维护性。
小结:
c项目中通常采用整数分区域的方式对异常进行表示。
异常号包含了模块信息以及模块相关的具体异常信息。
通常情况下,系统日志是异常报告的主要方式
尽量就近报告异常,尽量统一处理异常。
23、异常中
问题:当前的设计中,直接输出异常错误码的方式易于问题的定位吗?是否有更好的异常输出方式?
使用c++独立程序,通过c++程序自动生成我们想要的c代码,c++独立程序编译过后就可以将定义异常的头文件作为输入产生输出,输出是我们想要的c代码,而这个c代码作用是异常错误码到对应的异常名字之间的映射。方便日志的打印。
期望的异常输出方式:
直接将错误码所对应的枚举常量名输出:
枚举常量名是精心设计的有意义的名字。
枚举常量名包含了模块名和模块内错误名。
error_t err=ERROR_T(ERROR_TIMER_ALLOC_NOTIMER);
LOG(sub_func,err);
=>sub_func[main.c:41]=>8007000A
=>ERROR_TIMER_ALLOC_NOTIMER
思路:
简历枚举常量名到字符串数组的映射。
通过错误号查找字符串数组得到对应的名字。
维护性上的考虑:
每个模块有自己的异常枚举定义(数量不同)
异常的类型无法一次性设计完善(后期可能增加后减少)
当异常类型改动时,必须正确改动对应的字符串数组。
自动产生?如何产生?
解决方案设计:
1、每个模块的异常枚举定义于独立的文件
2、异常枚举的定义遵循固有的编码规范
3、编写独立程序处理异常枚举定义文件,生成对应的字符串数组。
4、当项目中出现异常时:根据错误码中的模块名定位字符串数组。根据错误码中的内部错误号定位字符串。
解决方案设计:
errtmr.h errheap.h errmod.h err2str.cpp-->err2str--out->errstr.def
错误码到枚举常量名的映射:
g_errsr_array(包含指针)--module_id-->g_error_MODULE_TIMER--error_id-->
"ERROR_TIMER_ALLOC_INVCB"
"ERROR_TIMER_ALLOC_NOTIMER"
"ERROR_TIMER_FREE_INVHANDLE"
数据结构设计:
static struct errstr_t{
int available_; (标记是否存在错误名数组)
int last_error_; (模块内的最后一个错误号)
const char **error_array_; //错误名数组
}g_errstr_array[MODULE_COUNT];
错误名查找函数的设计:
const char *errstr(error_t _error)
{
static bool initialized=false;
module_t module_id=MODULE_ID (_error);
int error_id=MODULE_ERROR (_error);
if(!initialized){
errstr_init(); //将自动生成的错误名数组映射到对应的模块ID
initialized=true;
}
//...
return g_errstr_array[module_id].error_array_[error_id];
}
makefile:
.PHONY : all clean rebuild
ERR_INC := errheap.h errtmr.h
APP := app.out
APP_SRC := $(wildcard *.c)
APP_INC := $(wildcard *.h)
ERR2STR := err2str
ERR2STR_SRC := err2str.cpp
ERRSTR_DEF := errstr.def
all : $(APP)
@echo "BUILD SUCCESS => $^"
$(APP) : $(APP_SRC) $(APP_INC) $(ERRSTR_DEF)
gcc -o $@ $(filter %.c, $^)
$(ERRSTR_DEF) : $(ERR2STR) $(ERR_INC)
./$(ERR2STR) $(ERR_INC) > $@
$(ERR2STR) : $(ERR2STR_SRC)
g++ -o $@ $^
clean :
rm -fr $(APP) $(ERR2STR) $(ERRSTR_DEF) rebuild : clean all
小结:异常输出时,期望的是可读性强的描述,而不是错误码。
使用独立程序自动建立错误码到字符信息的映射。
代码自动生成技术建立于项目中的代码规范之上。
当项目中有固定规则编写的代码时,可以考虑代码自动生成。
24、异常处理下
使用c++独立程序,通过c++程序自动生成我们想要的c代码,c++独立程序编译过后就可以将定义异常的头文件作为输入产生输出,输出是我们想要的c代码,而这个c代码作用是异常错误码到对应的异常名字之间的映射。方便日志的打印。
自动代码生成需要考虑的问题:
输入:如果是数据(非代码)->数据是否被格式化组织(XML,JSon)
如果是代码->代码是否遵循严格的编码规范(有规律可寻)
输出:
是否需要基于代码模板完成?
是否需要动态“组装”生成代码?
问题分析:
常量名所在行均以ERROR_开头--》第一个常量名所在行包含模块ID->常量名在目标数组中的下标可计算-》MODULE_ERROR(常量名)
处理流程设计:
开始->输入文件是否合法->文件处理结束?->寻找枚举常量->生成错误码到常量名的映射代码->生成模块错误名初始化函数->生成全局错误名初始化函数->结束
分析err2str.cpp
小结:代码自动生成--静态->基于格式化参数(输入为数据)->数据描述方式 / 代码模板
代码自动生成--动态-->基于编码规范(输入为代码)
25、项目中需要思考的问题:
开发流程的定义
平台与框架的选择
目录结构及源码管理
嵌入式产品的开发效率
什么是软件开发流程?
通过一系列步骤保证软件产品的顺利完成。软件产品在生命期内的管理方法学。
软件开发流程的本质:开发流程与具体技术无关,开发流程是开发团队必须遵守的规则。
瀑布模型? 不是qt中讲的吗?
增量模型
螺旋模型
敏捷模型
2平台与框架的选择:
软件开发平台:开发平台是位于操作系统之上的软件层。开发平台提供更多模块化的功能,简化(加速)软件开发
软件开发框架:开发框架是位于开发平台之上的软件层,开发框架是为特定应用所设计的更抽象的软件模块。
平台与框架的示例->spring/java
0S-java-sprint-web应用程序
平台与框架示例2->qt
os--qt core(平台层)--状态机框架,模型视图框架,动画框架--框架层--桌面应用程序
3目录结构及源码管理
项目中每个模块的代码用一个文件夹进行管理。
文件夹由inc,src,makefile构成
项目中每个模块的对外函数声明统一放置同一目录中。
common.h xxxfunc.h
common 对外 module 具体功能 main 产品入口
目录设计的意义:
书架功能:反映项目中代码的层次感和模块化。
意识向导:引导对于新增文件功能,命名以及位置的思考。
增强维护性:加快开发人员对于项目整体架构的理解。
4嵌入式产品的开发效率
常规开发方式:
代码编写-代码修改-交叉编译-程序烧写-功能验证-结束
存在的问题:
开发工程师必须人手一台设备(项目早期可能无法满足)
每次代码改动必须到设备进行验证(效率低下)
反复多次烧写可能导致设备损坏(不稳定的早期设备)
嵌入式开发的调试问题:
需要基于额外硬件(JTAG)链接设备进行断点调试。
常规日志只能写于文件中,无法实时查看。
几乎无法进行现场调试(客户环境调试)
嵌入式基础设施的建设:
架构设计时,模块之间遵循强内聚,若耦合原则:模块能够基于pc环境编译,并进行单元测试。
开发pc环境中的设备模拟器:产品代码能够在pc环境完整编译并运行与pc环境。
开发产品中可实时查看输出的日志系统:设备运行日志输出可以实时传输到pc环境。