程式碼中的軟體工程--menu專案
1、前言
本篇博文基於孟寧老師上課內容和所提供資料(https://gitee.com/mengning997/se/blob/master/README.md#%E4%BB%A3%E7%A0%81%E4%B8%AD%E7%9A%84%E8%BD%AF%E4%BB%B6%E5%B7%A5%E7%A8%8B)而完成,通過一個menu專案,淺談一些我對於軟體工程的理解。感謝孟寧老師在我此過程中所給予的幫助!
2、通過VSCode裝C++及環境配置
1、開啟VSCode,在擴充套件欄內搜尋c++,並安裝。
2、下載MinGW,找到最新版的 "x86_64-posix-seh"。安裝MinGW。
3、配置MinGw環境。
配好後,按下 win + R,輸入cmd,回車鍵之後輸入g++,再回車,驗證環境是否配置成功。出現如下介面,即代表配置成功。
4、配置C++環境,在VSCode內點選執行欄,點選建立launch.json檔案。
選擇C++(GDB/LLDB)
對檔案進行修改,如下圖所示。其餘配置按所給提示配好。
修改後:
"version": "0.2.0", "configurations": [ { "name": "(gdb) 啟動", "type": "cppdbg", "request": "launch", "program": "${fileDirname}\\${fileBasenameNoExtension}.exe", "args": [], "stopAtEntry": false, "cwd": "${workspaceFolder}", "environment": [], "externalConsole": true, "MIMode": "gdb", "miDebuggerPath": "D:\\MinGW\\mingw64\\bin\\gdb.exe", //自己路徑 "setupCommands": [ { "description": "為 gdb 啟用整齊列印", "text": "-enable-pretty-printing", "ignoreFailures": true } ],"preLaunchTask": "task g++" } ] }
該檔案配置完成後,返回hello.c檔案,按F5執行,點選“任務配置”,配置tasks.json,如下所示。
{ "version": "2.0.0", "tasks": [ { "type": "shell", "label": "task g++", //修改此項 "command": "D:\\MinGW\\mingw64\\bin\\g++.exe", "args": [ "-g", "${file}", "-o", "${fileDirname}\\${fileBasenameNoExtension}.exe" ], "options": { "cwd": "D:\\MinGW\\mingw64\\bin" }, "problemMatcher": [ "$gcc" ], "group": "build" } ] }
至此,環境配置完畢。
3、程式碼的成長
在menu專案中,我們可以很清晰看到整個程式碼的成長過程。最開始的lab1中僅僅可以判別輸入是否是help,而後面的每一個版本都在前一版本的基礎上進行新功能的新增和原功能的改進,直到最後實現了一個完整的選單功能。
4、模組化設計
模組化軟體設計的主要思想是,在設計較複雜的程式時,一般會採用自頂向下的方法,將問題劃分為幾個部分,再將各個部分再次細化,直到分解為比較好解決的問題為止。這樣做可以有效地降低程式的複雜度,使程式設計、除錯和維護等操作變得簡單化。
此外,為了衡量模組化程度,我們引入了耦合度和內聚度的概念。其中,耦合度是指軟體模組之間的依賴程度;而內聚度是指一個軟體模組內部各種元素之間互相依賴的緊密程度。
程式碼中模組化的主要目的是為了包容變化,當我們的需求發生變化時,我們可以儘可能的減少對程式碼的修改。下面我們結合menu專案裡lab3.1中的menu.c的具體程式碼來看看模組化具體是如何實現的。
#include <stdio.h> #include <stdlib.h> int Help(); int Quit(); #define CMD_MAX_LEN 128 #define DESC_LEN 1024 #define CMD_NUM 10 typedef struct DataNode { char* cmd; char* desc; int (*handler)(); struct DataNode *next; } tDataNode; static tDataNode head[] = { {"help", "this is help cmd!", Help,&head[1]}, {"version", "menu program v1.0", NULL, &head[2]}, {"quit", "Quit from menu", Quit, NULL} }; int main() { /* cmd line begins */ while(1) { char cmd[CMD_MAX_LEN]; printf("Input a cmd number > "); scanf("%s", cmd); tDataNode *p = head; while(p != NULL) { if(strcmp(p->cmd, cmd) == 0) { printf("%s - %s\n", p->cmd, p->desc); if(p->handler != NULL) { p->handler(); } break; } p = p->next; } if(p == NULL) { printf("This is a wrong cmd!\n "); } } } int Help() { printf("Menu List:\n"); tDataNode *p = head; while(p != NULL) { printf("%s - %s\n", p->cmd, p->desc); p = p->next; } return 0; } int Quit() { exit(0); }
這段程式碼便是一段比較經典的使用模組化設計的程式碼。我們可以看到,在這段程式碼中main()部分程式碼邏輯是基本不需要修改的,而我們主要需要進行改動的是命令描述部分及其處理函式部分的程式碼,主要是下面部分的程式碼:
儘管上面的程式碼已經完成了初步的模組化,但還可以進一步改進,使其可以滿足開閉原則,即對擴充套件開放,對修改關閉。我們發現上面的程式碼在功能性需求出現變化時,我們可以很好的進行維護,但當一些非功能性需求發生改變(例如資料結構發生變化等情況時),我們就需要對上述程式碼進行大量的修改。因此,我們可以把整段程式碼劃分為業務邏輯層和資料儲存層。我們把與功能相關的程式碼放在menu.c檔案中,把與資料結構及其操作相關的程式碼放在linklist.h檔案中在,並且在linklist.c檔案中實現所需函式,於是便有了lab3.3中的程式碼。
5、可重用介面
在對程式碼進行初步模組化設計之後,我們還對程式碼進行優化,便需要為程式碼設計一些合適的介面,進一步消除模組間的耦合,使每個模組所實現的功能更加清晰。
在menu專案中,我們用到了連結串列的資料結構,而在一個比較大的專案中,我們可能會多次用到連結串列的結構。因此,我們希望能寫一個連結串列的通用模組,方便以後的重用。於是便有lab4中linktable.c和linktable.h檔案。
#ifndef _LINK_TABLE_H_ #define _LINK_TABLE_H_ #include <pthread.h> #define SUCCESS 0 #define FAILURE (-1) /* * LinkTable Node Type */ typedef struct LinkTableNode { struct LinkTableNode * pNext; }tLinkTableNode; /* * LinkTable Type */ typedef struct LinkTable { tLinkTableNode *pHead; tLinkTableNode *pTail; int SumOfNode; pthread_mutex_t mutex; }tLinkTable; /* * 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); /* * Search a LinkTableNode from LinkTable * int Conditon(tLinkTableNode * pNode); */tLinkTableNode * GetLinkTableHead(tLinkTable *pLinkTable); /* * get next LinkTableNode */ tLinkTableNode * GetNextLinkTableNode(tLinkTable *pLinkTable,tLinkTableNode * pNode); #endif /* _LINK_TABLE_H_ */
但在使用通用的linktable模組後,menu程式業務程式碼變得複雜了許多,顯然是因為我們的介面定義的不夠好。於是,便進行了lab4到lab5的進化。下面是lab5中linktable.h檔案:
#ifndef _LINK_TABLE_H_ #define _LINK_TABLE_H_ #include <pthread.h> #define SUCCESS 0 #define FAILURE (-1) /* * LinkTable Node Type */ typedef struct LinkTableNode { struct LinkTableNode * pNext; }tLinkTableNode; /* * LinkTable Type */ typedef struct LinkTable { tLinkTableNode *pHead; tLinkTableNode *pTail; int SumOfNode; pthread_mutex_t mutex; }tLinkTable; /* * 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); /* * Search a LinkTableNode from LinkTable * int Conditon(tLinkTableNode * pNode); */ tLinkTableNode * SearchLinkTableNode(tLinkTable *pLinkTable, int Conditon(tLinkTableNode * pNode)); /* * get LinkTableHead */ tLinkTableNode * GetLinkTableHead(tLinkTable *pLinkTable); /* * get next LinkTableNode */ tLinkTableNode * GetNextLinkTableNode(tLinkTable *pLinkTable,tLinkTableNode * pNode); #endif /* _LINK_TABLE_H_ */
優化程式碼的精髓在於callback方法。在上面兩段程式碼中,唯一變化在於lab5中的linktable.h中新增了一個SearchLinkTableNode函式,這是一個acll-in方式的函式;而在lab5中的menu.c檔案中又新增了一SearchCondition函式,這是一個callback方式函式(如下所示)。利用這種方式就可以使得linktable介面變得更加通用。
int SearchCondition(tLinkTableNode * pLinkTableNode) { tDataNode * pNode = (tDataNode *)pLinkTableNode; if(strcmp(pNode->cmd, cmd) == 0) { return SUCCESS; } return FAILURE; }
6、執行緒安全
執行緒安全是指在多執行緒同時執行時,它們可能同時運行同一段程式碼,如果它們執行的結果與期待的結果相同的話,則說明是執行緒安全的。執行緒安全問題只會出現在寫操作上,而讀操作無需考慮。我們通常採用鎖機制來保證執行緒安全。以lab7.1中的linktable.c為例,程式碼中對於刪除節點的函式,新增節點的函式均採用上鎖的方法來保證執行緒的安全。
int DeleteLinkTable(tLinkTable *pLinkTable) { if(pLinkTable == NULL) { return FAILURE; } while(pLinkTable->pHead != NULL) { tLinkTableNode * p = pLinkTable->pHead; pthread_mutex_lock(&(pLinkTable->mutex)); pLinkTable->pHead = pLinkTable->pHead->pNext; pLinkTable->SumOfNode -= 1 ; pthread_mutex_unlock(&(pLinkTable->mutex)); free(p); } pLinkTable->pHead = NULL; pLinkTable->pTail = NULL; pLinkTable->SumOfNode = 0; pthread_mutex_destroy(&(pLinkTable->mutex)); free(pLinkTable); return SUCCESS; } /* * Add a LinkTableNode to LinkTable */ int AddLinkTableNode(tLinkTable *pLinkTable,tLinkTableNode * pNode) { if(pLinkTable == NULL || pNode == NULL) { return FAILURE; } pNode->pNext = NULL; pthread_mutex_lock(&(pLinkTable->mutex)); if(pLinkTable->pHead == NULL) { pLinkTable->pHead = pNode; } if(pLinkTable->pTail == NULL) { pLinkTable->pTail = pNode; } else { pLinkTable->pTail->pNext = pNode; pLinkTable->pTail = pNode; } pLinkTable->SumOfNode += 1 ; pthread_mutex_unlock(&(pLinkTable->mutex)); return SUCCESS; } /* * Delete a LinkTableNode from LinkTable */ int DelLinkTableNode(tLinkTable *pLinkTable,tLinkTableNode * pNode) { if(pLinkTable == NULL || pNode == NULL) { return FAILURE; } pthread_mutex_lock(&(pLinkTable->mutex)); if(pLinkTable->pHead == pNode) { pLinkTable->pHead = pLinkTable->pHead->pNext; pLinkTable->SumOfNode -= 1 ; if(pLinkTable->SumOfNode == 0) { pLinkTable->pTail = NULL; } pthread_mutex_unlock(&(pLinkTable->mutex)); return SUCCESS; } tLinkTableNode * pTempNode = pLinkTable->pHead; while(pTempNode != NULL) { if(pTempNode->pNext == pNode) { pTempNode->pNext = pTempNode->pNext->pNext; pLinkTable->SumOfNode -= 1 ; if(pLinkTable->SumOfNode == 0) { pLinkTable->pTail = NULL; } pthread_mutex_unlock(&(pLinkTable->mutex)); return SUCCESS; } pTempNode = pTempNode->pNext; } pthread_mutex_unlock(&(pLinkTable->mutex)); return FAILURE; }
7、小結
以上便是我通過閱讀menu專案程式碼,結合孟寧老師上課所講的內容以及所提供的學習資料,對軟體工程的一個初步的理解。希望可以將所學到的內容應用在以後所寫的程式碼中,使程式碼更簡潔、更易讀、更便於重用。再次感謝孟寧老師在學習過程中所給予的幫助!
8、參考資料
https://github.com/mengning/menu