软件工程方法:Menu程序分析
参考:https://gitee.com/mengning997/se/tree/master
在上过孟宁老师的高级软件工程课程后,我对于软件工程有了更加深刻的理解。
我又仔细研读了menu这个项目的代码,这个项目的确很有启发意义,对于我们平常的开发也很有价值
1. 代码的运行与开发过程
在CentOS系统中,通过yum命令安装gcc。而后便可以对程序进行编译和运行
同样也可以使用VSCode与gcc环境进行编译
我们下面对整个开发过程进行回顾
代码的开发过程
版本编号 | 版本更新内容 | 作用 |
Lab 1 | 添加hello.c,menu.c | 初步测试,原型设计 |
Lab 2 | 去掉hello.c | |
Lab 3.1 | 添加DataNode结构体与处理函数 | 进行事物与数据结构的隔离 |
Lab 3.2 | 添加显示函数与查找函数 | 对数据结构的操作进行函数化 |
Lab 3.3 | 添加linklist.h与linklist.c | 将数据结构的操作与源文件隔离 |
Lab 4 | 添加tlinktable.c,test.c | 添加LinkTable与Node数据结构 |
Lab 5.1 | 去掉test.c | |
Lab 5.2 | 添加Makefile | 便于进行make |
Lab 7.1 | 添加menu.h等 | 将菜单作为独立模块 |
Lab 7.2 | 添加readme | 添加项目说明 |
以上的开发过程显示整个开发过程是快速迭代的这也是符合快速开发的过程的。
2 代码分析
我们更进一步说明代码中不同部分的关系
上面的图片说明了不同结构体的组成元素和接口,其中加粗部分重点说明了结构体的功能。
下面我们会从几个小的角度来分析menu的代码编写过程中体现的软件工程。
数据结构与事务的隔离
首先体现的是数据结构之间的隔离,即顶层数据结构与底层数据结构之间的隔离。在这个项目中,LinkTableNode和LinkTable总体要实现的就是将不同的命令实现为一个链表,从而完成动态的删除和添加。而DataNode才是menu中调用的数据结构。进行数据结构的隔离。从开发角度来讲是有助于降低耦合的。当然,不同的数据结构也体现了一种抽象的思想,将整个命令链表作为一个整体,而后将接口提供给后续要使用它的数据结构,这就是一种抽象的思想。
其次体现的是事务之间的隔离,最终LinkTable提供的是一组接口以对LinkTable进行修改,这是一种事务,这个事务与menu部分接收用户的输入并调用相应的处理函数是不相关的,所以通过提供接口可以进行事务之间的隔离。不同的事务是有差别的,如果全部作为一个文件把所有函数实现为静态函数,这样会在开发时造成一些混乱和安全上的问题,如果作为一个团队的话,某个人可能会修改到和他本人并不相关的代码。所以通过接口来实现事务之间的隔离是很有必要的。
回调函数
回调函数是指DataNode中的函数指针,在不同的命令下,可以通过该指针指向不同的处理函数。在对menu的功能实现的过程中,会出现很多不同的命令,如果都将其实现为静态函数显然是不够好的,所以通过这种方法在适当情况进行回调是符合软件工程的设计原则的。同时,函数指针也可以作为其他函数的传入参数,从某种角度来讲,也是模块化的一种体现。
强制类型转换
这一段的第二行实现了一个强制类型转换,由于tDataNode类型和pLinkTableNode类型中都有一个同类型的pnext(同名)指针,所以在类型转换后,其pnext的偏移量都是相同的,这也就实现了一个数据结构的转换,将pNode中的next元素赋值。这种方式对于我来讲是十分新颖的,我进一步思考,也可以通过同样的方式,将功能有联系,但成员不同的数据结构互相转换,特别是在多人协同开发的情况下,利用这种语言特性可以实现抽象与分工合作。
3 线程安全与可重入函数
线程安全的主要目的是避免多个线程在同一时刻对命令链表进行操作,主要的实现方式是加锁,也就是对可能引起链表改变的部分加锁,但是加锁和释放锁的条件需要很好的控制。
在LinkTable结构体中有一个信号量mutex来实现互斥,也就是说,每一个链表有一个信号量。如果对于锁的控制不好的话,会有一些问题产生,此时这个函数是不可重用的:
我们来分析删除的这段代码所产生的问题:
不妨假设存在两个线程,线程1与线程2,如果线程1和线程2同时进入循环,而线程1先加锁的话,会出现这样的一个情况
此时线程1中的p指向pHead所指向的结点,即1号结点,而线程2也会指向1号结点。此时线程1执行剩余的语句,会将pHead指针向后移动一次,即形成第二个状态:
这时线程1释放锁,线程2开始执行剩余代码,仍会将pHead向后移动一格,但是p2指针还是指向1,无论线程1还是线程2谁先执行free释放空间,最终都会导致内存的访问错误,所以这一段代码是线程不安全的。通过对这一段代码的阅读,我们明显的看出,对代码的线程安全控制是要十分谨慎的。
可重用接口
可重用接口是用于实现软件重用开发的重要部分,对于已经验证正确的部分可以放入这个接口中,这也就可以大大的提高软件的开发效率,同时一个实现良好的接口可以减少耦合。这也是我们实现不同模块之间的低耦合的方法
在LinkTableNode和LinkTable的开发过后实际上将所有对命令行的操作全部放在这个模块之内,这样的接口可以被所有的部分使用。实际上在这个模块中的实现很好的,利用了两个数据结构,最终提供的接口只涉及数据结构。
typedef struct LinkTableNode { struct LinkTableNode * pNext; }tLinkTableNode; struct LinkTable { tLinkTableNode *pHead; tLinkTableNode *pTail; int SumOfNode; pthread_mutex_t mutex; }; tLinkTableNode * GetLinkTableHead(tLinkTable *pLinkTable);
总结
menu的这个项目是十分符合软件工程的思想的,通过C语言的语言特性和一些设计方法实现了数据结构和事务的隔离,并通过信号量实现了线程安全。这个项目对我的启发很大,最主要是在于代码的结构上的,我以后会更多的思考怎样的代码设计是更合理的,更安全的。