软件工程(C编码实践篇)学习总结

来源:互联网 发布:古鹰 知乎 编辑:程序博客网 时间:2024/06/15 19:48

孙玉强 原创作品转载请注明出处 《软件工程(C编码实践篇)》MOOC课程http://mooc.study.163.com/course/USTC-1000002006

对软件工程的理解与心得

长达两个月的《软件工程(C编码实践篇)》的学习终于画上了句点。C语言是我接触的第一门计算机编程语言,我想也是许多计算机相关专业同学学习过的第一门编程语言。C语言语法经典,运行速率快,虽然自诞生之日起已过了四十余年,C语言依然凭借着其良好的特性在软件底层开发和教学领域发挥着重要的作用。

但是仅仅学习过编程语言,对软件开发来说是远远不够的。一个软件的开发,不仅是功能的实现,还需要考虑代码的复用、代码的可重入、代码的模块化、代码的可读性、代码的风格以及线程安全等问题。事实上根据“80-20原则“,在软件开发过程中,功能实现占80%的比重,问题处理占20%的比重,但在所花费的时间上,功能实现只占20%,而问题处理则占到了80%,这也就是我们要学习软件工程的原因。

孟宁老师的软件工程课以C语言编码为例,指导我们将一个menu小程序从最初十分简单的雏形,一步步扩展成一个代码可复用、可重入,且线程安全、模块化程度非常高的程序。menu小程序大致经过了这样一个“进化”的过程:

switch case —— 使用链表 —— 可重用链表 —— callback机制 —— 独立为子程序

以往我们在解决多输入问题的时候,最先想到的就是使用switch case语句,代码结构非常简单,这显然是远远达不到软件工程的要求的。经过初步的改造,我们引入链表这一数据结构使代码内部模块化:

static tDataNode head[] = {    {"help",   "Command List",   ShowHelp,  &head[1]},    {"exit",   "Exit from menu", Exit,      &head[2]},    {"open",   "Open file",      NULL,      &head[3]},    {"close",  "Close file",     NULL,      &head[4]},    {"commit", "Commit file",    NULL,      &head[5]},    {"kill",   "Kill process",   NULL,      &head[6]},    {"time",   "Show time now",  ShowTime,  &head[7]},    {"file",   "File list",      ShowFile,  NULL},};

通过定义上述的结构体数组,我们可以逐条进行条件判断,同时相比于switch case方式更易于扩展,这也是许多开源软件常用的写法。

虽然使代码简单易读是软件工程的目的之一,但是我们还要考虑代码代码的可复用性、线程安全等问题。作为提升代码通用性的第一步,我们要通过定义接口的方式使链表独立出去作为一个模块,并且可被多个工程使用,即提升了链表的可重用性。

/* *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);/* *Get LinkTableHead */tLinkTableNode * GetLinkTableHead(tLinkTable * pLinkTable);/* *Get Next LinkTableNode */tLinkTableNode * GetNextLinkTableNode(tLinkTable * pLinkTable, tLinkTableNode * pNode);

我们创建一个linklist.h文件作为接口,里面只定义链表的各种操作,而将操作的具体实现在linklist.c中完成,具体的逻辑操作可由用户自行定制,只需调用接口即可。代码的模块化到这一步已出具端倪。

经过menu v2.5的改造后,链表的通用性已经非常高了。在实现模块独立上还有一个“大招”,就是callback机制。对于callback机制的解释,有一个非常精妙的比喻:

你到一个商店买东西,刚好你要的东西没有货,于是你在店员那里留下了你的电话,过了几天店里有货了,店员就打了你的电话,然后你接到电话后就到店里去取了货。在这个例子里,你的电话号码就叫回调函数,你把电话留给店员就叫登记回调函数,店里后来有货了叫做触发了回调关联的事件,店员给你打电话叫做调用回调函数,你到店里去取货叫做响应回调事件。

用户所编写的逻辑代码需要调用我们的通用链表模块,由于用户要实现的功能不尽相同,所以很多操作无法在通用链表中实现。于是用户可以在自己的逻辑处理部分编写回调函数,执行具体的操作,在调用链表的同时,链表在底层也会对逻辑处理部分的回调函数进行调用,形成回调响应。

/** *回调函数 */int SearchCondition(tLinkTableNode * pLinkTableNode, void *args){    char *cmd = (char *)args;    tDataNode * pNode = (tDataNode *)pLinkTableNode;    if(strcmp(pNode->cmd, cmd) == 0)    {        return  SUCCESS;      }    return FAILURE;           }/** *接口调用回调函数 */ tLinkTableNode * SearchLinkTableNode(tLinkTable *pLinkTable, int Conditon(tLinkTableNode * pNode, void *args), void *args){    if(pLinkTable == NULL || Conditon == NULL)    {        return NULL;    }    tLinkTableNode * pNode = pLinkTable->pHead;    while(pNode != NULL)    {            if(Conditon(pNode, args) == SUCCESS)        {            return pNode;                            }        pNode = pNode->pNext;    }    return NULL;}

利用callback机制,可以进一步增强代码模块的独立性,也更具通用性。

最后我们所编写的menu小程序并不是作为一个独立的应用而存在的,而是要嵌入其它的大型的软件中去。因此我们在创建了通用链表的基础上我们还要使整个menu成为一个独立的模块,用户自己编写的逻辑处理部分调用整个menu模块二不仅是通用链表。

初次之外,我们还需考虑代码的线程安全。在可能会发生代码重入的地方加线程锁,当某一线程访问该段代码时其它线程无法访问代码,当线程锁被释放后,另一个线程才可以访问代码。值得注意的是,可重入代码不一定是线程安全的,但线程安全的代码一定是可重入的。


历次实验报告:

以下是历次实验的实验报告,包括实验步骤和解析。实验均在实验楼完成,可在原链接中启动实验环境对实验进行回归测试,谨供参考。

  • 实验一:写一个hello world小程序

  • 实验二:命令行菜单小程序V1.0

  • 实验三:内部模块化的命令行菜单小程序V2.0

  • 实验四:用可重用的链表模块来实现命令行菜单小程序V2.5

  • 实验五:用callback增强链表模块来实现命令行菜单小程序V2.8

  • 实验七:将menu设计为可重用的子系统

总结:

在这段学习过程中,我不仅对软件工程有了很深入的了解,编程能力有了很大的提升,最直观的体现就是,遇到多问题处理问题不会一股脑的写下一串switch case代码。也明白了为什么一些开源软件会写的那么复杂,虽然和我们用简单的if els、switch case实现的功能相同,但是逻辑更加严谨,而且代码的通用性要高得多。此外,我对代码的编写风格也有了很多认识,良好的代码编写风格虽然并不会提升编译的效率,但是对于团队开发来说大有裨益。

原创粉丝点击