代码中的软件工程
前言
之前软件工程课程的学习,都是从宏观角度叙述,但很少从编码的角度切入,使人丈二和尚摸不着头脑。通过本节孟老师的课程,从实现一个通用menu小项目的案例出发,教我们如何编写出一个通用的,安全的,健壮的,高内聚,低耦合的符合软件工程思想的代码。
参考资料:https://gitee.com/mengning997/se/blob/master/README.md
一. 编译调试环境配置
1.在vs code 中添加C/C++扩展包

2.安装C++编译器和调试器
可以使用MinGW Installation Manager工具下载base setup,之后在环境变量中设置path,最后可以在cmd中执行命令gcc -v查看

3.运行hello.c

二. 工程化编程实战
2.1模块化分析
模块化(Modularity)是在软件系统设计时保持系统内各部分相对独立,以便每一个部分可以被独立地进行设计和开发。这种做法背后的基本原理是关注点的分离(SoC,Separation of Concerns)。
关注点得分离在软件工程领域是最重要的原则,习惯上成为模块化。
从软件的角度出发:模块化设计可以保持系统的可复用性和可维护性,实现稳定/性能双鲁棒性。模块间遵循松散耦合原则,除必要接口,尽量减少模块间、分系统、子系统间的逻辑依赖,尽量避免多对多关系,能解耦必解耦,任务模块相对独立。后期维护更新升级,互不干涉。
从项目管理的角度出发:模块化的同时,意味着除极少数核心架构设计者或任务分解者外,人力资源的流动对项目进度的影响会降低。
从开发者的角度出发:关注点分离的思想背后的根源是由于人脑处理复杂问题时容易出错,把复杂问题分解成一个个简单问题,从而减少出错的情形。
在menu项目中,源代码分为menu.c,menu.h,linktable.h,linktable.c。在.h文件中可以定义函数,常量,结构体,引入头文件等。在.c文件中引入.h头文件,使用.h文件中的定义。这样若以后定义变动,即可在.h文件中改动,保持代码变动简洁,易于维护。且该项目中,把对数据的直接操作都放在linktable.c文件中,menu.c文件只负责上层的逻辑,进一步解耦。
1 tLinkTable * CreateLinkTable();
2 /*
3 * Delete a LinkTable
4 */
5 int DeleteLinkTable(tLinkTable *pLinkTable);
6 /*
7 * Delete a LinkTableNode from LinkTable
8 */
9 int DelLinkTableNode(tLinkTable *pLinkTable,tLinkTableNode * pNode);
10 /*
11 * Search a LinkTableNode from LinkTable
12 * int Conditon(tLinkTableNode * pNode,void * args);
13 */
14 tLinkTableNode * SearchLinkTableNode(tLinkTable *pLinkTable, int Conditon(tLinkTableNode * pNode, void * args), void * args);
可以看到,上述在linktable.c文件中实现的部分函数都只是底层对数据的直接操作,与上层的逻辑无关。
2.2可重用软件设计
消费者重用是指软件开发者在项目中重用已有的一些软件模块代码,以加快项目工作进度。软件开发者在重用已有的软件模块代码时一般会重点考虑如下四个关键因素:
该软件模块是否能满足项目所要求的功能;
采用该软件模块代码是否比从头构建一个需要更少的工作量,包括构建软件模块和集成软件模块等相关的工作;
该软件模块是否有完善的文档说明;
该软件模块是否有完整的测试及修订记录;
如上四个关键因素需要按照顺序依次评估。
1 tLinkTableNode * GetLinkTableHead(tLinkTable *pLinkTable);
该函数名GetLinkTableHead清晰明确地表明了接口的目标;
该接口的前置条件是链表必须存在使用该接口才有意义,也就是链表pLinkTable != NULL;
使用该接口的双方遵守的协议规范是通过数据结构tLinkTableNode和tLinkTable定义的;
使用该接口之后的效果是找到了链表的头节点,这里是通过tLinkTableNode类型的指针作为返回值来作为后置条件,C语言中也可以使用指针类型的参数作为后置条件;
该接口没有特别要求接口的质量属性,如果搜索一个节点可能需要在可以接受的延时时间范围内完成搜索;
1 //回调函数
2 int SearchConditon(tLinkTableNode * pLinkTableNode,void * arg)
3 {
4 char * cmd = (char*)arg;
5 tDataNode * pNode = (tDataNode *)pLinkTableNode;
6 if(strcmp(pNode->cmd, cmd) == 0)
7 {
8 return SUCCESS;
9 }
10 return FAILURE;
11 }
12 //核心代码,搜索结点
13 tLinkTableNode * SearchLinkTableNode(tLinkTable *pLinkTable, int Conditon(tLinkTableNode * pNode, void * args), void * args)
14 {
15 if(pLinkTable == NULL || Conditon == NULL)
16 {
17 return NULL;
18 }
19 tLinkTableNode * pNode = pLinkTable->pHead;
20 while(pNode != NULL)
21 {
22 if(Conditon(pNode,args) == SUCCESS)
23 {
24 return pNode;
25 }
26 pNode = pNode->pNext;
27 }
28 return NULL;
29 }
分析:1.在函数SearchLinkTableNode中,将判断cmd指令的任务交给回调函数SearchConditon,不需要知道回调函数的具体实现,减少了耦合度,将函数的args参数传入回调函数,而不是某一指定字符串,使接口朝着通用的方向发展。
2.在回调函数SearchConditon中开放参数 *args,减少了内部逻辑暴露,泛化了接口,使接口更加通用,而不是局限于某一个业务逻辑。
3.若以后回调函数变动,可以不变动SearchLinkTableNode函数,只需要改动回调函数接口即可。
3.1可重入函数与线程安全
可重入(reentrant)函数可以由多于一个任务并发使用,而不必担心数据错误。相反,不可重入(non-reentrant)函数不能由超过一个任务所共享,除非能确保函数的互斥(或者使用信号量,或者在代码的关键部分禁用中断)。可重入函数可以在任意时刻被中断,稍后再继续运行,不会丢失数据。可重入函数要么使用局部变量,要么在使用全局变量时保护自己的数据。
如果代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行读写操作,一般都需要考虑线程同步,否则就可能影响线程安全。
3.2函数的可重入性与线程安全之间的关系
可重入的函数不一定是线程安全的,可能是线程安全的也可能不是线程安全的;可重入的函数在多个线程中并发使用时是线程安全的,但不同的可重入函数(共享全局变量及静态变量)在多个线程中并发使用时会有线程安全问题;
不可重入的函数一定不是线程安全的。
3.3LinkTable软件模块的线程安全分析

在LinkTable结构体中,声明了互斥锁 mutex,用于对临界区的互斥访问。
1 int DeleteLinkTable(tLinkTable *pLinkTable) 2 { 3 if(pLinkTable == NULL) 4 { 5 return FAILURE; 6 } 7 while(pLinkTable->pHead != NULL) 8 { 9 tLinkTableNode * p = pLinkTable->pHead; 10 pthread_mutex_lock(&(pLinkTable->mutex)); 11 pLinkTable->pHead = pLinkTable->pHead->pNext; 12 pLinkTable->SumOfNode -= 1 ; 13 pthread_mutex_unlock(&(pLinkTable->mutex)); 14 free(p); 15 } 16 pLinkTable->pHead = NULL; 17 pLinkTable->pTail = NULL; 18 pLinkTable->SumOfNode = 0; 19 pthread_mutex_destroy(&(pLinkTable->mutex)); 20 free(pLinkTable); 21 return SUCCESS; 22 }
在DeleteLinkTable函数中,当要删除节点时,先用函数pthread_mutex_lock()进行上锁,进入临界区,等到删除完成后,使用pthread_mutex_unlock()释放锁,以实现线程安全。在彻底删除链表之前使用pthread_mutex_destroy销毁互斥锁mutex。

可以看到在添加节点,等需要访问临界资源的操作中,都需要对临界区进行上锁,释放锁的操作,以确保线程安全。
总结
在软件开发过程中,需要遵循软件设计的方法和原则,即不断重构的设计方法论和Modularity,Interfaces,Information hiding,Incremental development,Abstraction,Generaity这几个重要的设计指导原则,孟老师的课带我们初窥了代码中的软件工程方法,软件工程是一门关于实践的学科,学习也需要在软件开发过程中继续不间断。
浙公网安备 33010602011771号