原创作品转载请注明出处 + 《软件工程(C编码实践篇)》MOOC课程http://mooc.study.163.com/course/USTC-1000002006
在网易云课堂上完孟宁老师的《软件工程(C编码实践篇)》之后,感觉收获很大。我觉得这门课最大的特点就是理论与实践相结合,利用实验楼的在线Linux虚拟机环境让我们自己做,在实践中慢慢体会软件工程的原理,使得枯燥的理论变得生动形象。
代码层面的软件工程的理解:在实验过程中,我体会最深的有三点。
一是“规范”。遵守代码规范的主要目的并不是为了能编译出执行效率更高的可执行程序,而是方便他人阅读。因为软件工程既然是工程,就需要多人的分工协作,一个程序员写的代码不仅仅是给自己看的,还需要给合作者看,所以代码要遵守一些约定俗成的规则,例如缩进要有层次,大括号独占一行等等。各大公司甚至出台了自己的编程规范,例如Google就先后出台过C++、Java、Python等多种语言的规范。
二是“分解”,即把一个复杂的大问题分解为若干简单的、容易解决的小问题,然后去解决这些小问题;而如何定义这些“小问题”,又涉及到抽象层级的确定,最典型的例子如TCP/IP的分层协议模型。
软件工程中的模块是指整个系统中一些相对对独立的程序单元,每个程序单元完成和实现一个相对独立的软件功能。通俗点就是一些独立的程序段。关于模块的设计,需要遵循“KISS”,即Keep it simple and stupid.
模块通过接口与其他模块进行交互。耦合是软件结构中各模块之间相互连接的一种度量,耦合强弱取决于模块间接口的复杂程度、进入或访问一个模块的点以及通过接口的数据。而内聚是从功能角度来度量模块内的联系,一个好的内聚模块应当恰好做一件事。它描述的是模块内的功能联系。
- 三是“通用”,编写代码不仅仅是解决“一个”问题,而是要去解决“一类”问题。为了实现这一点,实验五中采用了利用callback函数参数使Linktable的查询接口更加通用。如果把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用为调用它所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
tLinkTableNode * SearchLinkTableNode(tLinkTable *pLinkTable, int Condition(tLinkTableNode * pNode, void * args), void * args)
{
if(pLinkTable == NULL || Condition == NULL)
{
return NULL;
}
tLinkTableNode * pNode = pLinkTable->pHead;
while(pNode != NULL)
{
if(Condition(pNode, args) == SUCCESS)
{
return pNode;
}
pNode = pNode->pNext;
}
return NULL;
}
而Condition需要调用者定义:
int SearchCondition(tLinkTableNode * pLinkTableNode, void * args)
{
tDataNode * pNode = (tDataNode *)pLinkTableNode;
char * tempchar = args;
if(strcmp(pNode->cmd, args) == 0)
{
return SUCCESS;
}
return FAILURE;
}
调用形式如下:
tDataNode* FindCmd(tLinkTable * head, char * cmd)
{
return (tDataNode*)SearchLinkTableNode(head,SearchCondition, cmd);
}
总结:
学完这么课后,我最大的收获是自己写C语言的能力获得很大提升,知道了如何用C语言里面的技巧实现软件工程的原则。例如,以前本科学习“数据结构”写链表程序的时候,写链表操作一般是这么定义ADT的:
typedef struct StructNode
{
tLinkListNode *pNext;
elemtype data;
} tStructNode;
然后对其进行增删改查等操作
而在软件工程里面:
typedef struct LinkListNode
{
struct LinkListNode * pNext;
} tLinkListNode;
这个ADT里面只有指向自己的指针,对其写好增删改查等函数:
tLinkList * CreateLinkList();
int DeleteLinkList(tLinkList *pLinkList);
int AddLinkListNode(tLinkList *pLinkList,tLinkListNode *pNode);
int DelLinkListNode(tLinkList *pLinkList,tLinkListNode *pNode);
tLinkListNode *GetLinkListHead(tLinkList *pLinkList);
tLinkListNode *GetNextLinkListNode(tLinkList *pLinkList,tLinkListNode *pNode);
之后,定义包含数据域的ADT:
typedef struct StructNode
{
tLinkListNode *pNext;
elemtype data;
} tStructNode;
tStructNode* pNode = (tStructNode*)malloc(sizeof(tStructNode));
调用按照如下方式即可(*ppLinkList为指向链表head的指针):
AddLinkListNode(*ppLinkList,(tLinkListNode *)pNode);
pNode = (tStructNode*)malloc(sizeof(tStructNode));
pNode->data = elemdata;
需要注意的是,由于在(tLinkListNode *)pNode里面涉及强制类型转换,定义ADT的第一行必须定义指针,所以像下面的定义方式是错误的:
typedef struct StructNode
{
elemtype data;
tLinkListNode *pNext;
} tStructNode;
这么做就是为了符合软件工程中的“分离”原则,将链表操作与菜单数据分离,使得程序员可以专注实现一项功能。同时也使得程序更加通用,因为不论菜单是什么样子,都需要设计链表的操作。这项实验里还用到了函数指针,这让程序更加灵活:可以在不知道菜单形式的情况下把框架写好,以后再填入即可。
如果说不足的话,这门课的名称为“软件工程”,但实际上只涉及道路软件工程的一部分。软件工程还有其他的内容,例如面向对象的分析、设计模式等,这些都没有详细讲解。所以我也非常期待软件工程的后两部分,OO分析与设计篇和过程与管理篇。
附:实验报告集合
https://www.shiyanlou.com/courses/reports/1244095
https://www.shiyanlou.com/courses/reports/1245018
https://www.shiyanlou.com/courses/reports/1245520
https://www.shiyanlou.com/courses/reports/1246284