在Menu小程式中學習軟體工程方法
前言
在編寫程式碼的過程中充分運用軟體工程的思想是一個優秀程式設計師不可或缺的技能。一個優秀的程式應該具有良好的可靠性、容錯性、易用性、易讀性、可擴充性、可理解性和可維護性等等效能。為了達到這些要求,我們應該學會如何讓自己編寫的程式更加符合軟體工程的思想,通過學習程式碼編寫的規範和各種面向物件思想的實現方式,我們就能在今後的職業生涯中一帆風順。
學習完孟寧老師的課程後,瞭解到了許多軟體工程的設計思想,需要注意的事項。老師通過一個menu小程式生動形象的介紹了程式碼的風格規範、模組化設計思想(高內聚,低耦合)、可重用介面的實現、使用回撥函式和注意執行緒安全等問題。
本篇文章將會分析孟寧老師的menu小程式來學習軟體工程設計中的思想,從工程中汲取知識和習慣將會是提升自己最快的方法。在上完孟寧老師的高階軟體工程的課程後,我對軟體工程思想的瞭解更近了一步。下面將會對每一個部分進行詳細的介紹。
參考文獻:https://gitee.com/mengning997/se/blob/master/README.md#程式碼中的軟體工程
一.VSCode中C/C++編譯環境配置
本專案的編譯執行環境為VSCdoe,首先我們應該配置C/C++的編譯環境
1.安裝C/C++ extension
2.編輯配置檔案tasks.json和launch.json來配置程式碼的編譯命令和Debug環境:
//tasks.json的檔案內容 { "version": "2.0.0", "tasks": [ { "type": "cppbuild", "label": "C/C++: clang build active file", "command": "/usr/bin/clang", "args": [ "-g", "${file}", "-o", "${fileDirname}/${fileBasenameNoExtension}" ], "options": { "cwd": "/usr/bin" }, "problemMatcher": [ "$gcc" ], "group": { "kind": "build", "isDefault": true }, "detail": "compiler: /usr/bin/clang" } ] } //launch.json的檔案內容 { "version": "0.2.0", "configurations": [ { "name": "(lldb) Launch", "type": "cppdbg", "request": "launch", "program": "enter program name, for example ${workspaceFolder}/a.out", "args": [], "stopAtEntry": false, "cwd": "${workspaceFolder}", "environment": [], "externalConsole": false, "MIMode": "lldb" } ] }
3.編譯執行,測試是否配置成功:
二.程式碼風格規範
1.程式碼風格的原則:
程式碼的風格規範可以總結為:簡約而不簡單。具體來說有簡明、易讀、無二義性這三個特性。我們把程式碼的風格分成三重境界:一是規範整潔。遵守常規語言規範,合理使用空格、空行、縮排、註釋等;二是邏輯清晰。沒有程式碼冗餘、重複,讓人清晰明瞭的命名規則。做到邏輯清晰不僅要求程式設計師的程式設計能力,更重要的是提高設計能力,選用合適的設計模式、軟體架構風格可以有效改善程式碼的邏輯結構,會讓程式碼簡潔清晰;三是優雅。優雅的程式碼是設計的藝術,是編碼的藝術,是程式設計的最高追求。
我們拿lab3.1版本中的menu.c程式來說明具體含,如果我們程式碼編寫為這個形式:
int main(){
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 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 ");
}
}
}
2.程式塊頭部的註釋:
檔案頭註釋和程式段註釋應該遵守規範:
- 註釋和版權資訊:註釋也要使用英文,不要使用中文或特殊字元,要保持原始碼是ASCII字元格式檔案;
- 不要解釋程式是如何工作的,要解釋程式做什麼,為什麼這麼做,以及特別需要注意的地方;
- 每個原始檔頭部應該有版權、作者、版本、描述等相關資訊
3.具體程式碼規範總結:
程式碼編寫規範:
- 縮排:4個空格;
- 行寬:< 100個字元;
- 程式碼行內要適當多留空格,如“=”、“+=” “>=”、“<=”、“+”、“*”、“%”、“&&”、“||”、“<<”,“^”等二元操作符的前後應當加空格。對於表示式比較長的for語句和if語句,為了緊湊起見可以適當地去掉一些空格,如for (i=0; i<10; i++)和if ((a<=b) && (c<=d));
- 在一個函式體內,邏揖上密切相關的語句之間不加空行,邏輯上不相關的程式碼塊之間要適當留有空行以示區隔;
- 在複雜的表示式中要用括號來清楚的表示邏輯優先順序;
- 花括號:所有 ‘{’ 和 ‘}’ 應獨佔一行且成對對齊;
- 不要把多條語句和多個變數的定義放在同一行;
命名規範:
- 類名、函式名、變數名等的命名一定要與程式裡的含義保持一致,以便於閱讀理解;
- 型別的成員變數通常用m_或者_來做字首以示區別;
- 一般變數名、物件名等使用LowerCamel風格,即第一個單詞首字母小寫,之後的單詞都首字母大寫,第一個單詞一般都表示變數型別,比如int型變數iCounter;
- 型別、類、函式名等一般都用Pascal風格,即所有單詞首字母大寫;
- 型別、類、變數一般用名詞或者組合名詞,如Member
- 函式名一般使用動詞或者動賓短語,如get/set,RenderPage;
三.模組化設計
模組化設計是指在對一定範圍內的不同功能或相同功能不同效能、不同規格的產品進行功能分析的基礎上,劃分並設計出一系列功能模組,通過模組的選擇和組合可以構成不同的產品,以滿足市場的不同需求的設計方法。
一個軟體系統可按功能不同劃分成若干功能模組。軟體系統的層次結構正是模組化的具體體現。把一個大而複雜的軟體系統劃分成易於理解的比較單純的模組結構,這些模組可以被組裝起來以滿足整個問題的需求。
模組化軟體設計的方法如果應用的比較好,最終每一個軟體模組都將只有一個單一的功能目標,並相對獨立於其他軟體模組,使得每一個軟體模組都容易理解容易開發。從而整個軟體系統也更容易定位軟體缺陷bug,因為每一個軟體缺陷bug都侷限在很少的一兩個軟體模組內。而且整個系統的變更和維護也更容易,因為一個軟體模組內的變更隻影響很少的幾個軟體模組。因此,軟體設計中的模組化程度便成為了軟體設計有多好的一個重要指標,一般我們使用耦合度(Coupling)和內聚度(Cohesion)來衡量軟體模組化的程度。
對比menu小程式的lab3.2版本和lab3.3版本,我們發程式中添加了linklist.c檔案和linklist.h檔案。進行了模組化設計之後我們往往將設計的模組與實現的原始碼檔案有個對映對應關係,因此我們需要將資料結構和它的操作獨立放到單獨的原始碼檔案中,這時就需要設計合適的介面,以便於模組之間互相呼叫。
內聚度:內聚度是指一個軟體模組內部各種元素之間互相依賴的緊密程度。理想的內聚是功能內聚,也就是一個軟體模組只做一件事,只完成一個主要功能點或者一個軟體特性(Feather)。
耦合度:耦合度是指軟體模組之間的依賴程度,一般可以分為緊密耦合(Tightly Coupled)、鬆散耦合(Loosely Coupled)和無耦合(Uncoupled)。一般在軟體設計中我們追求鬆散耦合。
良好模組設計標準:
- 模組可分解性:可將系統按問題/子問題分解的原則分解成系統的模組層次結構。
- 模組可組裝性:可利用已有的設計構件組裝成新系統,不必一切從頭開始。
- 模組可理解性:一個模組可不參考其他模組而被理解。
- 模組連續性:對軟體需求的一些微小變更只導致對某個模組的修改而整個系統不用大動。
- 模組保護:將模組內出現異常情況的影響範圍限制在模組內部。
- 我們在模組化程式設計中應該追求高內聚、低耦合。
- 遵守KISS(Keep It Simple & Stupid)原則
- 一行程式碼只做一件事
- 一個塊程式碼只做一件事
- 一個函式只做一件事
- 一個軟體模組只做一件事
四.可重用軟體設計
1.可重用軟體簡介
在軟體開發中,由於不同的環境和功能要求,我們可以通過對以往成熟軟體系統的區域性修改和重組,保持整體穩定性,以適應新要求。這樣的軟體稱為可重(chong)用軟體。據統計,現今,開發一個新的應用系統,40%~60%的程式碼是重複以前類似系統的成分,重複比例有時甚至更高。因此,軟體重用能節約軟體開發成本,真正有效地提高軟體生產效率。
2.介面的設計
介面對於實現軟體可重用有著重要的意義。介面就是互相聯絡的雙方共同遵守的一種協議規範,在我們軟體系統內部一般的介面方式是通過定義一組API函式來約定軟體模組之間的溝通方式。換句話說,介面具體定義了軟體模組對系統的其他部分提供了怎樣的服務,以及系統的其他部分如何訪問所提供的服務。在面向過程的程式設計中,介面一般定義了資料結構及操作這些資料結構的函式;而在面向物件的程式設計中,介面是物件對外開放(public)的一組屬性和方法的集合。函式或方法具體包括名稱、引數和返回值等。
介面的五個要素:
- 介面的目的;
- 介面使用前所需要滿足的條件,一般稱為前置條件或假定條件;
- 使用介面的雙方遵守的協議規範;
- 介面使用之後的效果,一般稱為後置條件;
- 介面所隱含的質量屬性。
3.menu程式舉例
為了更好的理解軟體的可重用性設計,我們可以檢視lab4中的linktable.h介面檔案:
/********************************************************************/
/* Copyright (C) SSE-USTC, 2012-2013 */
/* */
/* FILE NAME : linktabe.h */
/* PRINCIPAL AUTHOR : Mengning */
/* SUBSYSTEM NAME : LinkTable */
/* MODULE NAME : LinkTable */
/* LANGUAGE : C */
/* TARGET ENVIRONMENT : ANY */
/* DATE OF FIRST RELEASE : 2012/12/30 */
/* DESCRIPTION : interface of Link Table */
/********************************************************************/
/*
* Revision log:
*
* Created by Mengning,2012/12/30
*
*/
#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);
/*
* get LinkTableHead
*/
tLinkTableNode * GetLinkTableHead(tLinkTable *pLinkTable);
/*
* get next LinkTableNode
*/
tLinkTableNode * GetNextLinkTableNode(tLinkTable *pLinkTable,tLinkTableNode * pNode);
#endif /* _LINK_TABLE_H_ */
通過啟用linktable.h介面檔案,就實現了軟體的可重用設計,為了進一步實現軟體的可重用設計,我們新增一個callback方式的介面使Linktable的查詢介面更加通用。給Linktable增加Callback方式的介面,需要兩個函式介面,一個是call-in方式函式,如SearchLinkTableNode函式,其中有一個函式作為引數,這個作為引數的函式就是callback函式,如程式碼中Conditon函式。
SearchLinkTableNode函式:
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;
}
Condition函式:
int SearchCondition(tLinkTableNode * pLinkTableNode, void * args)
{
char * cmd = (char*) args;
tDataNode * pNode = (tDataNode *)pLinkTableNode;
if(strcmp(pNode->cmd, cmd) == 0)
{
return SUCCESS;
}
return FAILURE;
}
4.介面和耦合度之間的關係:
對於軟體模組之間的耦合度,前文中提到,耦合度是指軟體模組之間的依賴程度,一般可以分為緊密耦合(Tightly Coupled)、鬆散耦合(Loosely Coupled)和無耦合(Uncoupled)。一般在軟體設計中我們追求鬆散耦合。更細緻地對耦合度進一步劃分的話,耦合度依次遞增可以分為無耦合、資料耦合、標記耦合、控制耦合、公共耦合和內容耦合。這些耦合度劃分的依據就是介面的定義方式,我們接下來重點分析一下公共耦合、資料耦合和標記耦合。
公共耦合:
- 當軟體模組之間共享資料區或變數名的軟體模組之間即是公共耦合,顯然兩個軟體模組之間的介面定義不是通過顯式的呼叫方式,而是隱式的共享了共享了資料區或變數名。
資料耦合
- 在軟體模組之間僅通過顯式的呼叫傳遞基本資料型別即為資料耦合。
標記耦合
- 在軟體模組之間僅通過顯式的呼叫傳遞複雜的資料結構(結構化資料)即為標記耦合,這時資料的結構成為呼叫雙方軟體模組隱含的規格約定,因此耦合度要比資料耦合高。但相比公共耦合沒有經過顯式的呼叫傳遞資料的方式耦合度要低。
五.可重入函式和執行緒安全
1.基本概念
執行緒(thread)是作業系統能夠進行運算排程的最小單位。它包含在程序之中,是程序中的實際運作單位。一個執行緒指的是程序中一個單一順序的控制流,一個程序中可以併發多個執行緒,每條執行緒並行執行不同的任務。一般預設一個程序中只包含一個執行緒。作業系統中的執行緒概念也被延伸到CPU硬體上,多執行緒CPU就是在一個CPU上支援同時執行多個指令流,而多核CPU就是在一塊晶片上集成了多個CPU核,比如4核8執行緒CPU晶片就是在集成了4個CPU核,每個CPU核上支援2個執行緒。有了多核多執行緒CPU,作業系統就可以讓不同程序執行在不同的CPU核的不同執行緒上,從而大大減少程序排程程序切換的資源消耗。傳統上作業系統工作在單核單執行緒CPU上是通過分時共享CPU來模擬出多個指令執行流,從而實現多程序和多執行緒的。
可重入(reentrant)函式可以由多於一個任務併發使用,而不必擔心資料錯誤。相反,不可重入(non-reentrant)函式不能由超過一個任務所共享,除非能確保函式的互斥(或者使用訊號量,或者在程式碼的關鍵部分禁用中斷)。可重入函式可以在任意時刻被中斷,稍後再繼續執行,不會丟失資料。可重入函式要麼使用區域性變數,要麼在使用全域性變數時保護自己的資料。可重入函式的基本要求:
- 不為連續的呼叫持有靜態資料;
- 不返回指向靜態資料的指標;
- 所有資料都由函式的呼叫者提供;
- 使用區域性變數,或者通過製作全域性資料的區域性變數拷貝來保護全域性資料;
- 使用靜態資料或全域性變數時做周密的並行時序分析,通過臨界區互斥避免臨界區衝突;
- 絕不呼叫任何不可重入函式。。
執行緒安全:如果你的程式碼所在的程序中有多個執行緒在同時執行,而這些執行緒可能會同時執行這段程式碼。如果次執行結果和單執行緒執行的結果是一樣的,而且其他的變數的值也和預期的是一樣的,就是執行緒安全的。執行緒安全問題都是由全域性變數及靜態變數引起的。若每個執行緒中對全域性變數、靜態變數只有讀操作,而無寫操作,一般來說,這個全域性變數是執行緒安全的;若有多個執行緒同時執行讀寫操作,一般都需要考慮執行緒同步,否則就可能影響執行緒安全。
2.函式的可重入性與執行緒安全之間的關係
可重入的函式不一定是執行緒安全的,可能是執行緒安全的也可能不是執行緒安全的;可重入的函式在多個執行緒中併發使用時是執行緒安全的,但不同的可重入函式(共享全域性變數及靜態變數)在多個執行緒中併發使用時會有執行緒安全問題;不可重入的函式一定不是執行緒安全的。
3.Menu程式中的執行緒安全
在linktable.c檔案中新增pthread.h標頭檔案,並且修改LinkTable結構體,新增 pthread_mutex_t mutex。之後就可以利用mutex變數實現程序安全:
struct LinkTable
{
tLinkTableNode *pHead;
tLinkTableNode *pTail;
int SumOfNode;
pthread_mutex_t mutex;
};
之後修改linktable.c檔案,不需要考慮讀操作,只需要考慮寫操作即可。在寫操作的部分加鎖,程式碼內容如下,在加鎖和解鎖的部分加以註釋,比如下面的程式碼:
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;
}
這樣就實現了執行緒安全。
總結
最後感謝孟寧老師帶我瞭解軟體工程方法,學習到了很多程式碼編寫的習慣,以及專案結構搭建過程中需要注意的點,包括程式碼的風格規範、模組化設計思想(高內聚,低耦合)、可重用介面的實現、使用回撥函式和注意執行緒安全等等。
參考資料: