代码中的软件工程
一.编译和调试环境配置
本次实验在Ubuntu20.04系统下,VS code + GCC工具集编译环境下完成。
1.安装VS code:下载VS code安装包后,在文件路径下执行:
sudo dpkg -i filename.deb
安装GCC:在终端执行:
sudo apt-get install gcc
使用 gcc -v 命令可以看到GCC已经安装完成:
root@ubuntu:/home/hasi# gcc -v
gcc version 9.3.0 (Ubuntu 9.3.0-17ubuntu1~20.04)
使用make编译menu项目,执行test:
hasi@ubuntu:~/Desktop/menu-master$ make
gcc -o test linktable.o menu.o test.o
hasi@ubuntu:~/Desktop/menu-master$ ./test
* * **** ****
*** *** ** ** * * * * **
* * * * * * * * * * * * **
* * * * * * * * * * * * ***
* ** * ****** * * * * * * **
* * * * * * * * * **
* * * * * * ** * * **
* * *** * * ** * **** ****
MenuOS>>help
help - Menu List
* help - Menu List
* version - MenuOS V1.0(Based on Linux 3.18.6)
* quit - Quit from MenuOS
* time - Show System Time
* time-asm - Show System Time(asm)
MenuOS>>quit
quit - Quit from MenuOS
顺利编译,执行成功,说明编译环境已搭建完成。
二.模块化设计
模块化是在软件系统设计时保持系统内各部分相对独立,以便每一个部分可以被独立地进行设计和开发。模块化软件设计的方法如果应用的比较好,最终每一个软件模块都将只有一个单一的功能目标,并相对独立于其他软件模块,使得每一个软件模块都容易理解容易开发。因此,软件设计中的模块化程度便成为了软件设计有多好的一个重要指标,一般我们使用耦合度和内聚度来衡量软件模块化的程度。
耦合度是指软件模块之间的依赖程度,一般可以分为紧密耦合、松散耦合和无耦合。一般在软件设计中我们追求松散耦合。

内聚度是指一个软件模块内部各种元素之间互相依赖的紧密程度。理想的内聚是功能内聚,也就是一个软件模块只做一件事,只完成一个主要功能点或者一个软件特性。
进行了模块化设计之后我们往往将设计的模块与实现的源代码文件有个映射对应关系,因此我们需要将数据结构和它的操作独立放到单独的源代码文件中,这时就需要设计合适的接口,以便于模块之间互相调用。
在menu项目中,分为了linktable,menu,test三个模块。其中,linktable.h文件里声明了数据结构及对其的操作函数,其具体实现在linktable.c中完成。
对数据结构的定义:
typedef struct LinkTableNode
{
struct LinkTableNode * pNext;
}tLinkTableNode;
对数据结构的操作:
/*
* Create a LinkTable
*/
tLinkTable * CreateLinkTable();
/*
* Delete a LinkTable
*/
int DeleteLinkTable(tLinkTable *pLinkTable);
/*
* Add a LinkTableNode to LinkTable
*/
int AddLinkTableNode(tLinkTable *pLinkTable,tLinkTableNode * pNode);
/*
* Delete a LinkTableNode from LinkTable
*/
int DelLinkTableNode(tLinkTable *pLinkTable,tLinkTableNode * pNode);
/*
* Search a LinkTableNode from LinkTable
* int Conditon(tLinkTableNode * pNode,void * args);
*/
tLinkTableNode * SearchLinkTableNode(tLinkTable *pLinkTable, int Conditon(tLinkTableNode * pNode, void * args), void * args);
/*
* get LinkTableHead
*/
tLinkTableNode * GetLinkTableHead(tLinkTable *pLinkTable);
/*
* get next LinkTableNode
*/
tLinkTableNode * GetNextLinkTableNode(tLinkTable *pLinkTable,tLinkTableNode * pNode);
具体实现:
/*
* 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;
}
/*
* Delete a LinkTable
*/
int DeleteLinkTable(tLinkTable *pLinkTable)
{
if(pLinkTable == NULL)
{
return FAILURE;
}
while(pLinkTable->pHead != NULL)
{
tLinkTableNode * p = pLinkTable->pHead;
pthread_mutex_lock(&(pLinkTable->mutex));
pLinkTable->pHead = pLinkTable->pHead->pNext;
pLinkTable->SumOfNode -= 1 ;
pthread_mutex_unlock(&(pLinkTable->mutex));
free(p);
}
pLinkTable->pHead = NULL;
pLinkTable->pTail = NULL;
pLinkTable->SumOfNode = 0;
pthread_mutex_destroy(&(pLinkTable->mutex));
free(pLinkTable);
return SUCCESS;
}
在menu.c中导入linktable.h即可实现导入数据结构及其操作。
#include "menu.h"
三.可重用接口
接口就是互相联系的双方共同遵守的一种协议规范,在我们软件系统内部一般的接口方式是通过定义一组API函数来约定软件模块之间的沟通方式。换句话说,接口具体定义了软件模块对系统的其他部分提供了怎样的服务,以及系统的其他部分如何访问所提供的服务。在面向过程的编程中,接口一般定义了数据结构及操作这些数据结构的函数;而在面向对象的编程中,接口是对象对外开放的一组属性和方法的集合。函数或方法具体包括名称、参数和返回值等。
接口规格包含五个基本要素:
•接口的目的;
•接口使用前所需要满足的条件,一般称为前置条件或假定条件;
•使用接口的双方遵守的协议规范;
•接口使用之后的效果,一般称为后置条件;
•接口所隐含的质量属性。
下面的函数接口代码是取出链表头节点的函数声明,以此来分析函数接口的基本要素。
tLinkTableNode * GetLinkTableHead(tLinkTable *pLinkTable);
1.接口目的是取出链表的头节点,函数名GetLinkTableHead即表明接口目的;
2.接口前置条件是存在链表pLinkTable,即链表pLinkTable != NULL;
3.双方遵守的协议规范是通过数据结构tLinkTableNode和tLinkTable定义的;
4.接口后置条件是找到了链表的头节点,并返回tLinkTableNode类型的指针;
Callback接口:
是指通过参数将函数传递到其它代码的,某一块可执行代码的引用。这一设计允许了底层代码调用在高层定义的子程序。
回调机制提供了非常大的灵活性,在回调中,我们利用某种方式,把回调函数像参数一样传入中间函数。可以这么理解,在传入一个回调函数之前,中间函数是不完整的。换句话说,程序可以在运行时,通过登记不同的回调函数,来决定、改变中间函数的行为。
下面的代码是搜索链表中的节点的函数声明
tLinkTableNode * SearchLinkTableNode(tLinkTable *pLinkTable, int Conditon(tLinkTableNode * pNode, void * args), void * args)
{
}
作为参数的Conditon函数就是Callback接口。pLinkTable参数为Callin方式的接口。
四.线程安全
如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行读写操作,一般都需要考虑线程同步,否则就可能影响线程安全。通常可以使用mutex防止两条线程同时对同一公共资源进行读写。
在linktable.c中声明了互斥锁:mutex
struct LinkTable
{
tLinkTableNode *pHead;
tLinkTableNode *pTail;
int SumOfNode;
pthread_mutex_t mutex;
};
在AddLinkTableNode函数中,先lock 实现该线程对链表操作的独占,操作完成后unlock 释放对链表的独占:
/*
* 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;
}
五.总结
本文主要参考孟宁老师工程化编程实战——代码中的软件工程一文。
在menu项目由helloworld小程序发展壮大的过程中,我感受到了良好的代码习惯,模块化设计,可重入设计,线程安全等理念在项目开发中的重要意义。我也会将本次实践中所学到的知识积极应用在今后的软件开发中。

浙公网安备 33010602011771号