首页 > 代码库 > 记一次网易云课堂MOOC课程学习经历——《软件工程(C编码实践篇)》

记一次网易云课堂MOOC课程学习经历——《软件工程(C编码实践篇)》

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

  • 一、对课程的简要理解

正如计算机业内的一个非常经典的等式所言:

程序      =算法+数据结构

软件      =程序+软件工程

软件企业    =软件+商业模式

现代软件企业的成功离不开优秀的软件以及杰出的商业模式,同时,作为企业运营核心的软件亦离不开软件工程的指导。

作为商业软件而言,程序是软件的“内功”,“内功”分算法和数据结构两个方面,它们共同决定了程序运行的效率,但无关乎程序的正确与否。除了程序本身之外,软件工程才真正决定了软件的命运。软件工程是指导人们如何构建和开发软件的科学,是最优的组织软件开发的方法。

总的说来,有了软件工程设计思想的“武装”,以后就为开发规范、成功的软件打下了坚实的基础。

  • 二、课程中的实践

本课程有相对应的实验课程,在实验楼网站中运行虚拟的Linux环境来进行C语言的编程。

以下是实验的心得与体会,共六篇:

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

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

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

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

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

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

也欢迎大家到我的代码仓库查看源代码:http://git.shiyanlou.com/dustcenix/shiyanlou_cs122

  • 三、对代码的简要分析

在这个十分简单的“菜单”功能C程序的开发过程中,将会逐渐重现下列软件工程开发思想对软件开发的指导:

  1. 模块化

  2. 接口

  3. 信息隐藏

  4. 增量开发

  5. 抽象

  6. 代码重用

在开始的开始,还有一个十分重要的事情,那就是程序的代码风格。

什么是代码风格?什么是好的代码风格?

通常来说,代码风格就是,缩进、命名、注释等代码编排的风格规范。

好的代码风格的原则是:简明、易读、无二义性。

对于编译器来说,代码风格确实无关紧要,甚至对于一些追求极限性能的程序来说,有时候,考虑代码风格问题甚至会带来程序性能的下降(代码难以理解)。

但是,在实际开发的绝大多数场合,开发者所遇到的性能瓶颈问题,都基本不会是依靠打乱代码风格来解决的。

正相反,良好的代码风格,使程序代码简洁清晰,容易维护,也是开发人员之间的写作具有更高的效率。

 

(1)最原始的代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
int main()
{
    char command[128];
    time_t t;
    char Name[64]="NULL";
    while(1)
    {
        printf("command>>");
        scanf("%s",command);
        if(strcmp(command,"help") == 0)
        {
            printf("commandlist:help,info,echo,exit,time,setName,getName,clearName\n");
        }
        else if(strcmp(command,"info") == 0)
        {
            printf("author:Liu Dongxiao Version:1.0\n");
        }
        else if(strcmp(command,"echo") == 0)
        {
            scanf("%s",command);
            printf("%s\n",command);
        }
        else if(strcmp(command,"exit") == 0)
        {
            exit(0);
        }
        else if(strcmp(command,"time") == 0)
        {
            printf("Now:%s",ctime(&t));
        }
        else if(strcmp(command,"setName") == 0) 
        {
            printf("Please insert your name!\n");
            scanf("%s",Name);
        }
        else if(strcmp(command,"getName") == 0)
        {
            printf("%s\n",Name);
        }
        else if(strcmp(command,"clearName") == 0)
        {
            strcpy(Name,"NULL");
        }
        else printf("Wrong Command\n");    
    }
    return 0;
}

程序十分简单!学过C语言的都能看懂!  

(2)引入链表,实现代码的业务逻辑和数据存储之间的分离

前文的代码十分简单,同时也存在严重的不足,就是一点用也没有。

我们的目的是写一个可重用的菜单程序,上面那个连边也摸不着。

于是,我们引入了一种数据结构:链表

typedef struct dataNode
{
    char *cmd;
    char *desc;
    int (*handler)();
    struct dataNode *next;
}tDataNode;
tDataNode * findCMD(tDataNode *head,char *cmd);
void showAllCMD(tDataNode *head);

具体实现请参见代码库。

有了链表,再对源程序改造一番!

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include "Linklist.h"
#define CMD_MAX_LEN 128
#define DESC_LEN    1024
#define CMD_NUM     10
static time_t t;
void help();
void info();
void echo();
void Exit();
void Time();
static tDataNode menu[]=
{
        {"help","List all command in this program",help,&menu[1]},
        {"info","Show information",info,&menu[2]},
        {"echo","Repeat your input",echo,&menu[3]},
        {"exit","Exit this program",Exit,&menu[4]},
        {"time","Show time now",Time,NULL},
};
int main()
{
    char cmd[CMD_MAX_LEN];
    tDataNode *command;
    printf("Program is running\n");
    while (1)
    {
        printf("Command>>");
        scanf("%s",cmd);
        command=findCMD(menu,cmd);
        if(command == NULL)
        {
            printf("Command Not found\n");
        } else if(command->handler != NULL)
        {
            command->handler();
        }
    }
}
void help()
{
    showAllCMD(menu);
}
void info()
{
    printf("Author:Liu Dongxiao\nProgram Version:1.0\n");
}
void echo()
{
    char command[CMD_MAX_LEN];
    scanf("%s",command);
    printf("%s\n",command);
}
void Exit()
{
    printf("Program exited\n");
    exit(0);
}
void Time()
{
    printf("Now:%s",ctime(&t));
}

这样就将功能与存储分离了开来。同时,也应用了模块化的思想。

(3)改造链表,提升重用性

(2)中的链表很不完善,而且与源程序的耦合性也很强,继续改造之

typedef struct LinkListNode
{
    struct LinkListNode * pNext;
}tLinkListNode;
typedef struct LinkList
{
    tLinkListNode *pHead;
    tLinkListNode *pTail;
    int         SumOfNode;
}tLinkList;
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);
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>
#include "LinkList.h"
#define CMD_MAX_LEN 128
#define DESC_LEN    1024
#define CMD_NUM     10
void help();
void info();
void echo();
void Exit();
void Time();
typedef struct DataNode
{
    tLinkListNode *pNext;
    char* cmd;
    char* desc;
    void   (*handler)();
    struct DataNode *next;
}tDataNode;
tDataNode *FindCmd(tLinkList *head, char *cmd)
{
    tDataNode* pNode=(tDataNode*)GetLinkListHead(head);
    while(pNode !=NULL)
    {
        if(!strcmp(pNode->cmd,cmd))
        {
            return pNode;
        }
        pNode=(tDataNode*)GetNextLinkListNode(head,(tLinkListNode*)pNode);
    }
    return NULL;
}
int showAllCMD(tLinkList* head)
{
    tDataNode *pNode=(tDataNode*)GetLinkListHead(head);
    while(pNode !=NULL)
    {
        printf("%s - %s\n",pNode->cmd,pNode->desc);
        pNode =(tDataNode*)GetNextLinkListNode(head,(tLinkListNode*)pNode);
    }
    return 0;
}
int InitMenuData(tLinkList **ppLinkList)
{
    *ppLinkList=CreateLinkList();
    tDataNode* pNode=(tDataNode*)malloc(sizeof(tDataNode));
    pNode->cmd="help";
    pNode->desc="List all command in this program";
    pNode->handler=help;
    AddLinkListNode(*ppLinkList,(tLinkListNode *)pNode);
    pNode=(tDataNode*)malloc(sizeof(tDataNode));
    pNode->cmd="info";
    pNode->desc="Show information";
    pNode->handler=info;
    AddLinkListNode(*ppLinkList,(tLinkListNode *)pNode);
    pNode=(tDataNode*)malloc(sizeof(tDataNode));
    pNode->cmd="echo";
    pNode->desc="Repeat your input";
    pNode->handler=echo;
    AddLinkListNode(*ppLinkList,(tLinkListNode *)pNode);
    pNode=(tDataNode*)malloc(sizeof(tDataNode));
    pNode->cmd="exit";
    pNode->desc="Exit this program";
    pNode->handler=Exit;
    AddLinkListNode(*ppLinkList,(tLinkListNode *)pNode);
    pNode=(tDataNode*)malloc(sizeof(tDataNode));
    pNode->cmd="time";
    pNode->desc="Show time now";
    pNode->handler=Time;
    AddLinkListNode(*ppLinkList,(tLinkListNode *)pNode);
    return 0;
}
tLinkList* head=NULL;
int main()
{
    char cmd[CMD_MAX_LEN];
    InitMenuData(&head);
    tLinkListNode *command;
    printf("Program is running\n");
    while (1)
    {
        printf("Command>>");
        scanf("%s",cmd);
        tDataNode *p = FindCmd(head,cmd);
        if(p == NULL)
        {
            printf("Command Not found\n");
        } else if(p->handler!= NULL)
        {
            p->handler();
        }
    }
}
void help()
{
    showAllCMD(head);
}
void info()
{
    printf("Author:Liu Dongxiao\nProgram Version:1.0\n");
}
void echo()
{
    char command[CMD_MAX_LEN];
    scanf("%s",command);
    printf("%s\n",command);
}
void Exit()
{
    printf("Program exited\n");
    exit(0);
}
void Time()
{
    time_t t;
    printf("Now:%s",ctime(&t));
}

通过改造,就能在编码时方便的添加menu项啦!

(4)信息的隐藏与封装以及回调函数

可以看出,之前的代码中,我们将链表的结构体定义在LinkList.h中,而总所周知的是,C语言的库函数头文件中,存放的都只是函数和结构的声明,而不将实际的实现写在头文件中,这样做,可以防止其他人查看代码的内部实现,可以有效的隐藏信息。

typedef struct LinkListNode tLinkListNode;
typedef struct LinkList tLinkList;
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);
tLinkListNode *SearchLinkListNode(tLinkList *pLinkList,int Condition(tLinkListNode * pNode, void * args),void *args);

所谓回调,就是调用者调用被调用者,在被调用者执行的过程中,又去执行调用者代码段的过程逻辑。

我们在本程序中引入回调函数,是为了进一步增强链表的功能。

int SearchCondition(tLinkListNode * pNode, void * args){
    return (strcmp(((tDataNode*)pNode)->cmd,(char*)args)?0:1);
}
tDataNode *FindCmd(tLinkList *head, char *cmd)
{
        return (tDataNode*)SearchLinkListNode(head,SearchCondition,cmd);
}
tLinkListNode *SearchLinkListNode(tLinkList *pLinkList,int Condition(tLinkListNode * pNode, void * args),void *args)
{
    if(pLinkList == NULL)
    {
        return NULL;
    }
    tLinkListNode *tmp = pLinkList->pHead;
    while(tmp != NULL)
    {
        if(Condition(tmp,args) == 1)
        {
            return tmp;
        }
        tmp = tmp->pNext;
    }
    return NULL;
}

可以看出,在上述两段代码的过程逻辑中,主函数中的FindCMD调用了LinkList中的SearchLinkListNode,而在SearchLinkListNode中又使用了主函数传递过来的SearchCondition函数,这样可以做到,在主程序中定义了搜索条件,而不是预先定义在LinkList中。

通过一系列的改进,我们编写的程序做到了,在主函数中定义menu项,加入队列,自定义查询条件等一系列功能。

任务接近完成!

(5)最终的改造,接口设计

之前一系列的改造之后,menu功能实现的很顺利,是时候将整个代码封装成模块了。

int MenuConfig(char *cmd, char *desc, void (*handler)(int argc, char *argv[]));
int ExecuteMenu();

如果我们以后想使用menu模块,就要使用上面这两个接口。第一个用来增加menu项,第二个来启动menu页面。

接口的实现:

int MenuConfig(char *cmd, char *desc, void (*handler)(int argc, char *argv[]))
{
    if(head == NULL)
    {
        head = CreateLinkList();
        tDataNode *pNode = (tDataNode*) malloc(sizeof(tDataNode));
        pNode->cmd = "help";
        pNode->desc = "List all command in this program";
        pNode->handler = help;
        AddLinkListNode(head,(tLinkListNode *)pNode);
    }
    tDataNode* pNode=(tDataNode*)malloc(sizeof(tDataNode));
    pNode->cmd=cmd;
    pNode->desc=desc;
    pNode->handler=handler;
    AddLinkListNode(head,(tLinkListNode *)pNode);
    return 0;
};
int ExecuteMenu()
{
    int argc = 0;
    char cmd[CMD_MAX_LEN];
    char *argv[CMD_MAX_ARGV_NUM];
    char *command = NULL;
    MenuConfig("info","Show information\n\t‘-author‘ for author information\n\t‘-version‘ for version information",info);
    MenuConfig("exit","Exit this program",Exit);
    MenuConfig("time","Show time now",Time);
    printf("Program is running\n");
    while (1)
    {
        argc = 0;
        command = NULL;
        printf("Command>>");
        command=fgets(cmd,CMD_MAX_LEN,stdin);
        if (command == NULL)
        {
            continue;
        }
        command = strtok(command," ");
        while (command != NULL && argc < CMD_MAX_ARGV_NUM)
        {
            argv[argc] = command;
            argc++;
            command = strtok(NULL," ");
        }
        if(argc == 1)
        {
            int len = strlen(argv[0]);
            *(argv[0] + len -1) = \0;
        }
        tDataNode *p = FindCmd(head,argv[0]);
        if(p == NULL)
        {
            printf("Command Not found\n");
        } else if(p->handler!= NULL)
        {
            p->handler(argc,argv);
        }
    }
}

menu.c中原来的其他代码也有删改,详情请参见源码及实验报告。本实现中又新增了一个功能,即实现带参数的menu项。

使用示例:

    MenuConfig("info","Show information\n\t‘-author‘ for author information\n\t‘-version‘ for version information",info);
    MenuConfig("exit","Exit this program",Exit);
    MenuConfig("time","Show time now",Time);
#include "menu.h"
int main() {
    ExecuteMenu();
    return 0;
}

大功告成!

  • 四、结课总结

近两个月的学习已经结束了,本门课程的核心在于对软件开发思想的学习,而不是纠结于具体代码的一城一池,学习本课程的最大收获,就是在一个更高的理论层次上,总览全局,对软件的总体进行把握。只有学会了高屋建瓴,才能开发出高质量的软件。

除此之外,还对本课程抱有一丝遗憾:

(有一次课程作业忘记了互评……得分减半……好可惜……本句话删除)

希望可以在课程的学习中,实际动手做出一些更具有实用价值的软件。

感谢阅读!

记一次网易云课堂MOOC课程学习经历——《软件工程(C编码实践篇)》