以menu專案為例分析程式碼中的軟體工程
本次作業使用孟老師上課時使用的menu小專案為例進行程式碼中的軟體工程分析,使用VS Code+GCC工具集為主要編譯除錯環境。
參考資料: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
-
準備工作
下載VS Code和GCC並配置,因為上次作業已經使用過VS Code,在這裡就不再贅述本機為Windows10系統,使用MinGW,下載官網為:https://osdn.net/projects/mingw/releases/,選擇適合自己的版本下載安裝即可。配置完成之後開啟cmd,輸入gcc -v,可以看到相關資訊。如圖
注意:windows作業系統不支援pthread函式庫,需要下載windows系統支援的版本,下載地址:http://sourceware.org/pub/pthreads-win32/pthreads-w32-2-8-0-release.exe,具體配置方法見:https://blog.csdn.net/htf15/article/details/16846143
程式執行截圖
-
程式碼風格和程式碼規範
在實際的軟體專案開發過程中,僅靠個人便能完成整個專案變得不再實際,事實上每個人都有自己需要完成的部分,而各個部分又是彼此關聯的,所以在完成自己工作的同時如何給合作伙伴帶來更大的便利性就成了一個很重要的問題,這時,良好的程式碼風格和程式碼規範就成了解決問題的關鍵。
- 程式碼風格的原則 簡明 易讀 無二義性
- 在符合上述原則的前提下,什麼才是好的程式碼風格呢,在這裡我們把程式碼風格分為三重境界
- 程式塊頭部註釋
- 第一重境界也是最基本的就是規範整潔。符合常規語言規範,合理使用空格、空行、縮排、註釋等。
- 第二重境界要求邏輯清晰。沒有冗餘程式碼,做到路基清晰不僅要求程式設計師的程式設計能力,更重要的是提高設計能力。
- 第三重境界是優雅。優雅的程式碼是設計的藝術,是編碼的藝術,是程式設計的最高追求。
如上圖,該程式塊頭部的註釋將函式功能、程式語言、執行環境、版本時間、簡單描述等一一列舉出來,這往往是模組的對外介面,以便自動生成開發者文件。
4.程式碼風格規範總結
- 縮排:4個空格;
- 行寬:<100個字元;
- 在一個函式體內,邏輯上密切相關的語句之間不加空行,邏輯上不相關的程式碼塊之間要適當留有空行;
- 在複雜的表示式中要用括號來表示邏輯優先順序;
- 不要把多條語句和多個變數的定義放在同一行;
- 命名:合適的命名大大增加程式碼的可讀性;
- 類名、函式名、變數名等的命名一定要與程式利的函式保持一致,便於閱讀理解;
- 一般變數名、物件名等使用LowerCamel風格;
- 型別、類、函式名等一般使用Pascal風格;
- 函式名一般使用動詞或者動賓短語,如get/set,RenderPage;
-
模組化設計
模組化是在軟體系統設計時保持系統內各部分相對獨立,以便每一個部分可以被獨立地進行設計和開發。這個做法背後的基本原理是關注點的分離,是由軟體工程領域的奠基性任務迪傑斯特拉提出來的。
耦合度是指軟體模組之間的依賴程度,一般可以分為緊密耦合、鬆散耦合和無耦合,在軟體設計中我們追求鬆散耦合。
內聚度是指一個軟體模組內部各種元素之間互相依賴的緊密程度。理想的內聚是功能內聚,也就是一個軟體模組只做一件事,只完成一個主要功能點或者一個軟體特性。
以lab3.1、lab3.2和lab3.3為例進行分析模組化設計的優點
lab3.1
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 "); } } }
可以看到在main函式裡有查詢功能,利用程式碼實現
lab3.2
int main() { /* cmd line begins */ while(1) { char cmd[CMD_MAX_LEN]; printf("Input a cmd number > "); scanf("%s", cmd); tDataNode *p = FindCmd(head, cmd); if( p == NULL) { printf("This is a wrong cmd!\n "); continue; } printf("%s - %s\n", p->cmd, p->desc); if(p->handler != NULL) { p->handler(); } } }
在lab3.2中,查詢功能變成了一個函式FindCmd()
在lab3.3中,本來menu.c中實現的全部功能分離了一部分放在了linklist.c中,menu.c需要做的只是引用linklist.h標頭檔案就可以實現原先的功能,實現了模組之間的互相呼叫。
軟體設計中的一些基本方法
KISS(Keep It Simple & Stipid)原則
- 一行程式碼只做一件事
- 一個程式碼塊只做一件事
- 一個函式只做一件事
- 一個軟體模組只做一件事
使用本地化外部介面來提高程式碼的適應能力
先寫虛擬碼的程式碼結構更好一些
在從設計到編碼的過程中假如虛擬碼要好於直接將設計翻譯成實現程式碼
-
可重用介面
介面的基本概念
- 介面就是互相聯絡的雙方共同遵守的一種協議規範,在我們軟體系統呢不一般的介面方式是通過定義一組API函式來約定軟體模組之間的溝通方式。換句話說,介面具體定義了軟體模組對系統的其他部分提供了怎樣的服務,以及系統的其他部分如何訪問所提供的服務。
由以上定義可知,可重用介面就是通用的介面,不能只是被某個業務特用。
那麼,以menu專案為例,簡要分析可重用介面的設計。
首先,將連結串列的資料結構和操作獨立出來,即將通用的LinkTable模組整合到menu程式中
struct LinkTable { tLinkTableNode *pHead; tLinkTableNode *pTail; int SumOfNode; pthread_mutex_t mutex; }; /* * Create a LinkTable */ tLinkTable * CreateLinkTable() { tLinkTable * pLinkTable = (tLinkTable *)malloc(sizeof(tLinkTable)); if(pLinkTable == NULL) { return NULL; } pLinkTable->pHead = NULL; pLinkTable->pTail = NULL; pLinkTable->SumOfNode = 0; pthread_mutex_init(&(pLinkTable->mutex), NULL); return pLinkTable; }
可以看到在使用LinkTable模組之後menu程式業務程式碼變得複雜,使用起來比較繁瑣,這是因為我們的介面定義的還不夠好,接下來讓我們進一步改進介面設計。
給LinkTable增加Callback方式的介面
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; }
給LinkTable增加Callback方式的介面,需要兩個函式介面,一個是call-in方式函式,如SearchLinkTableNode函式,其中有一個函式作為引數,這個作為引數的函式就是callback函式,如Conditiion函式。
這裡重點談一下callback函式,這個函式負責收集tLinkTableNode *pNode,一旦發現pNode,就返回pNode.
tLinkTableNode * SearchLinkTableNode(tLinkTable *pLinkTable, int Conditon(tLinkTableNode * pNode, void * args), void * args)
call-in方式的函式介面SearchLinkTableNode增加了一個引數args,callback函式Condition也增加了一個引數args,那麼這麼做的好處是什麼呢?或許可以從全域性變數cmd身上得到答案,因為cmd是全域性變數,耦合度過高,那麼就很不符合可重用介面的定義,這樣我們增加一個args引數,當呼叫介面的時候,只需要傳入指標就行。
-
可重入函式與執行緒安全
開始之前,先看看什麼是執行緒
- 執行緒是作業系統能夠進行運算排程的最小單位。它包含在程序之中,是程序的實際運作單位。一個執行緒指的是程序中一個單一順序的控制流,一個程序可以併發多個執行緒,每條執行緒執行不同的任務。
- 執行緒安全:如果程序中由多個執行緒在同時執行,而這些執行緒可能會同時執行這段程式碼,如果每次執行結果和單執行緒執行的結果一樣,而且其他的變數的值也和預期的是一樣的,就是執行緒安全的。
那麼什麼是可重入函式呢
- 可重入函式可以由多於一個任務併發使用,而不必擔心資料錯誤。
LinkTable軟體模組的執行緒安全分析
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; }
以本段程式碼為例,可以看到在刪除節點的時候,會進行加鎖,在節點刪除完畢後,會解鎖,通過互斥鎖實現了函式的可重入,在進行刪除操作時,加鎖之後就只有當前執行緒可以進行相關操作,自然不用擔心一你想執行緒安全。
-
總結
本次作業不僅對於基礎的程式碼風格和程式碼規範有了更深一步的瞭解,更重要的是對於程式碼中的軟體工程有了初步的瞭解,作業只是一次很小的實踐,在以後的工作中還需要不斷地提高,在專案中總結經驗,並把總結的經驗和學到的理論再應用到專案中去。