软件设计——模块管理

简介: 软件设计——模块管理

文章目录

模块管理,保障系统有序运行

管理参照系

设计思路

程序实现

引入模块标识

实现层与级的表达

系统状态和回调函数原型定义

模块注册

系统启动

系统关闭

module示例程序

小结


这篇文章内容采自李云大牛编写的著作《专业嵌入式软件开发 全面走向高质高效编程》一书中的第14章内容。我看后根据自己在实际项目中遇到的情况感觉这一章内容对于程序设计及开发尤其重要,并且对于软件后期的正规维护都有很重要的参考价值,所以特地摘抄记录下来以供大家参考。

该书籍的电子版已经上传CSDN资源,大家可以下载阅读。

https://download.csdn.net/my


模块管理,保障系统有序运行

采用模块化的方法设计软件,早已成为行业的共识。模块化除了反映在方法上,还在实现上得下足功夫,既凸显模块的概念又实现整个系统的有序运行。


管理参照系

当一个系统比较复杂,所包含的模块数量比较多时,不可避免地会产生模块间负责的依赖关系,且有可能出现“牵一发而动全身”这种情形。无论采用怎么样的方法管理模块,都应当先将系统中所有模块之间的依赖关系通过某种方式表达出来,就比如同下图的形式表示:

20200221085014443.png

从上图中的各个模块之间的依赖关系可以看出,当系统进行初始化时堆管理模块应当最先被初始化,接着是紧跟其后的定时器管理模块和内存池管理模块,上图中所有模块的初始化顺序是从上到下的,且左右顺序并不重要。

为了更好表达模块间的依赖关系,我们需要引入分层的概念。一个系统中的模块可以分为三大层,分别是平台层、框架层和应用层。对于上图中模块分类采用分层的方式进行分割,可以得到下图这样的分类。

20200221085401961.png

对系统进行初始化的顺序显然应该是平台层最先,应用层最后。而终止化应采用完全相反的顺序,即先对应用层进行终止化,最后对平台层进行终止化。这遵循了顺序分配、逆序释放的原则。

当在系统中增加一个模块时,首先需要确定将其划入哪个层次。一个模块是否属于应用层相对容易判断,只要看这个模块实现的功能是否是产品所私有的。如果是,那说明该模块应属于应用层,否则就属于平台层或框架层。有时一些模块的归属并不明显,那么在这种情形下,最好一开始不要太计较,而是先给它定一个大概层次,等到项目开展下去对之认识加深后在调整也不迟。模块管理如果只有层的概念则粒度太大,因为在一层中也可能存在多个模块且模块之间也存在依赖关系。比如,上图中的平台层中就存在定时器管理模块依赖于堆管理模块。由此看来,在同一层中还需要进行划分,为此引入分级概念。对上图进行分割,每一层中自上而下分级。如下图:

20200221090145754.png

有了分层和分级的概念后,就为模块的初始化和终止化顺序提供了参照系。当新加入一个模块时,需要通过先分层后分级的方式以确定它应位于系统初始化过程中的哪一个点,这又间接地确立了终止化顺序点。在这个参照系中,模块的初始化顺序只与依赖关系中的上下位置有关,而与左右顺序无关。如果左右之间的模块存在某种情形下的依赖关系,那就将它们划分为不同级进从而转化为上下关系。


设计思路

模块管理最注意是做到能集中控制所有模块的初始化和终止化顺序,而要实现这个设计目的,方式之一是让每一个模块通过注册回调函数的形式实现控制反转。下图说明了模块管理模块的外部行为。

20200221090145754.png

如图中所示,每个模块都需要调用 module_register() 函数向模块管理模块进行注册,注册时需要指明模块所属层级和模块的回调函数。当系统启动时, system_up() 函数被调用,这时会直接触发模块管理模块根据层与级的顺序调用各注册模块的回调函数实现模块的初始化。反之,当系统终止时,system_down() 函数被调用,它将触发各模块终止化操作,终止化操作的顺序与初始化过程是完全相反的。

从追求简单的原则考虑,每一个注册模块只需注册一个回调函数。回调函数被设计成包含一个参数以指示每一次被调用时的目的。这里我们定义了“初始化(Initializing)”、“已启动(up)”、“已停止(down)”、“终止化(destroying)” 四种系统状态,每个函数的回调函数以它们作为参数值。如下图:

20200221100600907.png

注意:这里定义4个状态而非2个状态是考虑到——“初始化”和“已启动”是针对系统启动时的,“已停止”和“终止化”是针对系统终止时的,也就是说,为每一个模块分别提供两次初始化和终止化的机会。通过这样的定义有助于更好管理模块之间的依赖关系。比如,各个模块可以在“初始化”状态只依赖底层模块的初始化工作,而在“已启动”状态可以做依赖其他高层模块的初始化工作。


程序实现

到目前为止,我们已经塑造了模型,接下来看一看具体实现。


引入模块标识

要实现模块管理需要为模块定义数据结构。首先,需要引入模块标识值以方便识别和管理各个模块。标识值从0开始,采用这种方式有助于使用数组这种简单的数据结构实现模块管理,使得模块标识值能够作为数组下标。


typedef enm{
  MODULE_MODULE,  //模块管理模块
  MODULE_INTERRUPT, //中断管理
  MODULE_DEVICE,  //设备管理
  MODULE_CLOCK, //时钟管理
  MODULE_CONSOLE, //设备串口
  MODULE_CTRLC, //针对Ctrl+C的响应
  MOUDLE_FLASH, //flash设备
  MODULE_TIMER, //时间管理
  MODULE_TASK,  //task管理
  MODULE_SYNC,  //对task缓存目标管理
  MODULE_SEMAPHORE, //信号管理
  MODULE_MUTEX, //锁管理
  MODULE_QUEUE, //消息队列管理
  MODULE_HEAP,  //堆管理
  MODULE_MPOOL, //对内存池管理
  MODULE_TESTAPP, //测试应用模块
  MODULE_COUNT, //记录当前模块数据个数
  MODULE_LAST = (MODULE_COUNT - 1)  //数组end标识
}modeule_t;


其中,MODULE_COUNT和MODULE_LAST并不对应相应的模块,MODULE_COUNT用于表示整个系统一共有多少个模块,而MODULE_LAST表示系统中最后一个模块的标识值。当需要增加新模块标识值时,在枚举体中的MODULE_COUNT之前直接添加即可,这样不会影响到MODULE_COUNT和MODULE_LAST所表达的意思。

模块标识除了被运用于模块管理以外,还被运用于错误码管理中,以标识一个错误发生在哪一个模块。


实现层与级的表达

尽管在模块管理参照系中,层与级是完全不同的概念,但从实现的角度来看,这两个概念的目的却都是为了表达模块初始化和终止化时的先后顺序。因此,在程序实现中完全可以对它们采用统一的方法加以表达,但通过名称加以区分。如下的init_level_t数据类型以表达层与级的概念。


typedef enum{
  LEVEL_FIRST,
  CPU_LEVEL = LEVEL_FIRST,
  PERIPHERALS_LEVEL,
  DRIVER_LEVEL,
  OS_LEVEL,
  //平台层
  FLATFORM_LEVEL0,
  FLATFORM_LEVEL1,
  FLATFORM_LEVEL2,
  FLATFORM_LEVEL3,
  FLATFORM_LEVEL4,
  FLATFORM_LEVEL5,
  FLATFORM_LEVEL6,
  FLATFORM_LEVEL7,
  //框架层
  FRAMEWORK_LEVEL0,
  FRAMEWORK_LEVEL1,
  FRAMEWORK_LEVEL2,
  FRAMEWORK_LEVEL3,
  FRAMEWORK_LEVEL4,
  FRAMEWORK_LEVEL5,
  FRAMEWORK_LEVEL6,
  FRAMEWORK_LEVEL7,
  //应用层
  APPLICATION_LEVEL0,
  APPLICATION_LEVEL1,
  APPLICATION_LEVEL2,
  APPLICATION_LEVEL3,
  APPLICATION_LEVEL4,
  APPLICATION_LEVEL5,
  APPLICATION_LEVEL6,
  APPLICATION_LEVEL7,
  //level个数和level_last必须在最后,类似模块枚举类型的含义
  LEVEL_COUNT,
  LEVEL_LAST = (LEVEL_COUNT -1 )  
}init_level_t;


从上面的枚举内容可以看出,一共包含平台、框架和应用三大层,且每一层又定义了八个级,LEVEL0到LEVEL7。除了这三大层八大级外还二外定义了处理器级(CPU_LEVEL)、外设级(PERIPHERALS_LEVEL)、驱动级(DRIVER_LEVEL)和操作系统级(OS_LEVEL)。从某种意义上来说,这四个级可以被归类为平台层,将它们独立出来完全是为了使概念更清晰。


系统状态和回调函数原型定义

从前面定义了表示系统四个状态的system_state_t数据类型和模块回调函数原型 module_callback_t。正如前面提到的,模块回调函数是以系统状态为参数。如下:


typedef enum{
  STATE_INITIALIZING,
  STATE_UP,
  STATE_DOWN,
  STATE_DESTROYING
}system_state_t;
typedef error_t (*module_callback_t)(system_state_t _state);


模块注册

下列代码说明了用于管理模块所需的数据结构和全局变量。


typedef struct{
  dll_node_t node_;
  const char *p_name_;
  module_callback_t callback_;
  bool is_registered_;
}module_init_t;
static dll_t g_levels[LEVEL_COUNT];
static module_init_t g_modules[MODULE_CONT];


每一个模块需要使用一个module_init_t 结构实例来记录它的注册信息。module_init_t结构各成员变量的作用是:


  • node_:当用户进行模块注册时,会通过这个链表节点将相同初始化级别的模块串联在一起。
  • p_name_:用于记录模块名。这里存在一个假设,即所传入的模块名的内存并不是采用malloc()函数分配的。而是采用字符串字面值的形式传入的。这一假设使得可以省去内存拷贝而简化实现。
  • callback_:用于保存模块的回调函数。系统状态的变迁都将导致各模块的回调函数被依次调用。
  • is_registered_:这个变量用于放置模块的重复注册。当一个模块被注册后,这个变量的值将变成true。

其中数组g_levels为每一个初始化级别都定义了一个链表。当模块被注册到某一级别时,模块的注册信(即module_init_t实例)将挂接到对应的链表上。(同级模块以链表方式进行管理)。具体如下:


typedef struct dll_node{
  struct dll_node *prev;
  struct dll_node *next;
}dll_node_t, *dll_node_handle_t;
typedef struct {
  dll_node_t *head_;
  dll_node_t *tail_;
  usize_t count_;
}dll_t, *dll_handle_t;



module_register() 函数用来实现模块注册,其实现如下所示。该函数各参数的含义是:参数_name指明被注册模块的名称是什么;参数 _module 指示所需注册的模块标识值;参数 _level标明这一模块将属于哪一个初始化级别;参数 _callback 用于指示模块的回调函数。


error_t module_register(const char _name[], module_t _module, init_level_t _level, module_callback_t _callback)
{
  module_init_t *p_module;
  if(_module > MODULE_LAST)
  {
  return ERRCR_T(ERROR_MODULE_REG_INVMODULE);
  }
  if(_level > LEVEL_LAST)
  {
  return ERROR_T(ERROR_MODULE_REG_INVLEVEL);
  }
  if(null == _callback)
  {
  return ERROR_T(ERROR_MODULE_REG_INVCB);
  }
  p_module = &g_modules[_module];
  if(p_module->is_registered_)
  {
  return ERROR_T(ERROR_MODULE_REGISTERED);
  }
  p_module->p_name_ = _name;
  p_module->callback_ = _callback;
  p_module->is_registered_ = true;
  dll_push_tail(&g_levels[_level], &p_module->node_);
  return 0;
}


注意:这个注册函数并没有采用上锁的方式以防止出现竞争问题,原因是模块注册行为通常发生在系统的最开始阶段,而此时并不存在多任务问题。别忘了,任务的创建是在注册模块的回调函数中完成的。

从模块注册函数的实现来看,它只是告知了被注册模块处于什么级别,而真正的初始化或终止化操作动作并没用发送。这两个行为是通过调用 system_up() 和 system_down() 函数触发的。


系统启动

system_up() 函数的调用标志这系统开始启动。具体实现如下:


static system_state_t g_state;
static bool init_for_each(dll_t *_p_dll, dll_node_t *_p_node, void *_p_arg)
{
  module_init_t *p_module = (module_init_t *)_p_node;
  error_t result = p_module->callback_(STATE_INITALIZING);
  UNUSED(_p_dll);
  UNUSED(_p_arg);
  if(0 != result)
  {
  console_print("Error: can't initialize module %s (%s)", p_module->p_name, errstr(result));
  return false;
  }
  return true;
}
static bool up_for_each(dll_t *_p_dll, dll_node_t *_p_node, void *_p_arg)
{
  module_init_t *p_module = (module_init_t *)_p_node;
  error_t result = p_module->callback_(STATE_UP);
  UNUSED(_p_dll);
  UNUSED(_p_arg);
  if(0 != result)
  {
  console_print("Error: can't start up module %s (%s)", p_module->p_name_, errstr(result));
  return false;
  }
  return true;
}
error_t system_up()
{
  init_level_t level;
  g_state = STATE_INITALIZING;
  for(level = LEVEL_FIRST; level <= LEVEL_LAST; ++level)
  {
  if(0 != dll_traverse(&g_levels[level], init_for_each, (void *)&level))
  {
    return ERROR_T(ERROR_MODULE_INIT_FAILURE);
  }
  }
  g_state = STATE_UP;
  for(level = LEVEL_FIRST; level <= LEVEL_LAST; ++level)
  {
  if(0 != dll_traverse(&g_levels[level], up_for_each, (void *)&level))
  {
    return ERROR_T(ERROR_MODULE_UP_FAILURE);
  }
  }
  return 0;
}


system_up() 函数将从 LEVEL_FIRST 级开始,依次按序调用g_levels数组中各链表内所记录模块的回调函数,对于链表中各节点的遍历是通过调用 dll_traverse() 函数实现的。

值得一提的是,在system_up() 函数内通过使用LEVEL_FIRST 和 LEVEL_LAST 两个枚举值,使得 init_level_t 数据类型内的级别即使被更改也不必跟着更改,这提高了程序的可维护性。

为了方便阅读,下面给出了链表遍历函数 dll_traverse() 函数的实现。


dll_node_t *dll_traverse(dll_t *_p_dll, traverse_callback_t _cb, void *_p_arg)
{
  register dll_node_t *p_node = _p_dll->head_;
  if(null == cb)
  {
  return 0;
  }
  while((0 != p_node) && ((*_cb)(_p_dll, p_node, _p_arg)))
  {
  p_node = p_node->next;
  }
  return p_node;
}


系统关闭

系统关闭函数 system_down() 的实现与 system_up() 很相似。


static bool down_for_each(dll_t *_p_dll, dll_node_t *_p_node, void *_p_arg)
{
  module_init_t *p_module = (module_init_t *)_p_node;
  error_t result = p_module->callback_(STATE_DOWN);
  UNUSED(_p_dll);
  UNUSED(_p_arg);
  if(0 != result)
  {
  console_print("Error: can't shut down module %s (%s)", p_module->p_name, errstr(result));
  //!!! don't return false;
  }
  return true;
}
static bool destroy_for_each(dll_t *_p_dll, dll_node_t *_p_node, void *_p_arg)
{
  module_init_t *p_module = (module_init_t *)_p_node;
  error_t result = p_module->callback_(STATE_UP);
  UNUSED(_p_dll);
  UNUSED(_p_arg);
  if(0 != result)
  {
  console_print("Error: can't start up module %s (%s)", p_module->p_name_, errstr(result));
  //!!! don't return false;
  }
  return true;
}
void system_down()
{
  init_level_t level;
  g_state = STATE_DOWN;
  for(level = LEVEL_LAST; level > LEVEL_FIRST; --level)
  {
  (void)dll_traverse_reversely(&g_levels[level], down_for_each, null);
  }
  g_state = STATE_DESTROYING;
  for(level = LEVEL_LAST; level > LEVEL_FIRST; --level)
  {
  (void)dll_traverse_reversely(&g_levels[level], destroy_for_each, null);
  }
  dll_traverse_reversely(&g_levels[LEVEL_FIRST], destroy_for_each, null);
}


system_down() 函数的实现与 system_up() 函数有几个不同点。


  • 链表节点回调函数 destroy_for_each() 和 down_for_each() 在发现错误时并不返回错误,而只是输出一行错误信息。
  • system_down() 函数调用各个模块的注册回调函数的顺序与 system_up() 函数是逆序的。
dll_node_t *dll_traverse_reversely(dll_t *_p_dll, traverse_callback_t _cb, void *_p_arg)
{
  register dll_node_t *p_node = _p_dll->tail_;
  if(null == cb)
  {
  return 0;
  }
  while((0 != p_node) && ((*_cb)(_p_dll, p_node, _p_arg)))
  {
  p_node = p_node->prev_;
  }
  return p_node;
}


对于 system_down() 函数的调用可以考虑使用人为或信号触发。比如通过命令行、以太网想设备发送一个消息等等。在各模块实现其终止行为时,要考虑在进行真正的终止动作之前做必要检查,以确保它所管理的资源都被回收了。如果发现仍有资源没用被回收,可以通过日志等形式提示以帮助发现潜在的资源泄漏问题。


module示例程序

以下是用于测试模块管理功能的小程序。


#include <stdio.h>
#include <stdarg.h>
#include "module.h"
error_t module_timer(system_state_t _state)
{
  if(STATE_INITIALIZING == _state)
  {
  printf("info: timer module is initializing\n");
  }
  else if(STATE_UP == _state)
  {
  printf("info: timer module is up\n");
  }
  else if(STATE_DOWN == _state)
  {
  printf("info: timer module is down\n");
  }
  else if(STATE_DESTROYING == _state)
  {
  printf("info: timer module is destroying\n");
  }
  return 0;
}
error_t module_memory(system_state_t _state)
{
  if(STATE_INITALIZING == _state)
  {
  printf("info: memory module is initializing\n");
  }
  else if(STATE_UP == _state)
  {
  printf("info: memory module is up\n");
  }
  else if(STATE_DOWN == _state)
  {
  printf("info: memory module is down\n");
  }
  else if(STATE_DESTROYING == _state)
  {
  printf("info: memory module is destroying\n");
  }
  return 0; 
}
void module_registration_entry()
{
  (void) module_register("Timer", MODULE_TIMER, OS_LEVEL, module_timer);
  (void) module_register("Memory", MODULE_HEAP, OS_LEVEL, module_memory);
}
int main()
{
  module_registration_entry();
  printf("\nSystem is going to be up\n");
  if(0 != system_up())
  {
  printf("Error: system cannot be up\n");
  return -1;
  }
  printf("\nSystem is going to be down\n");
  system_down();
  return 0;
}


小结

通过引入分层和分级的概念,为模块的初始化和终止化提供了一个运行先后顺序的参照系,每一个模块都在这个参照系中占有一席之地。

模块管理的目的就是为了做到系统中各个模块有序启停,并在系统中强化模块的概念。在设计模块时,需要考虑在初始化过程中获取资源(包括任务创建),以及在终止化过程中释放资源。


相关文章
|
4月前
|
uml
系统分析与设计问题之什么是完全复用
系统分析与设计问题之什么是完全复用
|
6月前
|
存储 算法 安全
软件系统设计步骤与原理
软件系统设计步骤与原理
|
6月前
|
监控 项目管理 调度
深入探究ERP系统的项目管理模块
深入探究ERP系统的项目管理模块
149 3
|
存储 架构师 算法
架构设计的本质:系统与子系统、模块与组件、框架与架构
在软件研发这个领域,程序员的终极目标都是想成为一名合格的架构师。然而梦想很美好,但现实却很曲折。
架构设计的本质:系统与子系统、模块与组件、框架与架构
【系统架构】模块的耦合类型
【系统架构】模块的耦合类型
226 0
|
Java 关系型数据库 程序员
【组件设计开发】采用领域驱动设计设计和开发可组装的组件
采用领域驱动设计设计和开发可组装的组件
27946 7
【组件设计开发】采用领域驱动设计设计和开发可组装的组件
|
设计模式 监控 安全
内部系统界面设计【下】 | 设计技巧
关于内部系统 UI 设计的五个技巧
585 0
内部系统界面设计【下】 | 设计技巧
|
数据库 数据库管理
【软件系统分析与设计】
【软件系统分析与设计】
117 0
|
存储 消息中间件 缓存
系统设计基础理解下
《基础系列》
112 0
|
JSON uml 数据格式
设计系统
设计系统
161 0
设计系统
下一篇
无影云桌面