前言
首先感谢孟宁老师的教学指导,通过课上的学习,我了解到了许多软件工程的设计思想。
孟老师通过一个简单的menu小程序,直观细致地给我们讲解了代码规范、模块化设计、可重用接口以及线程安全等问题,我从中学到了很多。
本文中用到的menu程序源码:
https://github.com/mengning/menu
参考文献:
环境搭建
首先必须确保gcc已经安装好,在mac平台下用brew install gcc
即可安装,安装完成后查看gcc版本:
然后在vscode中安装C/C++插件
运行menu代码:
- 在项目目录下用
make
进行编译,然后./test
执行test案例,如下图所示即为运行成功
- 对各功能进行测试
代码测试正常
代码风格规范
以前我总是认为代码是写给机器看的,完全不注意代码的规范,这样的代码写出来,往往会让日后的维护变得非常的困难,连自己都不知道写的是什么含义,孟老师用menu程序里的代码给我做了个示范,让我深切明白了代码规范性的重要,代码不只是写给机器看的,更是写给人看的。
注释规范
/**************************************************************************************************/
/* Copyright (C) mc2lab.com, SSE@USTC, 2014-2015 */
/* */
/* FILE NAME : menu.c */
/* PRINCIPAL AUTHOR : Mengning */
/* SUBSYSTEM NAME : menu */
/* MODULE NAME : menu */
/* LANGUAGE : C */
/* TARGET ENVIRONMENT : ANY */
/* DATE OF FIRST RELEASE : 2014/08/31 */
/* DESCRIPTION : This is a menu program */
/**************************************************************************************************/
/*
* Revision log:
*
* Created by Mengning, 2014/08/31
*
*/
以这段menu.c中的头部注释为例:注释要使用英文,不要使用中文或特殊字符,要保持源代码是ASCII字符格式文件,要解释程序做什么,为什么这么做,以及特别需要注意的地方,每个源文件头部应该有版权、作者、版本、描述等相关信息。
命名规范
- 类名、函数名、变量名等的命名一定要与程序里的含义保持一致,以便于阅读理解;
- 类型的成员变量通常用m_或者_来做前缀以示区别;
- 一般变量名、对象名等使用LowerCamel风格,即第一个单词首字母小写,之后的单词都首字母大写,第一个单词一般都表示变量类型,比如int型变量iCounter;
- 类型、类、函数名等一般都用Pascal风格,即所有单词首字母大写;
- 类型、类、变量一般用名词或者组合名词,如Member
- 函数名一般使用动词或者动宾短语,如get/set,RenderPage;
typedef struct DataNode
{
tLinkTableNode * pNext;
char* cmd;
char* desc;
int (*handler)(int argc, char *argv[]);
} tDataNode;
int SearchConditon(tLinkTableNode * pLinkTableNode,void * arg)
{
char * cmd = (char*)arg;
tDataNode * pNode = (tDataNode *)pLinkTableNode;
if(strcmp(pNode->cmd, cmd) == 0)
{
return SUCCESS;
}
return FAILURE;
}
如menu.c中的这段代码为例:变量名为驼峰式,函数名、类型名首字母大写,并且变量名的含义与程序含义一致,读起来一目了然
模块化设计
简介
模块化是在软件系统设计时保持系统内各部分相对独立,以便每一个部分可以被独立地进行设计和开发。这个做法背后的基本原理是关注点的分离,关注点的分离的思想背后的根源是由于人脑处理复杂问题时容易出错,把复杂问题分解成一个个简单问题,从而减少出错的情形。模块化软件设计最终每一个软件模块都将只有一个单一的功能目标,并相对独立于其他软件模块,使得每一个软件模块都容易理解容易开发。
模块化程度的一个重要指标就是耦合度,耦合度是指软件模块之间的依赖程度,一般可以分为紧密耦合、松散耦合和无耦合。而内聚度是指一个软件模块内部各种元素之间互相依赖的紧密程度。
如何来具体实现模块化呢,就是将数据结构和它的操作与菜单业务处理进行分离处理,尽管还是在同一个源代码文件中,但是已经在逻辑上做了切分,可以认为有了初步的模块化。
模块化设计的实现
![image-20201105101310820](https://tva1.sinaimg.cn/large/0081Kckwgy1gke2tfi654j30b80ommyl.jpg)
还是以menu程序的代码为例,可以看出其分为三个模块:
-
程序的入口test
以下为test.c中的main函数内容,可以看出其主要作用是提供程序的入口,与用户打交道
int main()
{
PrintMenuOS();
SetPrompt("MenuOS>>");
MenuConfig("version","MenuOS V1.0(Based on Linux 3.18.6)",NULL);
MenuConfig("quit","Quit from MenuOS",Quit);
MenuConfig("time","Show System Time",Time);
MenuConfig("time-asm","Show System Time(asm)",TimeAsm);
ExecuteMenu();
}
-
菜单逻辑menu
menu.c中存放的是test.c背后逻辑的具体实现,将实现与调用相分离,便于代码的维护
/* add cmd to menu */
int MenuConfig(char * cmd, char * desc, int (*handler)())
{
tDataNode* pNode = NULL;
if ( head == NULL)
{
head = CreateLinkTable();
pNode = (tDataNode*)malloc(sizeof(tDataNode));
pNode->cmd = "help";
pNode->desc = "Menu List";
pNode->handler = Help;
AddLinkTableNode(head,(tLinkTableNode *)pNode);
}
pNode = (tDataNode*)malloc(sizeof(tDataNode));
pNode->cmd = cmd;
pNode->desc = desc;
pNode->handler = handler;
AddLinkTableNode(head,(tLinkTableNode *)pNode);
return 0;
}
-
菜单使用到的数据结构linktable
如下为Linktable.c中的代码,包括了结点的定义,以及对链表的创建,插入,删除等操作
/*
* LinkTable Type
*/
struct LinkTable
{
tLinkTableNode *pHead;
tLinkTableNode *pTail;
int SumOfNode;
pthread_mutex_t mutex;
};
/*
* Create a LinkTable
*/
tLinkTable * CreateLinkTable()
{
tLinkTable * pLinkTable = (tLinkTable *)malloc(sizeof(tLinkTable));
if(pLinkTable == NULL)
{
return NULL;
}
pLinkTable->pHead = NULL;
pLinkTable->pTail = NULL;
pLinkTable->SumOfNode = 0;
pthread_mutex_init(&(pLinkTable->mutex), NULL);
return pLinkTable;
}
可重用接口
简介
接口就是互相联系的双方共同遵守的一种协议规范,在我们软件系统内部一般的接口方式是通过定义一组API函数来约定软件模块之间的沟通方式。
软件设计中的模块化程度便成为了软件设计有多好的一个重要指标,一般我们使用耦合度(Coupling)和内聚度(Cohesion)来衡量软件模块化的程度,耦合度是指软件模块之间的依赖程度,一般可以分为紧密耦合(Tightly Coupled)、松散耦合(Loosely Coupled)和无耦合(Uncoupled)。一般在软件设计中我们追求松散耦合,因为这样的话接口就可以重复使用了。
要想使接口可重用,就得使接口通用化,通用接口定义的基本方法:参数化上下文, 移除前置条件,简化后置条件。
如何实现
具体还是以menu程序为例,看看是如何实现可重用接口的:
在linktable.h中可以看到一个新的数据结构,该结构体中只保留了最基本的遍历功能,具体的data数据并没有包含,这是因为用户可以自己添加自己所需要的数据,而linktable.h这个通用接口只需要实现最基本的遍历功能即可,无需关心数据,只需关心遍历这一个逻辑,这样就使接口更通用,可重用性更高。
typedef struct LinkTableNode
{
struct LinkTableNode * pNext;
}tLinkTableNode;
寻找某个节点时使用FindCmd方法,数据结构里的的寻找节点方法又通过SearchCondition来判断此节点是否是我们所找的节点,而SearchCondition用依赖cmd这个业务层中定义的变量,所以linktable中依然存在业务层的痕迹,所以需要修改。
int SearchCondition(tLinkTableNode * pLinkTableNode)
{
tDataNode * pNode = (tDataNode *)pLinkTableNode;
if(strcmp(pNode->cmd, cmd) == 0)
{
return SUCCESS;
}
return FAILURE;
}
/* find a cmd in the linklist and return the datanode pointer */
tDataNode* FindCmd(tLinkTable * head, char * cmd)
{
return (tDataNode*)SearchLinkTableNode(head,SearchCondition);
}
为了更加通用,可以修改cmd数组,使其变为局部变量,同时增加一个args参数
tLinkTableNode * SearchLinkTableNode(tLinkTable *pLinkTable, int Conditon(tLinkTableNode * pNode, void * args), void * args);
int SearchCondition(tLinkTableNode * pLinkTableNode, void * args)
{
char * cmd = (char*) args;
tDataNode * pNode = (tDataNode *)pLinkTableNode;
if(strcmp(pNode->cmd, cmd) == 0)
{
return SUCCESS;
}
return FAILURE;
}
线程安全
简介
线程是操作系统最小的执行单位,在一个进程中可能共享同一个全局变量。在一个并发的计算机中,同时有两个或以上的线程在运行,如果每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行读写操作,一般都需要考虑线程同步,否则就可能影响线程安全。所以比较合适的方法就是为这个变量上读写锁。
如何实现
以Linktable.c中的这段代码为例
- 在定义结构体时便定义了线程互斥量
typedef struct LinkTable
{
tLinkTableNode *pHead;
tLinkTableNode *pTail;
int SumOfNode;
pthread_mutex_t mutex;
}tLinkTable;
- 在结点的插入操作中加入加锁与解锁的操作,从而实现了线程安全
/*
* Add a LinkTableNode to LinkTable
*/
int AddLinkTableNode(tLinkTable *pLinkTable,tLinkTableNode * pNode)
{
if(pLinkTable == NULL || pNode == NULL)
{
return FAILURE;
}
pNode->pNext = NULL;
pthread_mutex_lock(&(pLinkTable->mutex)); //加锁
if(pLinkTable->pHead == NULL)
{
pLinkTable->pHead = pNode;
}
if(pLinkTable->pTail == NULL)
{
pLinkTable->pTail = pNode;
}
else
{
pLinkTable->pTail->pNext = pNode;
pLinkTable->pTail = pNode;
}
pLinkTable->SumOfNode += 1 ;
pthread_mutex_unlock(&(pLinkTable->mutex)); //释放锁
return SUCCESS;
}