1. 前言
嵌入式是軟件設(shè)計(jì)領(lǐng)域的一個(gè)分支,它自身的諸多特點(diǎn)決定了系統(tǒng)架構(gòu)師的選擇,同時(shí)它的一些問(wèn)題又具有相當(dāng)?shù)耐ㄓ眯?,可以推廣到其他的領(lǐng)域。
提起嵌入式軟件設(shè)計(jì),傳統(tǒng)的印象是單片機(jī),匯編,高度依賴硬件。傳統(tǒng)的嵌入式軟件開(kāi)發(fā)者往往只關(guān)注實(shí)現(xiàn)功能本身,而忽視諸如代碼復(fù)用,數(shù)據(jù)和界面分離,可測(cè)試性等因素。從而導(dǎo)致嵌入式軟件的質(zhì)量高度依賴開(kāi)發(fā)者的水平,成敗系之一身。
隨著嵌入式軟硬件的飛速發(fā)展,今天的嵌入式系統(tǒng)在功能,規(guī)模和復(fù)雜度各方面都有了極大的提升。比如,Marvell公司的PXA3xx系列的最高主頻已經(jīng)達(dá)到800Mhz,內(nèi)建USB,WIFI,2D圖形加速,32位DDR內(nèi)存。在硬件上,今天的嵌入式系統(tǒng)已經(jīng)達(dá)到甚至超過(guò)了數(shù)年前的PC平臺(tái)。在軟件方面,完善的操作系統(tǒng)已經(jīng)成熟,比如Symbian, Linux, WinCE。
基于完善的操作系統(tǒng),諸如字處理,圖像,視頻,音頻,游戲,網(wǎng)頁(yè)瀏覽等各種應(yīng)用程序?qū)映霾桓F,其功能性和復(fù)雜度比諸PC軟件不遑多讓。原來(lái)多選用專用硬件和專用系統(tǒng)的一些商業(yè)設(shè)備公司也開(kāi)始轉(zhuǎn)換思路,以出色而廉價(jià)的硬件和完善的操作系統(tǒng)為基礎(chǔ),用軟件的方式代替以前使用專有硬件實(shí)現(xiàn)的功能,從而實(shí)現(xiàn)更低的成本和更高的可變更,可維護(hù)性。
2.決定架構(gòu)的因素和架構(gòu)的影響
架構(gòu)不是一個(gè)孤立的技術(shù)的產(chǎn)物,它受多方面因素的影響。同時(shí),一個(gè)架構(gòu)又對(duì)軟件開(kāi)發(fā)的諸多方面造成影響。
下面舉一個(gè)具體的例子。
摩托車的發(fā)動(dòng)機(jī)在出廠前必須通過(guò)一系列的測(cè)試。在流水線上,發(fā)動(dòng)機(jī)被送到每個(gè)工位上,由工人進(jìn)行諸如轉(zhuǎn)速,噪音,振動(dòng)等方面的測(cè)試。要求實(shí)現(xiàn)一個(gè)嵌入式設(shè)備,具備以下基本功能:
-
安裝在工位上,工人上班前開(kāi)啟并登錄。
-
通過(guò)傳感器自動(dòng)采集測(cè)試數(shù)據(jù),并顯示在屏幕上。
-
記錄所有的測(cè)試結(jié)果,并提供統(tǒng)計(jì)功能。比如次品率。
如果你是這個(gè)設(shè)備的架構(gòu)師,哪些問(wèn)題是在設(shè)計(jì)架構(gòu)的時(shí)候應(yīng)該關(guān)注的呢?
2.1. 常見(jiàn)的誤解
2.1.1. 小型的系統(tǒng)不需要架構(gòu)
有相當(dāng)多的嵌入式系統(tǒng)規(guī)模都較小,一般是為了某些特定的目的而設(shè)計(jì)的。受工程師認(rèn)識(shí),客戶規(guī)模和項(xiàng)目進(jìn)度的影響,經(jīng)常不做任何架構(gòu)設(shè)計(jì),直接以實(shí)現(xiàn)功能為目標(biāo)進(jìn)行編碼。這種行為表面上看滿足了進(jìn)度、成本、功能各方面的需求。但是從長(zhǎng)遠(yuǎn)來(lái)看,在擴(kuò)展和維護(hù)上付出的成本,要遠(yuǎn)遠(yuǎn)高于最初節(jié)約的成本。如果系統(tǒng)的最初開(kāi)發(fā)者繼續(xù)留在組織內(nèi)并負(fù)責(zé)這個(gè)項(xiàng)目,那么可能一切都會(huì)正常,一旦他離開(kāi),后續(xù)者因?yàn)閷?duì)系統(tǒng)細(xì)節(jié)的理解不足,就可能引入更多的錯(cuò)誤。
要注意,嵌入式系統(tǒng)的變更成本要遠(yuǎn)遠(yuǎn)高于一般的軟件系統(tǒng)。好的軟件架構(gòu),可以從宏觀和微觀的不同層次上描述系統(tǒng),并將各個(gè)部分隔離,從而使新特性的添加和后續(xù)維護(hù)變得相對(duì)簡(jiǎn)單。
舉一個(gè)城鐵刷卡機(jī)的例子,這個(gè)例子在前面的課程中出現(xiàn)過(guò)。簡(jiǎn)單的城鐵刷卡機(jī)只需要實(shí)現(xiàn)如下功能:
一個(gè)While循環(huán)足以實(shí)現(xiàn)這個(gè)系統(tǒng),直接就可以開(kāi)始編碼調(diào)試。但是從一個(gè)架構(gòu)師的角度,這里有沒(méi)有值得抽象和剝離的部分呢?
-
計(jì)費(fèi)系統(tǒng)。計(jì)費(fèi)系統(tǒng)是必須抽象的,比如從單次計(jì)費(fèi)到按里程計(jì)費(fèi)。
-
傳感器系統(tǒng)。傳感器包括磁卡感應(yīng)器,投幣器等。設(shè)備可能更換。
-
故障處理和恢復(fù)。考慮到較高的可靠性和較短的故障恢復(fù)時(shí)間,這部分有必要單獨(dú)設(shè)計(jì)。
未來(lái)很可能出現(xiàn)的需求變更:
-
操作界面。是否需要抽象出專門(mén)的Model來(lái)?以備將來(lái)實(shí)現(xiàn)View。
-
數(shù)據(jù)統(tǒng)計(jì)。是否需要引入關(guān)系型數(shù)據(jù)庫(kù)?
如果直接以上面的流程圖編碼,當(dāng)出現(xiàn)變更后,有多少代碼可以復(fù)用?
不過(guò),也不要因此產(chǎn)生過(guò)度的設(shè)計(jì)。架構(gòu)應(yīng)當(dāng)立足滿足當(dāng)前需求,并適當(dāng)?shù)目紤]重用和變更。
2.1.2. 敏捷開(kāi)發(fā)不需要架構(gòu)
極限編程,敏捷開(kāi)發(fā)的出現(xiàn)使一些人誤以為軟件開(kāi)發(fā)無(wú)需再做架構(gòu)了。這是一個(gè)很大的誤解。敏捷開(kāi)發(fā)是在傳統(tǒng)瀑布式開(kāi)發(fā)流程出現(xiàn)明顯弊端后提出的解決方案,所以它必然有一個(gè)更高的起點(diǎn)和對(duì)開(kāi)發(fā)更嚴(yán)格的要求。而不是倒退到石器時(shí)代。
事實(shí)上,架構(gòu)是敏捷開(kāi)發(fā)的一部分,只不過(guò)在形式上,敏捷開(kāi)發(fā)推薦使用更高效,簡(jiǎn)單的方式來(lái)做設(shè)計(jì)。比如畫(huà)在白板上然后用數(shù)碼相機(jī)拍下的UML圖;用用戶故事代替用戶用例等。測(cè)試驅(qū)動(dòng)的敏捷開(kāi)發(fā)更是強(qiáng)迫工程師在寫(xiě)實(shí)際代碼前設(shè)計(jì)好組件的功能和接口,而不是直接開(kāi)始寫(xiě)代碼。敏捷開(kāi)發(fā)的一些特征:
-
針對(duì)比傳統(tǒng)開(kāi)發(fā)流程更大的系統(tǒng)
-
承認(rèn)變化,迭代架構(gòu)
-
簡(jiǎn)潔而不混亂
-
強(qiáng)調(diào)測(cè)試和重構(gòu)
2. 嵌入式環(huán)境下軟件設(shè)計(jì)的特點(diǎn)
要談嵌入式的軟件架構(gòu),首先必須了解嵌入式軟件設(shè)計(jì)的特點(diǎn)。
2.1. 和硬件密切相關(guān)
嵌入式軟件普遍對(duì)硬件有著相當(dāng)?shù)囊蕾囆?。這體現(xiàn)在幾個(gè)方面:
-
一些功能只能通過(guò)硬件實(shí)現(xiàn),軟件操作硬件,驅(qū)動(dòng)硬件。
-
硬件的差異/變更會(huì)對(duì)軟件產(chǎn)生重大影響。
-
沒(méi)有硬件或者硬件不完善時(shí),軟件無(wú)法運(yùn)行或無(wú)法完整運(yùn)行。
這些特點(diǎn)導(dǎo)致幾方面的后果:
-
軟件工程師對(duì)硬件的理解和熟練程度會(huì)很大程度的決定軟件的性能/穩(wěn)定性等非功能性指標(biāo),而這部分一向是相對(duì)復(fù)雜的,需要資深的工程師才能保證質(zhì)量。
-
軟件對(duì)硬件設(shè)計(jì)高度依賴,不能保持相對(duì)穩(wěn)定,可維護(hù)性和可重用性差
-
軟件不能離開(kāi)硬件單獨(dú)測(cè)試和驗(yàn)證,往往需要和硬件驗(yàn)證同步進(jìn)行,造成進(jìn)度前松后緊,錯(cuò)誤定位范圍擴(kuò)大。
針對(duì)這些問(wèn)題,有幾方面的解決思路:
-
用軟件實(shí)現(xiàn)硬件功能。選用更強(qiáng)大的處理器,用軟件來(lái)實(shí)現(xiàn)部分硬件功能,不僅可以降低對(duì)硬件的依賴,在響應(yīng)變化,避免對(duì)特定型號(hào)和廠商的依賴方面都很有好處。這在一些行業(yè)里已經(jīng)成為了趨勢(shì)。在PC平臺(tái)也經(jīng)歷了這樣的過(guò)程,比如早期的漢卡。
-
將對(duì)硬件的依賴獨(dú)立成硬件抽象層,盡可能使軟件的其他部分硬件無(wú)關(guān),并可以脫離硬件運(yùn)行。一方面將硬件變更甚至換件的風(fēng)險(xiǎn)控制在有限的范圍內(nèi),另一方面提高軟件部分的可測(cè)試性。
2.2. 穩(wěn)定性要求高
大部分嵌入式軟件都對(duì)程序的長(zhǎng)期穩(wěn)定運(yùn)行有較高的要求。比如手機(jī)經(jīng)常幾個(gè)月開(kāi)機(jī),通訊設(shè)備則要求24*7正常運(yùn)行,即使是通訊上的測(cè)試設(shè)備也要求至少正常運(yùn)行8小時(shí)。為了穩(wěn)定性的目標(biāo),有一些比較常用的設(shè)計(jì)手段:
-
將不同的任務(wù)分布在獨(dú)立的進(jìn)程中。良好的模塊化設(shè)計(jì)是關(guān)鍵
-
Watch Dog, Heart beat,重新啟動(dòng)失效的進(jìn)程。
-
完善而統(tǒng)一的日志系統(tǒng)以快速定位問(wèn)題。嵌入式設(shè)備一般缺乏有力的調(diào)試器,日志系統(tǒng)尤其重要。
-
將錯(cuò)誤孤立在最小的范圍內(nèi),避免錯(cuò)誤的擴(kuò)散和連鎖反應(yīng)。核心代碼要經(jīng)過(guò)充分的驗(yàn)證,對(duì)非核心代碼,可以在監(jiān)控或者沙盒中運(yùn)行,避免其破壞整個(gè)系統(tǒng)。
舉例,Symbian上的GPRS訪問(wèn)受不同硬件和操作系統(tǒng)版本影響,功能不是非常穩(wěn)定。其中有一個(gè)版本上當(dāng)關(guān)閉GPRS連接時(shí)一定會(huì)崩潰,而且屬于known issue。將GPRS連接,HTTP協(xié)議處理,文件下載等操作獨(dú)立到一個(gè)進(jìn)程中,雖然每次操作完畢該進(jìn)程都會(huì)崩潰,對(duì)用戶卻沒(méi)有影響。
- 雙備份這樣的手段較少采用
2.3. 內(nèi)存不足
雖然當(dāng)今的嵌入式系統(tǒng)的內(nèi)存比之以K計(jì)數(shù)的時(shí)代已經(jīng)有了很大的提高,但是隨著軟件規(guī)模的增長(zhǎng),內(nèi)存不足的問(wèn)題依然時(shí)時(shí)困擾著系統(tǒng)架構(gòu)師。有一些原則,架構(gòu)師在進(jìn)行設(shè)計(jì)決策的時(shí)候可以參考:
2.3.1. 虛擬內(nèi)存技術(shù)
有一些嵌入式設(shè)備需要處理巨大的數(shù)據(jù)量,而這些數(shù)據(jù)不可能全部裝入內(nèi)存中。一些嵌入式操作系統(tǒng)不提供虛擬內(nèi)存技術(shù),比如WinCE4.2每個(gè)程序最多只能使用32M內(nèi)存。對(duì)這樣的應(yīng)用,架構(gòu)師應(yīng)該特別設(shè)計(jì)自己的虛擬內(nèi)存技術(shù)。所謂的虛擬內(nèi)存技術(shù)的核心是,將暫時(shí)不太可能使用的數(shù)據(jù)移出內(nèi)存。這涉及到一些技術(shù)點(diǎn):
-
引用計(jì)數(shù),正在使用的數(shù)據(jù)不能移出。
-
使用預(yù)測(cè),預(yù)測(cè)下一個(gè)階段某個(gè)數(shù)據(jù)的使用可能性。基于預(yù)測(cè)移出數(shù)據(jù)或者提前裝入數(shù)據(jù)。
-
占位數(shù)據(jù)/對(duì)象。
-
高速緩存。在復(fù)雜數(shù)據(jù)結(jié)果下緩存高頻率使用的數(shù)據(jù),直接訪問(wèn)。
-
快速的持久化和裝載。
下圖是一個(gè)全國(guó)電信機(jī)房管理系統(tǒng)的界面示意圖:
每個(gè)節(jié)點(diǎn)下都有大量的數(shù)據(jù)需要裝載,可以使用上述技術(shù)將內(nèi)存占用降到最低。
2.3.2. 兩段式構(gòu)造
在內(nèi)存有限的系統(tǒng)里,對(duì)象構(gòu)造失敗是必須要處理的問(wèn)題,失敗的原因中最常見(jiàn)的則是內(nèi)存不足(實(shí)際上這也是對(duì)PC平臺(tái)的要求,但是在實(shí)際中往往忽略,因?yàn)閮?nèi)存實(shí)在便宜)。兩段式構(gòu)造就是一種常用而有效的設(shè)計(jì)。舉例來(lái)說(shuō):
CMySimpleClass:
class CMySimpleClass
{
public:
CMySimpleClass();
~CMySimpleClass();
...
private:
int SomeData;
};
CMyCompoundClass:
class CMyCompoundClass
{
public:
CMyCompoundClass();
~CMyCompoundClass();
...
private:
CMySimpleClass* iSimpleClass;
};
在CMyCompoundClass的構(gòu)造函數(shù)里初始化iSimpleClass對(duì)象。
CMyCompoundClass::CMyCompoundClass()
{
iSimpleClass = new CMySimpleClass;
}
當(dāng)創(chuàng)建CMyCompoundClass的時(shí)候會(huì)發(fā)生什么呢?
CMyCompoundClass* myCompoundClass = new CMyCompoundClass;
-
為CMyCompoundClass的對(duì)象分配內(nèi)存
-
調(diào)用CMyCompoundClass對(duì)象的構(gòu)造函數(shù)
-
在構(gòu)造函數(shù)中創(chuàng)建一個(gè)CMySimpleClass的實(shí)例
-
構(gòu)造函數(shù)結(jié)束返回
一切看起來(lái)都很簡(jiǎn)單,但是如果第三步創(chuàng)建CMySimpleClass對(duì)象的時(shí)候發(fā)生內(nèi)存不足的錯(cuò)誤怎么辦呢?構(gòu)造函數(shù)無(wú)法返回任何錯(cuò)誤信息以提示調(diào)用者構(gòu)造沒(méi)有成功。調(diào)用者于是獲得了一個(gè)指向CMyCompoundClass的指針,但是這個(gè)對(duì)象并沒(méi)有構(gòu)造完整。
如果在構(gòu)造函數(shù)中拋出異常會(huì)怎么樣呢?這是個(gè)著名的噩夢(mèng),因?yàn)槲鰳?gòu)函數(shù)不會(huì)被調(diào)用,在創(chuàng)建CMySimpleClass對(duì)象之前如果分配了資源就會(huì)泄露。關(guān)于在構(gòu)造函數(shù)中拋出異??梢詥沃v一個(gè)小時(shí),但是有一個(gè)建議是:盡量避免在構(gòu)造函數(shù)中拋出異常。
所以,使用兩段式構(gòu)造法是一個(gè)更好的選擇。簡(jiǎn)單的說(shuō),就是在構(gòu)造函數(shù)避免任何可能產(chǎn)生錯(cuò)誤的動(dòng)作,比如分配內(nèi)存,而把這些動(dòng)作放在構(gòu)造完成之后,調(diào)用另一個(gè)函數(shù)。比如:
AddressBook* book = new AddressBook()
If(!book->Construct())
{
delete book;
book = NULL;
}
這樣可以保證當(dāng)Construct不成功的時(shí)候釋放已經(jīng)分配的資源。
在最重要的手機(jī)操作系統(tǒng)Symbian上,二段式構(gòu)造法普遍使用。
2.3.3. 內(nèi)存分配器
不同的系統(tǒng)有著不同的內(nèi)存分配的特點(diǎn)。有些要求分配很多小內(nèi)存,有的則需要經(jīng)常增長(zhǎng)已經(jīng)分配的內(nèi)存。一個(gè)好的內(nèi)存分配器對(duì)嵌入式的軟件的性能有時(shí)具有重大的意義。應(yīng)該在系統(tǒng)設(shè)計(jì)時(shí)保證整個(gè)系統(tǒng)使用統(tǒng)一的內(nèi)存分配器,并且可以隨時(shí)更換。
2.3.4. 內(nèi)存泄漏
內(nèi)存泄漏對(duì)嵌入式系統(tǒng)有限的內(nèi)存是非常嚴(yán)重的。通過(guò)使用自己的內(nèi)存分配器,可以很容易的跟蹤內(nèi)存的分配釋放情況,從而檢測(cè)出內(nèi)存泄漏的情況。
2.4. 處理器能力有限,性能要求高
這里不討論實(shí)時(shí)系統(tǒng),那是一塊很大的專業(yè)話題。對(duì)一般的嵌入式系統(tǒng)而言,由于處理器能力有限,要特別注意性能的問(wèn)題。一些很好的架構(gòu)設(shè)計(jì)由于不能滿足性能要求,最終導(dǎo)致整個(gè)項(xiàng)目的失敗。
2.4.1. 抵御新技術(shù)的誘惑
架構(gòu)師必須明白,新技術(shù)常常意味著復(fù)雜和更低的性能。即使這不是絕對(duì)的,由于嵌入式系統(tǒng)硬件性能所限,彈性較低。一旦發(fā)現(xiàn)新技術(shù)有和當(dāng)初設(shè)想不同之處,就更難通過(guò)修改來(lái)適應(yīng)。比如GWT技術(shù)。這是Google推出的Ajax開(kāi)發(fā)工具,它可以讓程序員像開(kāi)發(fā)一個(gè)桌面應(yīng)用程序一樣開(kāi)發(fā)Web的Ajax程序。這使得在嵌入式系統(tǒng)上用一套代碼實(shí)現(xiàn)遠(yuǎn)程和本地操作界面成為了很容易的一件事。
但是,在嵌入式設(shè)備上運(yùn)行B-S結(jié)構(gòu)的應(yīng)用,性能上是一個(gè)很大的挑戰(zhàn)。同時(shí),瀏覽器兼容方面的問(wèn)題也很嚴(yán)重,GWT目前的版本還不夠完善。
事實(shí)證明,嵌入式的遠(yuǎn)程控制方案還是要采用Activex,VNC或者其他的方案。
2.4.2. 不要有太多的層次
分層結(jié)構(gòu)有利于清晰的劃分系統(tǒng)職責(zé),實(shí)現(xiàn)系統(tǒng)的解耦,但是每多一個(gè)層次,就意味著性能的一次損失。尤其是當(dāng)層和層之間需要傳遞大量數(shù)據(jù)的時(shí)候。對(duì)嵌入式系統(tǒng)而言,在采用分層結(jié)構(gòu)時(shí)要控制層次數(shù)量,并且盡量不要傳遞大量數(shù)據(jù),尤其是在不同進(jìn)程的層次之間。如果一定要傳遞數(shù)據(jù),要避免大量的數(shù)據(jù)格式轉(zhuǎn)換,如XML到二進(jìn)制,C++結(jié)構(gòu)到Python結(jié)構(gòu)。
嵌入式系統(tǒng)能力有限,一定要將有限的能力用在系統(tǒng)的核心功能上。
2.5. 存儲(chǔ)設(shè)備易損壞,速度較慢
受體積和成本的限制,大部分的嵌入式設(shè)備使用諸如Compact Flash, SD, mini SD, MMC等作為存儲(chǔ)設(shè)備。這些設(shè)備雖然有著不擔(dān)心機(jī)械運(yùn)動(dòng)損壞的優(yōu)點(diǎn),但是其本身的使用壽命都比較短暫。比如,CF卡一般只能寫(xiě)100萬(wàn)次。而SD更短,只有10萬(wàn)次。對(duì)于像數(shù)碼相機(jī)這樣的應(yīng)用,也許是足夠的。但對(duì)于需要頻繁擦寫(xiě)磁盤(pán)的應(yīng)用,比如歷史數(shù)據(jù)庫(kù),磁盤(pán)的損壞問(wèn)題會(huì)很快顯現(xiàn)。比如有一個(gè)應(yīng)用式每天向CF卡上寫(xiě)一個(gè)16M的文件,文件系統(tǒng)是FAT16, 每簇大小是2K,那么寫(xiě)完這個(gè)16M的文件,分區(qū)表需要寫(xiě)8192次,于是一個(gè)100萬(wàn)次壽命的CF實(shí)際能夠工作的時(shí)間是1000000/8192 = 122天。而損壞的時(shí)候,CF卡的其他絕大部分地方的使用次數(shù)不過(guò)萬(wàn)分之一。
除了因?yàn)殪o態(tài)的文件分區(qū)表等區(qū)塊被頻繁的讀寫(xiě)而提前損壞,一些嵌入式設(shè)備還要面對(duì)直接斷電的挑戰(zhàn),這會(huì)在存儲(chǔ)設(shè)備上產(chǎn)生不完整的數(shù)據(jù)。
2.5.1. 損耗均衡
損耗均衡的基本思路是平均地使用存儲(chǔ)器上的各個(gè)區(qū)塊。需要維護(hù)一張存儲(chǔ)器區(qū)塊使用情況的表,這個(gè)表包括區(qū)塊的偏移位置,當(dāng)前是否可用,以及已經(jīng)擦寫(xiě)地次數(shù)。當(dāng)有新的擦寫(xiě)請(qǐng)求的時(shí)候,根據(jù)以下原則選擇區(qū)塊:
-
盡量連續(xù)
-
擦寫(xiě)次數(shù)最少
即使是更新已經(jīng)存在的數(shù)據(jù),也會(huì)使用以上原則分配新的區(qū)塊。同樣,這張表的存放位置也不能是固定不變的,否則這張表所占據(jù)的區(qū)塊就會(huì)最先損壞。當(dāng)要更新這張表的時(shí)候,同樣要使用以上算法分配區(qū)塊。
如果存儲(chǔ)器上有大量的靜態(tài)數(shù)據(jù),那么上述算法就只能針對(duì)剩下的空間生效,這種情況下還要實(shí)現(xiàn)對(duì)這些靜態(tài)數(shù)據(jù)的搬運(yùn)的算法。但是這種算法會(huì)降低寫(xiě)操作的性能,也增加了算法的復(fù)雜度。一般都只使用動(dòng)態(tài)均衡算法。
目前比較成熟的損耗均衡的文件系統(tǒng)有JFFS2, 和 YAFFS。也有另一種思路就是在FAT16等傳統(tǒng)文件系統(tǒng)上實(shí)現(xiàn)損耗均衡,只要事先分配一塊足夠大的文件,在文件內(nèi)部實(shí)現(xiàn)損耗均衡算法。不過(guò)必須修改FAT16的代碼,關(guān)閉對(duì)最后修改時(shí)間的更新。
現(xiàn)在的CF卡和SD卡有的已經(jīng)在內(nèi)部實(shí)現(xiàn)了損耗均衡,這種情況下就不需要軟件實(shí)現(xiàn)了。
2.5.2. 錯(cuò)誤恢復(fù)
如果在向存儲(chǔ)器寫(xiě)數(shù)據(jù)的時(shí)候發(fā)生斷電或者被拔出,那么所寫(xiě)的區(qū)域的數(shù)據(jù)就處于未知的狀態(tài)。在一些應(yīng)用中,這會(huì)導(dǎo)致不完整的文件,而在另一些應(yīng)用中,則會(huì)導(dǎo)致系統(tǒng)失敗。所以對(duì)這類錯(cuò)誤的恢復(fù)也是嵌入式軟件設(shè)計(jì)必須考慮的。常用的思路有兩種:
- 日志型的文件系統(tǒng)
這種文件系統(tǒng)并不是直接存儲(chǔ)數(shù)據(jù),而是一條條的日志,所以當(dāng)發(fā)生斷電的時(shí)候,總可以恢復(fù)到之前的狀態(tài)。這類文件系統(tǒng)的代表如ext3。
- 雙備份
雙備份的思路更簡(jiǎn)單,所有的數(shù)據(jù)都寫(xiě)兩份。每次交替使用。文件分區(qū)表也必須是雙備份的。假設(shè)有數(shù)據(jù)塊A,A1是他的備份塊,在初始時(shí)刻和A的內(nèi)容是一致的。在分區(qū)表中,F(xiàn)指向數(shù)據(jù)塊A,F(xiàn)1是他的備份塊。當(dāng)修改文件時(shí),首先修改數(shù)據(jù)塊A1的內(nèi)容,如果此時(shí)斷電,A1的內(nèi)容錯(cuò)誤,但因?yàn)镕指向的是完好的A,所以數(shù)據(jù)沒(méi)有損壞。如果A1修改成功,則修改F1的內(nèi)容,如果此時(shí)斷電,因?yàn)镕是完好的,所以依然沒(méi)有問(wèn)題。
現(xiàn)在的Flash設(shè)備,有的已經(jīng)內(nèi)置錯(cuò)誤檢測(cè)和錯(cuò)誤校正技術(shù),可以保證在斷電時(shí)數(shù)據(jù)的完整。還有的包括自動(dòng)的動(dòng)態(tài)/靜態(tài)損耗均衡算法和壞塊處理,完全無(wú)須上層軟件額外對(duì)待,可以當(dāng)作硬盤(pán)使用。所以,硬件越發(fā)達(dá),軟件就會(huì)越可靠,技術(shù)不斷的進(jìn)步,將讓我們可以把更多的精力投入到軟件功能的本身,這是發(fā)展的趨勢(shì)。
2.6. 故障成本高昂
嵌入式產(chǎn)品都是軟硬件一起銷售的給用戶的,所以這帶來(lái)了一個(gè)純軟件所不具備的問(wèn)題,那就是當(dāng)產(chǎn)品發(fā)生故障時(shí),如果需要返廠才能修復(fù),則成本就很高。嵌入式設(shè)備常見(jiàn)有以下的幾類故障:
a) 數(shù)據(jù)故障。由于某些原因?qū)е聰?shù)據(jù)不能讀出或者不一致。比如斷電引起的數(shù)據(jù)庫(kù)錯(cuò)誤。
b) 軟件故障。軟件本身的缺陷,需要通過(guò)發(fā)布補(bǔ)丁程序或者新版本的軟件修正。
c) 系統(tǒng)故障。比如用戶下載了錯(cuò)誤的系統(tǒng)內(nèi)核,導(dǎo)致系統(tǒng)無(wú)法啟動(dòng)。
d) 硬件故障。這種故障只有返廠,不屬于我們的討論范圍。
針對(duì)前三類故障,要盡可能保證客戶自己,或者現(xiàn)場(chǎng)技術(shù)人員就可以解決。從架構(gòu)的角度考慮,如下原則可以參考:
a) 使用具備錯(cuò)誤恢復(fù)能力的數(shù)據(jù)管理設(shè)計(jì)。當(dāng)數(shù)據(jù)發(fā)生錯(cuò)誤時(shí),用戶可以接受的處理依次是:
i. 錯(cuò)誤被糾正,所有數(shù)據(jù)有效
ii. 錯(cuò)誤發(fā)生時(shí)的數(shù)據(jù)(可能不完整)丟失,之前的數(shù)據(jù)有效。
iii. 所有數(shù)據(jù)丟失
iv. 數(shù)據(jù)引擎崩潰無(wú)法繼續(xù)工作
一般而言,滿足第二個(gè)條件即可。(日志,事務(wù),備份,錯(cuò)誤識(shí)別)
b) 將應(yīng)用程序和系統(tǒng)分離。應(yīng)用程序應(yīng)該放置在可插拔的Flash卡上,可以通過(guò)讀卡器進(jìn)行文件復(fù)制升級(jí)。非必要的情況不要使用專用應(yīng)用軟件來(lái)升級(jí)應(yīng)用程序。
c) 要有“安全模式”。即當(dāng)主系統(tǒng)被損壞后,設(shè)備依然可以啟動(dòng),重新升級(jí)系統(tǒng)。常見(jiàn)的uboot可以保證這一點(diǎn),在系統(tǒng)損壞后,可以進(jìn)入uboot通過(guò)tftp重新升級(jí)。
3. 軟件框架
在桌面系統(tǒng)和網(wǎng)絡(luò)系統(tǒng)上,框架是普遍應(yīng)用的,比如著名的ACE, MFC, Ruby On Rails等。而在嵌入式系統(tǒng)中,框架則是很少使用的。究其原因,大概是認(rèn)為嵌入式系統(tǒng)簡(jiǎn)單,沒(méi)有重復(fù)性,過(guò)于注重功能的實(shí)現(xiàn)和性能的優(yōu)化。在前言中我們已經(jīng)提到,現(xiàn)在的嵌入式發(fā)展趨勢(shì)是向著復(fù)雜化,大型化,系列化發(fā)展的。所以,在嵌入式下設(shè)計(jì)軟件框架也是很有必要,也很有價(jià)值的。
3.1. 嵌入式軟件架構(gòu)面臨的問(wèn)題
前面我們講到,嵌入式系統(tǒng)軟件架構(gòu)所面臨的一些問(wèn)題,其中很重要的一點(diǎn)是,對(duì)硬件的依賴和硬件相關(guān)軟件的復(fù)雜性。還包括嵌入式軟件在穩(wěn)定性和內(nèi)存占用等方面的苛刻要求。如果團(tuán)隊(duì)中的每個(gè)人都是這些方面高手的話,也許有可能開(kāi)發(fā)出高質(zhì)量的軟件,但事實(shí)是一個(gè)團(tuán)隊(duì)中可能只有一兩個(gè)資深人員,其他大部分都是初級(jí)工程師。人人都去和硬件打交道,都負(fù)責(zé)穩(wěn)定性,性能等等指標(biāo)的話,是很難保證最終產(chǎn)品質(zhì)量的。如果組件團(tuán)隊(duì)時(shí)都是精通硬件等底層技術(shù)的人才,又很難設(shè)計(jì)出在可用性,擴(kuò)展性等方面出色的軟件。術(shù)業(yè)有專攻,架構(gòu)師的選擇決定著團(tuán)隊(duì)的組成方式。
同時(shí),嵌入式軟件開(kāi)發(fā)雖然復(fù)雜,但是也存在大量的重用的可能性。如何重用,又如何應(yīng)對(duì)將來(lái)的變更?
所以,如何將復(fù)雜性對(duì)大多數(shù)人屏蔽,如何將關(guān)注點(diǎn)分離,如何保證系統(tǒng)的關(guān)鍵非功能指標(biāo),是嵌入式軟件架構(gòu)設(shè)計(jì)師應(yīng)該解決的問(wèn)題。一種可能的解決方案就是軟件框架。
3.2. 什么是框架
框架是在一個(gè)給定的問(wèn)題領(lǐng)域內(nèi),為了重用和應(yīng)對(duì)未來(lái)需求變化而設(shè)計(jì)的軟件半成品??蚣軓?qiáng)調(diào)對(duì)特定領(lǐng)域的抽象,包含大量的專業(yè)領(lǐng)域知識(shí),以縮短軟件的開(kāi)發(fā)周期,提高軟件質(zhì)量為目的。使用框架的二次開(kāi)發(fā)者通過(guò)重寫(xiě)子類或組裝對(duì)象的方式來(lái)實(shí)現(xiàn)特殊的功能。
3.2.1. 軟件復(fù)用的層次
復(fù)用是在我們經(jīng)常談到的話題,“不要重復(fù)發(fā)明輪子”也是耳熟能詳?shù)慕錀l。不過(guò)對(duì)于復(fù)用的理解實(shí)際上是有很多個(gè)層次的。
最基礎(chǔ)的復(fù)用是復(fù)制粘貼。某個(gè)功能以前曾經(jīng)實(shí)現(xiàn)過(guò),再次需要的時(shí)候就復(fù)制過(guò)來(lái),修改一下就可以使用。經(jīng)驗(yàn)豐富的程序員一般都會(huì)有自己的程序庫(kù),這樣他們實(shí)現(xiàn)的時(shí)候就會(huì)比新的程序員快。復(fù)制粘貼的缺點(diǎn)是代碼沒(méi)有經(jīng)過(guò)抽象,往往并不完全的適用,所以需要進(jìn)行修改,經(jīng)過(guò)多次復(fù)用后,代碼將會(huì)變得混亂,難以理解。很多公司的產(chǎn)品都有這個(gè)問(wèn)題,一個(gè)產(chǎn)品的代碼從另一個(gè)產(chǎn)品復(fù)制而來(lái),修改一下就用,有時(shí)候甚至類名變量名都不改。按照“只有為復(fù)用設(shè)計(jì)的代碼才能真正復(fù)用”的標(biāo)準(zhǔn),這稱不上是復(fù)用,或者說(shuō)是低水平的復(fù)用。
更高級(jí)的復(fù)用是則是庫(kù)。這種功能需要對(duì)經(jīng)常使用的功能進(jìn)行抽象,提取出其中恒定不變的部分,以庫(kù)的形式提供給二次開(kāi)發(fā)程序員使用。因?yàn)樵O(shè)計(jì)庫(kù)的時(shí)候不知道二次開(kāi)發(fā)者會(huì)如何使用,所以對(duì)設(shè)計(jì)者有著很高的要求。這是使用最廣泛的一種復(fù)用,比如標(biāo)準(zhǔn)C庫(kù),STL庫(kù)。現(xiàn)在非常流行的Python語(yǔ)言的重要優(yōu)勢(shì)之一就是其庫(kù)支持非常廣泛,相反C++一直缺少一個(gè)強(qiáng)大統(tǒng)一的庫(kù)支持,成為短板。在公司內(nèi)部的開(kāi)發(fā)中總結(jié)常用功能并開(kāi)發(fā)成庫(kù)是很有價(jià)值的,缺點(diǎn)是對(duì)庫(kù)的升級(jí)會(huì)影響到很多的產(chǎn)品,必須慎之又慎。
框架是另一種復(fù)用。和庫(kù)一樣,框架也是對(duì)系統(tǒng)中不變的部分進(jìn)行抽象并加以實(shí)現(xiàn),由二次開(kāi)發(fā)者實(shí)現(xiàn)其他變化的部分。典型的框架和庫(kù)的最大的區(qū)別是,庫(kù)是靜態(tài)的,由二次開(kāi)發(fā)者調(diào)用的;框架是活著的,它是主控者,二次開(kāi)發(fā)者的代碼必須符合框架的設(shè)計(jì),由框架決定在何時(shí)調(diào)用。
舉個(gè)例子,一個(gè)網(wǎng)絡(luò)應(yīng)用總是要涉及到連接的建立,數(shù)據(jù)收發(fā)和連接的關(guān)閉。以庫(kù)的形式提供是這樣的:
conn = connect(host,port);
if(conn.isvalid())
{
data = conn.recv();
printf(data);
conn.close();
}
框架則是這樣的:
class mycomm:class connect
{
public:
host();
port();
onconnected();
ondataarrived(unsigned char* data, int len);
onclose();
};
框架會(huì)在“適當(dāng)”的時(shí)機(jī)創(chuàng)建mycomm對(duì)象,并查詢host和port,然后建立連接。在連接建立后,調(diào)用onconnected()接口,給二次開(kāi)發(fā)者提供進(jìn)行處理的機(jī)會(huì)。當(dāng)數(shù)據(jù)到達(dá)的時(shí)候調(diào)用ondataarrived接口讓二次開(kāi)發(fā)者處理。這是好萊塢原則,“不要來(lái)找我們,我們會(huì)去找你”。
當(dāng)然,一個(gè)完整的框架通常也要提供各種庫(kù)供二次開(kāi)發(fā)者使用。比如MFC提供了很多的庫(kù),如CString, 但本質(zhì)上它是一個(gè)框架。比如實(shí)現(xiàn)一個(gè)對(duì)話框的OnInitDialog接口,就是由框架規(guī)定的。
3.2.2. 針對(duì)高度特定領(lǐng)域的抽象
和庫(kù)比較起來(lái),框架是更針對(duì)特定領(lǐng)域的抽象。庫(kù),比如C庫(kù),是面向所有的應(yīng)用的。而框架相對(duì)來(lái)說(shuō)則要狹窄的多。比如MFC提供的框架只適合于Windows平臺(tái)的桌面應(yīng)用程序開(kāi)發(fā),ACE則是針對(duì)網(wǎng)絡(luò)應(yīng)用開(kāi)發(fā)的框架,Ruby On Rails是為快速開(kāi)發(fā)web站點(diǎn)設(shè)計(jì)的。
越是針對(duì)特定的領(lǐng)域,抽象就可以做的越強(qiáng),二次開(kāi)發(fā)就可以越簡(jiǎn)單,因?yàn)楣残缘臇|西越多。比如我們上面談到嵌入式系統(tǒng)軟件開(kāi)發(fā)的諸多特點(diǎn),這就是特定領(lǐng)域的共性,就屬于可以抽象的部分。具體到實(shí)際的嵌入式應(yīng)用,又會(huì)有更多的共性可以抽象。
框架的設(shè)計(jì)目的是總結(jié)特定領(lǐng)域的共性,以框架的方式實(shí)現(xiàn),并規(guī)定二次開(kāi)發(fā)者的實(shí)現(xiàn)方式,從而簡(jiǎn)化開(kāi)發(fā)。相應(yīng)的,針對(duì)一個(gè)領(lǐng)域開(kāi)發(fā)的框架就不能服務(wù)于另一個(gè)領(lǐng)域。對(duì)企業(yè)而言,框架是一種極好的積累知識(shí),降低成本的技術(shù)手段。
3.2.3. 解除耦合和應(yīng)對(duì)變化
框架設(shè)計(jì)的一個(gè)重要目的就是應(yīng)對(duì)變化。應(yīng)對(duì)變化的本質(zhì)就是解耦。從架構(gòu)師的角度看,解耦可以分為三種:
-
邏輯解耦。邏輯解耦是將邏輯上不同的模塊抽象并分離處理。如數(shù)據(jù)和界面的解耦。這也是我們最常做的解耦。
-
知識(shí)解耦。知識(shí)解耦是通過(guò)設(shè)計(jì)讓掌握不同知識(shí)的人僅僅通過(guò)接口工作。典型的如測(cè)試工程師所掌握的專業(yè)知識(shí)和開(kāi)發(fā)工程師所掌握的程序設(shè)計(jì)和實(shí)現(xiàn)的知識(shí)。傳統(tǒng)的測(cè)試腳本通常是將這二者合二為一的。所以要求測(cè)試工程師同時(shí)具備編程的能力。通過(guò)適當(dāng)?shù)姆绞?,可以讓測(cè)試工程師以最簡(jiǎn)單的方式實(shí)現(xiàn)他的測(cè)試用例,而開(kāi)發(fā)人員編寫(xiě)傳統(tǒng)的程序代碼來(lái)執(zhí)行這些用例。
-
變與不變的解耦。這是框架的重要特征。框架通過(guò)對(duì)領(lǐng)域知識(shí)的分析,將共性,也就是不變的內(nèi)容固定下來(lái),而將可能發(fā)生變化的部分交給二次開(kāi)發(fā)者實(shí)現(xiàn)。
3.2.4. 框架可以實(shí)現(xiàn)和規(guī)定非功能性需求
非功能性需求是指如性能,可靠性,可測(cè)試性,可移植性等。這些特性可以通過(guò)框架來(lái)實(shí)現(xiàn)。以下我們一一舉例。
性能。對(duì)性能的優(yōu)化最忌諱的就是普遍優(yōu)化。系統(tǒng)的性能往往取決于一些特定的點(diǎn)。比如在嵌入式系統(tǒng)中,對(duì)存儲(chǔ)設(shè)備的訪問(wèn)是比較慢的。如果開(kāi)發(fā)者不注意這方面的問(wèn)題,頻繁的讀寫(xiě)存儲(chǔ)設(shè)備,就會(huì)造成性能下降。如果對(duì)存儲(chǔ)設(shè)備的讀寫(xiě)由框架設(shè)計(jì),二次開(kāi)發(fā)者只作為數(shù)據(jù)的提供和處理者,那么就可以在框架中對(duì)讀寫(xiě)的頻率進(jìn)行調(diào)節(jié),從而達(dá)到優(yōu)化性能的目的。由于框架都是單獨(dú)開(kāi)發(fā)的,完成后供廣泛使用,所以就有條件對(duì)關(guān)鍵的性能點(diǎn)進(jìn)行充分的優(yōu)化。
可靠性。以上面的網(wǎng)絡(luò)通訊程序?yàn)槔捎诳蚣茇?fù)責(zé)了連接的創(chuàng)建和管理,也處理了各種可能的網(wǎng)絡(luò)錯(cuò)誤,具體的實(shí)現(xiàn)者無(wú)須了解這方面的知識(shí),也無(wú)須實(shí)現(xiàn)這方面錯(cuò)誤處理的代碼,就可以保證整個(gè)系統(tǒng)在網(wǎng)絡(luò)通訊方面的可靠性。以框架的方式設(shè)計(jì)在可靠性方面的最大優(yōu)勢(shì)就是:二次開(kāi)發(fā)的代碼是在框架的掌控之內(nèi)運(yùn)行的。一方面框架可以將容易出錯(cuò)的部分實(shí)現(xiàn),另一方面對(duì)二次開(kāi)發(fā)的代碼產(chǎn)生的錯(cuò)誤也可以捕獲和處理。而庫(kù)則不能代替使用者處理錯(cuò)誤。
可測(cè)試性??蓽y(cè)試性是軟件架構(gòu)需要考慮的一個(gè)重要方面。下面的章節(jié)會(huì)講到,軟件的可測(cè)試性是由優(yōu)良的設(shè)計(jì)來(lái)保證的。一方面,由于框架規(guī)定了二次開(kāi)發(fā)的接口,所以可以迫使二次開(kāi)發(fā)者開(kāi)發(fā)出便于進(jìn)行單元測(cè)試的代碼。另一方面,框架也可以在系統(tǒng)測(cè)試的層面上提供易于實(shí)現(xiàn)自動(dòng)化測(cè)試和回歸測(cè)試的設(shè)計(jì),例如統(tǒng)一提供的TL1接口。
可移植性。如果軟件的可移植性是軟件設(shè)計(jì)的目標(biāo),框架設(shè)計(jì)者可以在設(shè)計(jì)階段來(lái)保證這一點(diǎn)。一種方式是通過(guò)跨平臺(tái)的庫(kù)來(lái)屏蔽系統(tǒng)差異,另一種可能的方式更加極端,基于框架的二次開(kāi)發(fā)可以是腳本化的。組態(tài)軟件是這方面的一個(gè)例子,在PC上組態(tài)的工程,也可以在嵌入式設(shè)備上運(yùn)行。
3.3. 一個(gè)框架設(shè)計(jì)的實(shí)例
3.3.1. 基本架構(gòu)
3.3.2. 功能特點(diǎn)
上面是一個(gè)產(chǎn)品系列的架構(gòu)圖,其特點(diǎn)是硬件部分是模塊化的,可以隨時(shí)插拔。不同的硬件應(yīng)用于不同的通訊測(cè)試場(chǎng)合。比如光通訊測(cè)試,xDSL測(cè)試,Cable Modem測(cè)試等等。針對(duì)不同的硬件,需要開(kāi)發(fā)不同的固件和軟件。固件層的功能主要是通過(guò)USB接口接收來(lái)自軟件的指令,并讀寫(xiě)相應(yīng)的硬件接口,再進(jìn)行一些計(jì)算后,將結(jié)果返回給軟件。軟件運(yùn)行在WinCE平臺(tái),除了提供一個(gè)觸摸式的圖形化界面外,還對(duì)外提供基于XML(SOAP)接口和TL1接口。為了實(shí)現(xiàn)自動(dòng)化測(cè)試,還提供了基于Lua的腳本語(yǔ)言接口。整個(gè)產(chǎn)品系列有幾十個(gè)不同的硬件模塊,相應(yīng)的需要開(kāi)發(fā)幾十套軟件。這些軟件雖然服務(wù)于不同的硬件,但是彼此之間有著高度的相似性。所以,選擇先開(kāi)發(fā)一個(gè)框架,再基于框架開(kāi)發(fā)具體的模塊軟件成了最優(yōu)的選擇。
###3.3.3. 分析
軟件部分的結(jié)構(gòu)分析如下:
系統(tǒng)分為軟件,固件和硬件三大塊。軟件和固件運(yùn)行在兩塊獨(dú)立的板子上,有各自的處理器和操作系統(tǒng)。硬件則插在固件所在的板子上,是可以替換的。
軟件和固件其實(shí)都是軟件,下面我們分別分析。
軟件
軟件的主要工作是提供各種用戶界面。包括本地圖形化界面,SOAP訪問(wèn)界面,TL1訪問(wèn)界面。
整個(gè)軟件部分分為五大部分:
- 通訊層
- 協(xié)議層
- 圖形界面
- SOAP服務(wù)器
- TL1服務(wù)器
通訊層要屏蔽用戶對(duì)具體通信介質(zhì)和協(xié)議的了解,無(wú)論是USB還是socket,對(duì)上層都不產(chǎn)生影響。通訊層負(fù)責(zé)提供可靠的通訊服務(wù)和適當(dāng)?shù)腻e(cuò)誤處理。通過(guò)配置文件,用戶可以改變所使用的通訊層。
協(xié)議層的目的是將數(shù)據(jù)進(jìn)行編碼和解碼。編碼的產(chǎn)生物是可以在通訊層發(fā)送的流,按照嵌入式軟件的特點(diǎn),我們選擇二進(jìn)制作為流的格式。解碼的產(chǎn)生物是多種的,既有供界面使用的C Struct,也可以是XML數(shù)據(jù),還可以是Lua的數(shù)據(jù)結(jié)構(gòu)(tablegt)。如果需要,還可以產(chǎn)生JSON,TL1,Python數(shù)據(jù),TCL數(shù)據(jù)等等。這一層在框架中是通過(guò)機(jī)器自動(dòng)生成的,我們后面會(huì)講到。
內(nèi)存數(shù)據(jù)庫(kù),SOAP Server和TL1 Server都是協(xié)議層的用戶。圖形界面通過(guò)讀寫(xiě)內(nèi)存數(shù)據(jù)庫(kù)和底層通訊。
圖形界面是框架設(shè)計(jì)的重點(diǎn)之一,原因是這里工作量最大,重復(fù)而無(wú)聊的工作最多。
讓我們分析一下在圖形界面開(kāi)發(fā)工作中最主要的事情是什么。
-
收集用戶輸入的數(shù)據(jù)和命令
-
將數(shù)據(jù)和命令發(fā)給底層
-
接收底層反饋
-
將數(shù)據(jù)顯示在界面上
同時(shí)有一些庫(kù)用來(lái)進(jìn)一步簡(jiǎn)化開(kāi)發(fā):
這是一個(gè)簡(jiǎn)化的例子,但是很好的說(shuō)明了框架的特點(diǎn):
-
客戶代碼必須按照規(guī)定的接口實(shí)現(xiàn)
-
框架在適當(dāng)?shù)臅r(shí)候調(diào)用客戶實(shí)現(xiàn)的接口
-
每個(gè)接口都被設(shè)計(jì)為只完成特定的單一功能
-
將各個(gè)步驟有機(jī)的串起來(lái)是框架的事,二次開(kāi)發(fā)者不知道,也無(wú)須知道。
-
通常都要有附帶的庫(kù)。
固件
固件的主要工作是接受來(lái)自軟件的命令,驅(qū)動(dòng)硬件工作;獲取硬件的狀態(tài),進(jìn)行一定的計(jì)算后返回給軟件。早期的固件是很薄的一層,因?yàn)榻^大部分工作是由硬件完成的,固件只起到一個(gè)中轉(zhuǎn)通訊的作用。隨著時(shí)代發(fā)展,現(xiàn)在的固件開(kāi)始承擔(dān)越來(lái)越多原來(lái)由硬件完成的工作。
整個(gè)固件部分分為五大部分:
硬件抽象層,提供對(duì)硬件的訪問(wèn)接口
互相獨(dú)立的任務(wù)群
任務(wù)/消息派發(fā)器
協(xié)議層
通訊層
針對(duì)不同的設(shè)備,工作量集中在硬件抽象層和任務(wù)群上。硬件抽象層是以庫(kù)的形式提供的,由對(duì)硬件最熟悉,經(jīng)驗(yàn)最豐富的工程師來(lái)實(shí)現(xiàn)。任務(wù)群則由一系列的任務(wù)組成,他們分別代表不同的業(yè)務(wù)應(yīng)用。比如測(cè)量誤碼率。這部分由相對(duì)經(jīng)驗(yàn)較少的工程師來(lái)實(shí)現(xiàn),他們的主要工作是實(shí)現(xiàn)規(guī)定的接口,按照標(biāo)準(zhǔn)化文檔定義的方式實(shí)現(xiàn)算法。
任務(wù)定義了如下接口,由具體開(kāi)發(fā)者來(lái)實(shí)現(xiàn):
OnInit();
OnRegisterMessage();
OnMessageArrive();
Run();
OnResultReport();
框架的代碼流程如下:(偽代碼)
CTask* task = new CBertTask();
task->OnInit();
task->OnRegisterMessage();
while(TRUE)
{
task->OnMessageArrive();
task->Run();
task->OnResultReport();
}
delete task;
task = NULL;
這樣,具體任務(wù)的實(shí)現(xiàn)者所關(guān)注的最重要的事情就是實(shí)現(xiàn)這幾個(gè)接口。其他如硬件的初始化,消息的收發(fā),編碼解碼,結(jié)果的上報(bào)等等事情都由框架進(jìn)行了處理。避免了每個(gè)工程師都必須處理從上到下的所有方面。并且這樣的任務(wù)代碼還有很高的重用性,比如是在以太網(wǎng)上還是在Cable Modem上實(shí)現(xiàn)PING的算法都是一樣的。
3.3.4. 實(shí)際效果
在實(shí)際項(xiàng)目中,框架大大降低了開(kāi)發(fā)難度。對(duì)軟件部分尤其明顯,由實(shí)習(xí)生即可完成高質(zhì)量的界面開(kāi)發(fā),開(kāi)發(fā)周期縮短50%以上。產(chǎn)品質(zhì)量大大提升。對(duì)固件部分的貢獻(xiàn)在于降低了對(duì)精通底層硬件的工程師的需要,一般的工程師熟知測(cè)量算法即可。同時(shí),框架的存在保證了性能,穩(wěn)定和可測(cè)試性等要素。
3.4. 框架設(shè)計(jì)中的常用模式
3.4.1. 模板方法模式
模板方法模式是框架中最常用的設(shè)計(jì)模式。其根本的思路是將算法由框架固定,而將算法中具體的操作交給二次開(kāi)發(fā)者實(shí)現(xiàn)。例如一個(gè)設(shè)備初始化的邏輯,框架代碼如下:
TBool CBaseDevice::Init()
{
if ( DownloadFPGA() != KErrNone )
{
LOG(LOG_ERROR,_L(“Download FPGA fail”));
return EFalse;
}
if ( InitKeyPad() != KerrNone )
{
LOG(LOG_ERROR,_L(“Initialize keypad fail”));
return EFalse;
}
return ETrue;
}
DownloadFPGA和InitKeyPad都是CBaseDevice定義的虛函數(shù),二次開(kāi)發(fā)者創(chuàng)建一個(gè)繼承于CBaseDevice的子類,具體來(lái)實(shí)現(xiàn)這兩個(gè)接口??蚣芏x了調(diào)用的次序和錯(cuò)誤的處理方式,二次開(kāi)發(fā)者無(wú)須關(guān)心,也無(wú)權(quán)決定。
3.4.2. 創(chuàng)建型模式
由于框架通常都涉及到各種不同子類對(duì)象的創(chuàng)建,創(chuàng)建型模式是經(jīng)常使用的。例如一個(gè)繪圖軟件的框架,有一個(gè)基類定義了圖形對(duì)象的接口,基于它可以派生出橢圓,矩形,直線各種子類。當(dāng)用戶繪制一個(gè)圖形時(shí),框架就要實(shí)例化該子類。這時(shí)候可以用工廠方法,原型方法等等。
class CDrawObj
{
public:
virtual int DrawObjTypeID()=0;
virtual Icon GetToolBarIcon()=0;
virtual void Draw(Rect rect)=0;
virtual CDrawObj* Clone()=0;
};
3.4.3. 消息訂閱模式
消息訂閱模式是最常用的分離數(shù)據(jù)和界面的方式。界面開(kāi)發(fā)者只需要注冊(cè)需要的數(shù)據(jù),當(dāng)數(shù)據(jù)變化時(shí)框架就會(huì)將數(shù)據(jù)“推”到界面。界面開(kāi)發(fā)者可以無(wú)須關(guān)注數(shù)據(jù)的來(lái)源和內(nèi)部組織形式。
消息訂閱模式最常見(jiàn)的問(wèn)題是同步模式下如何處理重入和超時(shí)。作為框架設(shè)計(jì)者,一定要考慮好這個(gè)問(wèn)題。所謂重入,是二次開(kāi)發(fā)者在消息的回調(diào)函數(shù)中執(zhí)行訂閱/取消訂閱的操作,這會(huì)破壞消息訂閱的機(jī)制。所謂超時(shí)是指二次開(kāi)發(fā)者的消息回調(diào)函數(shù)處理時(shí)間過(guò)長(zhǎng),導(dǎo)致其他消息無(wú)法響應(yīng)。最簡(jiǎn)單的辦法是使用異步模式,讓訂閱者和數(shù)據(jù)發(fā)布者在獨(dú)立進(jìn)程/線程中運(yùn)行。如果不具備此條件,則必須作為框架的重要約定,禁止二次開(kāi)發(fā)者產(chǎn)生此類問(wèn)題。
3.4.4. 裝飾器模式
裝飾器模式賦予了框架在后期增加功能的能力??蚣芏x裝飾器的抽象基類,而由具體的實(shí)現(xiàn)者實(shí)現(xiàn),動(dòng)態(tài)地添加到框架中。
舉一個(gè)游戲中的例子,圖形繪制引擎是一個(gè)獨(dú)立的模塊,比如可以繪制人物的靜止,跑動(dòng)等圖像。如果策劃決定在游戲中增加一種叫“隱身衣”的道具,要求穿著此道具的玩家在屏幕上顯示的是若有若無(wú)的半透明圖像。應(yīng)該如何設(shè)計(jì)圖像引擎來(lái)適應(yīng)后期的游戲升級(jí)呢?
當(dāng)隱身衣被裝備后,就向圖像引擎添加一個(gè)過(guò)濾器。這是個(gè)極度簡(jiǎn)化的例子,實(shí)際的游戲引擎要比這個(gè)復(fù)雜。裝飾器模式還常見(jiàn)用于數(shù)據(jù)的前置和后置處理上。
3.5. 框架的缺點(diǎn)
一個(gè)好的框架可以大大提高產(chǎn)品的開(kāi)發(fā)效率和質(zhì)量,但也有它的缺點(diǎn)。
-
框架一般都比較復(fù)雜,設(shè)計(jì)和實(shí)現(xiàn)一個(gè)好的框架需要相當(dāng)?shù)臅r(shí)間。所以,一般只有在框架可以被多次反復(fù)應(yīng)用的時(shí)候適合,這時(shí)候,前提投入的成本會(huì)得到豐厚的回報(bào)。
-
框架規(guī)定了一系列的接口和規(guī)則,這雖然簡(jiǎn)化了二次開(kāi)發(fā)工作,但同時(shí)也要求二次開(kāi)發(fā)者必須記住很多規(guī)定,如果違反了這些規(guī)定,就不能正常工作。但是由于框架屏蔽了大量的領(lǐng)域細(xì)節(jié),相對(duì)而言,其學(xué)習(xí)成本還是大大降低了的。
-
框架的升級(jí)對(duì)已有產(chǎn)品可能會(huì)造成嚴(yán)重的影響,導(dǎo)致需要完整的回歸測(cè)試。對(duì)這個(gè)問(wèn)題有兩個(gè)辦法。第一是對(duì)框架本身進(jìn)行嚴(yán)格的測(cè)試,有必要建立完善的單元測(cè)試庫(kù),同時(shí)開(kāi)發(fā)示例項(xiàng)目,用來(lái)測(cè)試框架的所有功能。第二則是使用靜態(tài)鏈接,讓已有產(chǎn)品不輕易跟隨升級(jí)。當(dāng)然,如果已有產(chǎn)品有較好的回歸測(cè)試手段,就更好。
-
性能損失。由于框架對(duì)系統(tǒng)進(jìn)行了抽象,增加了系統(tǒng)的復(fù)雜性。諸如多態(tài)這樣的手段使用也會(huì)普遍的降低系統(tǒng)的性能。但是從整體上來(lái)看,框架可以保證系統(tǒng)的性能處于一個(gè)較高的水平。
4. 自動(dòng)代碼生成
4.1. 機(jī)器能做的事就不要讓人來(lái)做
懶惰是程序員的美德,更是架構(gòu)師的美德。軟件開(kāi)發(fā)的過(guò)程就是人告訴機(jī)器如何做事的過(guò)程。如果一件事情機(jī)器自己就可以做,那就不要讓人來(lái)做。因?yàn)闄C(jī)器不僅不知疲倦,而且絕不會(huì)犯錯(cuò)。我們的工作是讓客戶的工作自動(dòng)化,多想一點(diǎn),就能讓我們自己的工作也部分自動(dòng)化。極有耐心的程序員是好的,也是不好的。
經(jīng)過(guò)良好設(shè)計(jì)的系統(tǒng),往往會(huì)出現(xiàn)很多高度類似而且具有很強(qiáng)規(guī)律的代碼。未經(jīng)良好設(shè)計(jì)的系統(tǒng)則可能對(duì)同一類功能產(chǎn)生很多不同的實(shí)現(xiàn)。前面關(guān)于框架設(shè)計(jì)的部分已經(jīng)證明了這一點(diǎn)。有時(shí)候,我們更進(jìn)一步,分析出這些相似代碼之中的規(guī)律,用格式化的數(shù)據(jù)來(lái)描述這些功能,而由機(jī)器來(lái)產(chǎn)生代碼。
4.2. 舉例
4.2.1. 消息的編碼和解碼
上面關(guān)于框架的實(shí)例中,可以看到消息編解碼的部分已經(jīng)被獨(dú)立出來(lái),和其他部分沒(méi)有耦合。加上他本身的特點(diǎn),非常適合進(jìn)一步將其“規(guī)則化”,用機(jī)器產(chǎn)生代碼。
編碼,就是把數(shù)據(jù)結(jié)構(gòu)流化;解碼反之。以編碼為例,代碼無(wú)非是這樣的:(二進(jìn)制協(xié)議)
stream << a.i;
stream << a.j;
stream << a.object;
(為了簡(jiǎn)化,這里假設(shè)已經(jīng)設(shè)計(jì)了一個(gè)流對(duì)象,可以流化各種數(shù)據(jù)類型,并且已經(jīng)處理了諸如字節(jié)序轉(zhuǎn)換等問(wèn)題。)
最后我們得到一個(gè)stream。大家是否已經(jīng)習(xí)慣了寫(xiě)這種代碼?但是這樣的代碼不能體現(xiàn)工程師任何的創(chuàng)造性,因?yàn)槲覀冊(cè)缫呀?jīng)知道有i, 有j, 還有一個(gè)object,為什么還要自己敲入這些代碼呢?如果我們分析一下a的定義,是不是就可以自動(dòng)產(chǎn)生這樣的代碼呢?
struct dataA
{
int i;
int j;
struct dataB object;
};
只需要一個(gè)簡(jiǎn)單的語(yǔ)義分析器解析這段代碼,得到一棵關(guān)于數(shù)據(jù)類型的樹(shù),就可以輕易的產(chǎn)生流化的代碼。這樣的分析器用Python等字符串處理能力強(qiáng)的語(yǔ)言不過(guò)兩百行左右。關(guān)于數(shù)據(jù)類型的樹(shù)類似下圖:
只要遍歷這棵樹(shù),就可以生成所有數(shù)據(jù)結(jié)構(gòu)的流化代碼。
在上一個(gè)框架所舉例的項(xiàng)目中,為一個(gè)硬件模塊自動(dòng)產(chǎn)生的消息編碼解碼器代碼量高達(dá)三萬(wàn)行,幾乎相當(dāng)于一個(gè)小軟件。由于是自動(dòng)產(chǎn)生,沒(méi)有任何錯(cuò)誤,為上層提供了高可靠性。
還可以用XML或者其他的格式定義數(shù)據(jù)結(jié)構(gòu),從而產(chǎn)生自動(dòng)代碼。根據(jù)需要,C++/Java/Python,任何類型的都可以。如果希望提供強(qiáng)檢查,可以使用XSD來(lái)定義數(shù)據(jù)結(jié)構(gòu)。有一個(gè)商業(yè)化的產(chǎn)品,xBinder,很貴,很難用,還不如自己開(kāi)發(fā)。(為什么難用?因?yàn)樗ㄓ?。除了編碼為二進(jìn)制格式,還可以編碼為任何你需要的格式。我們知道二進(jìn)制格式雖然效率很高,但是太難調(diào)試(當(dāng)然有些人看內(nèi)存里的十六進(jìn)制還是很快的),所以我們可以在編碼成二進(jìn)制的同時(shí),還生成編碼為其他可閱讀的格式的代碼,比如XML。這樣,通訊使用二進(jìn)制,而調(diào)試使用XML,兩全其美。產(chǎn)生二進(jìn)制的代碼大概是這樣的:
Xmlbuilder.addelement(“i”,a.i);
Xmlbuilder.addelement(“j”,a.j);
Xmlbuilder.addelement(“object”,a.object);
同樣也很適合機(jī)器產(chǎn)生。同樣的思路可以用來(lái)讓軟件內(nèi)嵌腳本支持。這里不多說(shuō)了。(內(nèi)嵌腳本支持最大的問(wèn)題是在C/C++和腳本之間交換數(shù)據(jù),也是針對(duì)數(shù)據(jù)類型的大量相似代碼。)
最近Google 發(fā)布了它的protocol buffer,就是這樣的思路。目前支持C++/Python,估計(jì)很快會(huì)支持更多的語(yǔ)言,大家可以關(guān)注。以后就不要再手寫(xiě)編碼解碼器了。
4.2.2. GUI代碼
上面的框架設(shè)計(jì)部分,我們說(shuō)到框架對(duì)界面數(shù)據(jù)收集和界面更新無(wú)能為力,只能抽象出接口,由程序員具體實(shí)現(xiàn)。但是讓我們看看這些界面程序員做的事情吧。(代碼經(jīng)過(guò)簡(jiǎn)化,可以看作偽代碼)。
void onDataArrive(CDataBinder& data)
{
m_biterror.setText(“%d”,data.biterror);
m_signallevel.setText(“%d”,data.signallevel”);
m_latency.setText(“%d”,data.latency”);
}
Void onCollectData(CDataBinder& data)
{
data.biterror = atoi(m_biterror.getText());
data. signallevel = atoi(m_ signallevel.getText());
data. latency = atoi(m_ latency.getText());
}
這樣的代碼很有趣嗎?想想我們可以怎么做?(XML描述界面,問(wèn)題是對(duì)于復(fù)雜邏輯很難)
4.2.3. 小結(jié)
由此可見(jiàn),在軟件架構(gòu)的過(guò)程中,首先要遵循一般性的原則,盡量將系統(tǒng)各個(gè)功能部分獨(dú)立出來(lái),實(shí)現(xiàn)高內(nèi)聚低耦合,進(jìn)而發(fā)現(xiàn)系統(tǒng)存在的高度重復(fù),規(guī)律性很強(qiáng)的代碼,進(jìn)一步將他們規(guī)則化,形式化,最后用機(jī)器來(lái)產(chǎn)生這些代碼。目前這方面最成功的應(yīng)用就是消息的編解碼。對(duì)界面代碼的自動(dòng)化生成有一定局限,但也可以應(yīng)用。大家在自己的工作中要擅于發(fā)現(xiàn)這樣的可能,減少工作量,提高工作效率。
4.2.4. Google Protocol Buffer
Google剛剛發(fā)布的Protocol Buffer是使用代碼自動(dòng)生成的一個(gè)典范。
Protocol buffers are a flexible, efficient, automated mechanism for serializing structured data – think XML, but smaller, faster, and simpler. You define how you want your data to be structured once, then you can use special generated source code to easily write and read your structured data to and from a variety of data streams and using a variety of languages. You can even update your data structure without breaking deployed programs that are compiled against the "old" format.
你要做的首先是定義消息的格式,Google指定了它的格式:
message Person {
required string name = 1;
required int32 id = 2;
optional string email = 3;
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
message PhoneNumber {
required string number = 1;
optional PhoneType type = 2 [default = HOME];
}
repeated PhoneNumber phone = 4;
}
Once you've defined your messages, you run the protocol buffer compiler for your application's language on your .proto file to generate data access classes. These provide simple accessors for each field (like query() and set_query()) as well as methods to serialize/parse the whole structure to/from raw bytes – so, for instance, if your chosen language is C++, running the compiler on the above example will generate a class called Person. You can then use this class in your application to populate, serialize, and retrieve Person protocol buffer messages. You might then write some code like this:
Person person;
person.set_name("John Doe");
person.set_id(1234);
person.set_email("jdoe@example.com");
fstream output("myfile", ios::out | ios::binary);
person.SerializeToOstream(&output);
Then, later on, you could read your message back in:
fstream input("myfile", ios::in | ios::binary);
Person person;
person.ParseFromIstream(&input);
cout << "Name: " << person.name() << endl;
cout << "E-mail: " << person.email() << endl;
Protocol Buffer的編碼格式是二進(jìn)制的,同時(shí)也提供可讀的文本格式。效率高,體積小,上下兼容。目前支持Java,Python和C++,很快會(huì)支持更多的語(yǔ)言。
5. 面向語(yǔ)言編程(LOP)
5.1. 從自動(dòng)化代碼生成更進(jìn)一步
面向語(yǔ)言編程的通俗定義是:將特定領(lǐng)域的知識(shí)融合到一種專用的計(jì)算機(jī)語(yǔ)言當(dāng)中,從而提高人與計(jì)算機(jī)交流的效率。
自動(dòng)化代碼生成其實(shí)就是面向語(yǔ)言編程。語(yǔ)言不等于是編程語(yǔ)言,可以是圖,也可以是表,任何可以建立人和機(jī)器之間交流渠道的都是計(jì)算機(jī)語(yǔ)言。軟件開(kāi)發(fā)歷史上的一次生產(chǎn)率的飛躍是高級(jí)語(yǔ)言的發(fā)明。它讓我們以更簡(jiǎn)潔的方式實(shí)現(xiàn)更復(fù)雜的功能。但是高級(jí)語(yǔ)言也有它的缺點(diǎn),那就是從問(wèn)題領(lǐng)域到程序指令的過(guò)程很復(fù)雜。因?yàn)楦呒?jí)語(yǔ)言是為通用目的而設(shè)計(jì)的,所以離問(wèn)題領(lǐng)域很遠(yuǎn)。舉例來(lái)說(shuō),要做一個(gè)圖形界面,我可以跟另一個(gè)工程師說(shuō):這里放一個(gè)按鈕,那邊放一個(gè)輸入框,當(dāng)按下按鈕的時(shí)候,就在輸入框里顯示Hello World。我甚至可以隨手給他畫(huà)出來(lái)。
對(duì)于我和他直接的交流而言,這已經(jīng)足夠了,5分鐘。但是要讓轉(zhuǎn)變?yōu)橛?jì)算機(jī)能夠理解的語(yǔ)言,需要多久?
如果是匯編語(yǔ)言?(告訴計(jì)算機(jī)如何操作寄存器和內(nèi)存)
如果是C++? (告訴計(jì)算機(jī)如何在屏幕上繪圖,如果響應(yīng)鼠標(biāo)鍵盤(pán)消息)
如果有一個(gè)不錯(cuò)的圖形界面庫(kù)?(告訴計(jì)算機(jī)創(chuàng)建Button,Label對(duì)象,管理這些對(duì)象,放置這些對(duì)象,處理消息)
如果有一個(gè)不錯(cuò)的開(kāi)發(fā)框架+IDE? (用WYSIWYG工具繪制,設(shè)計(jì)類,類的成員變量,編寫(xiě)消息響應(yīng)函數(shù))
如果有一門(mén)專門(mén)做圖形界面開(kāi)發(fā)的語(yǔ)言?
可以是這樣的:
Label l {Text=””}
Button b{Text=”ok”,action=l.Text=”hello world”}
通用的計(jì)算機(jī)語(yǔ)言是基于變量,類,分支,循環(huán),鏈表,消息這些概念的。這些概念離問(wèn)題本身有著遙遠(yuǎn)的距離,而且表達(dá)能力非常有限。自然語(yǔ)言表達(dá)能力很強(qiáng),但是歧義和冗余太多,無(wú)法格式化標(biāo)準(zhǔn)化。傳統(tǒng)的思想告訴我們:計(jì)算機(jī)語(yǔ)言就是一條條的指令,編程就是寫(xiě)下這些指令。而面向語(yǔ)言編程的思想是,用盡量貼近問(wèn)題,貼近人的思維的辦法來(lái)描述問(wèn)題,從而降低從人的思想到計(jì)算機(jī)軟件轉(zhuǎn)換的難度。
舉一個(gè)游戲開(kāi)發(fā)的例子。現(xiàn)在的網(wǎng)絡(luò)游戲普遍的采用了C++或者C開(kāi)發(fā)游戲引擎。而具體的游戲內(nèi)容,則是由一系列二次開(kāi)發(fā)工具和語(yǔ)言完成的。地圖編輯器就是一種面向游戲的語(yǔ)言。Lua或者類似的腳本則被嵌入到游戲內(nèi)部,用來(lái)編寫(xiě)武器,技能,任務(wù)等等。Lua本身不具備獨(dú)立開(kāi)發(fā)應(yīng)用程序的能力,然而游戲引擎的設(shè)計(jì)者通過(guò)給Lua提供一系列的,各種層次上的接口,將領(lǐng)域知識(shí)密集的賦予了腳本,從而大大提高了游戲二次開(kāi)發(fā)的效率。網(wǎng)絡(luò)游戲的鼻祖MUD則是設(shè)計(jì)了LPC來(lái)作為游戲的開(kāi)發(fā)語(yǔ)言。MUD的引擎MudOS和LPC之間的關(guān)系如圖:
用LPC創(chuàng)建一個(gè)NPC的代碼類似如下:
inherit NPC;
void create()
{
set_name("菜花蛇", ({ "caihua she", "she" }) );
set("race", "野獸");
set("age", 1);
set("long", "一只青幽幽的菜花蛇,頭部呈橢圓形。n");
set("attitude", "peaceful");
set("str", 15);
set("cor", 16);
set("limbs", ({ "頭部", "身體", "七寸", "尾巴" }) );
set("verbs", ({ "bite" }) );
set("combat_exp", 100+random(50));
set_temp("apply/attack", 7);
set_temp("apply/damage", 4);
set_temp("apply/defence",6);
set_temp("apply/armor",5);
setup();
}
void die()
{
object ob;
message_vision("$N抽搐兩下,$N死了。n", this_object());
ob = new(__DIR__"obj/sherou");
ob->move(environment(this_object()));
destruct(this_object());
}
LPC培養(yǎng)了一大批業(yè)余游戲開(kāi)發(fā)者,甚至成為很多人進(jìn)入IT行業(yè)的起點(diǎn)。原因就是它簡(jiǎn)單,易理解,100%為游戲開(kāi)發(fā)設(shè)計(jì)。這就是LOP的魅力。
5.2. 優(yōu)勢(shì)和劣勢(shì)
LOP最重要的優(yōu)點(diǎn)是將領(lǐng)域知識(shí)固化到語(yǔ)言中,從而:
-
提高開(kāi)發(fā)效率。
-
優(yōu)化團(tuán)隊(duì)結(jié)構(gòu),降低交流成本,領(lǐng)域?qū)<液统绦騿T可以更好的合作。
-
降低耦合,易于維護(hù)。
其次,由于LOP不是通用語(yǔ)言,所涉及的范圍就狹窄很多,所以:
-
更容易得到穩(wěn)定的系統(tǒng)
-
更容易移植
相應(yīng)的,LOP也有它的劣勢(shì):
-
LOP對(duì)領(lǐng)域知識(shí)抽象的要求比框架更高。
-
開(kāi)發(fā)一門(mén)新的語(yǔ)言本身的成本。幸好現(xiàn)在設(shè)計(jì)一門(mén)新的語(yǔ)言不算太難,還有Lua這樣的“專用二次開(kāi)發(fā)”語(yǔ)言的支持。
-
性能損失。不過(guò)相比開(kāi)發(fā)成本的節(jié)約,在非性能核心部分使用LOP還是很值得的。
5.3. 在嵌入式系統(tǒng)中的應(yīng)用
舉例,嵌入式設(shè)備的Web服務(wù)器。很多設(shè)備都提供Web服務(wù)用于配置,比如路由器,ADSL貓等等。這種設(shè)備所提供的web服務(wù)的典型用例是用戶填寫(xiě)一些參數(shù),提交給Web服務(wù)器,Web 服務(wù)器將這些參數(shù)寫(xiě)入硬件,并將操作結(jié)果或者其他信息生成頁(yè)面返回給瀏覽器。由于典型的Apache,Mysql,PHP組合體積太大且不容易移植,通常嵌入式系統(tǒng)的Web服務(wù)都是用C/C++直接寫(xiě)就的。從socket管理,http協(xié)議到具體操作硬件,生成頁(yè)面,都一體負(fù)責(zé)。然而對(duì)于功能復(fù)雜,Web界面要求較高的情況,用C來(lái)寫(xiě)頁(yè)面效率就太低了。
shttpd是一個(gè)小巧的web服務(wù)器,小巧到只有一個(gè).c文件,4000余行代碼。雖然體積很小,卻具備了最基本的功能,比如CGI。它既可以獨(dú)立運(yùn)行,也可以嵌入到其他的應(yīng)用程序當(dāng)中。shttpd在大多數(shù)平臺(tái)上都可以順利編譯、運(yùn)行。lua是一個(gè)小巧的腳本語(yǔ)言,專用于嵌入和擴(kuò)展。它和C/C++代碼有著良好的交互能力。
將Lua引擎嵌入到shttpd中,再使用C編寫(xiě)一個(gè)(一些)驅(qū)動(dòng)硬件的擴(kuò)展,注冊(cè)成為L(zhǎng)ua的函數(shù),形成的系統(tǒng)結(jié)構(gòu)如下圖:
這樣的應(yīng)用在嵌入式系統(tǒng)中是有一定代表性的,即,以C實(shí)現(xiàn)底層核心功能,而把系統(tǒng)的易變部分以腳本實(shí)現(xiàn)。大家可以思考在自己的開(kāi)發(fā)過(guò)程中是否可以使用這種技術(shù)。這是LOP的一種具體應(yīng)用模式。(沒(méi)有創(chuàng)造一種全新的語(yǔ)言,而是使用Lua)
6. 測(cè)試
6.1. 可測(cè)試性是軟件質(zhì)量的一個(gè)度量指標(biāo)
好的軟件是設(shè)計(jì)出來(lái)的,好的軟件也一定是便于測(cè)試的。一個(gè)難于測(cè)試的軟件的質(zhì)量是難以得到保障的。在今天軟件規(guī)模越來(lái)越大的趨勢(shì)下,以下問(wèn)題是普遍存在的:
-
測(cè)試只能手工進(jìn)行,回歸測(cè)試代價(jià)極大,實(shí)際只能執(zhí)行點(diǎn)測(cè),質(zhì)量無(wú)法保證
-
各個(gè)模塊只有集成到一起后才能測(cè)試
-
代碼不經(jīng)過(guò)任何單元測(cè)試就集成
這些問(wèn)題的根源都在于缺乏一個(gè)良好的軟件設(shè)計(jì)。一個(gè)好的軟件設(shè)計(jì)應(yīng)該使得單元測(cè)試,模塊測(cè)試和回歸測(cè)試都變得容易,從而保證測(cè)試的廣度和深度,最終產(chǎn)生高質(zhì)量的軟件。除了功能,非功能性需求也必須是可測(cè)試的。所以,可測(cè)試性是軟件設(shè)計(jì)中一個(gè)重要的指標(biāo),是系統(tǒng)架構(gòu)師需要認(rèn)真考慮的問(wèn)題。
6.2. 測(cè)試驅(qū)動(dòng)的軟件架構(gòu)
這里談的是測(cè)試驅(qū)動(dòng)的軟件架構(gòu),而不是測(cè)試驅(qū)動(dòng)的開(kāi)發(fā)。TDD(Test Driven Development) 是一種開(kāi)發(fā)方式,是一種編碼實(shí)踐。而測(cè)試驅(qū)動(dòng)的架構(gòu)強(qiáng)調(diào)的是,從提高可測(cè)試性的角度進(jìn)行架構(gòu)設(shè)計(jì)。軟件的測(cè)試分為多個(gè)層次:
6.3. 系統(tǒng)測(cè)試
系統(tǒng)測(cè)試是指由測(cè)試人員執(zhí)行的,驗(yàn)證軟件是否完整正確的實(shí)現(xiàn)了需求的測(cè)試。這種測(cè)試中,測(cè)試人員作為用戶的角色,通過(guò)程序界面進(jìn)行測(cè)試。在大部分情況下這些工作是手工完成的。在規(guī)范的流程中,這個(gè)過(guò)程通常要占到整個(gè)軟件開(kāi)發(fā)時(shí)間的1/3以上。而當(dāng)有新版本發(fā)布的時(shí)候,盡管只涉及了軟件的一部分,測(cè)試部門(mén)依然需要完整的測(cè)試整個(gè)軟件。這是由代碼“副作用”特點(diǎn)決定的。有時(shí)候修改一個(gè)bug可以引發(fā)更多的bug,破壞原來(lái)工作正常的代碼。這在測(cè)試中叫回歸測(cè)試(Regression test)。對(duì)于規(guī)模較大的軟件,回歸測(cè)試需要很長(zhǎng)的時(shí)間,在版本新增功能和錯(cuò)誤修正不多的情況下,回歸測(cè)試可以占到整個(gè)軟件開(kāi)發(fā)過(guò)程了一半以上,嚴(yán)重影響了軟件的交付,也使軟件測(cè)試部門(mén)成為軟件開(kāi)發(fā)流程中的瓶頸。測(cè)試過(guò)程自動(dòng)化,是部分解決這個(gè)問(wèn)題的辦法。
作為架構(gòu)師,有必要考慮如何實(shí)現(xiàn)軟件的可自動(dòng)化測(cè)試性。
6.3.1. 界面自動(dòng)化測(cè)試
在沒(méi)有圖形化界面以前,字符方式的界面是比較容易進(jìn)行自動(dòng)化測(cè)試的。一個(gè)編寫(xiě)良好的腳本就可以實(shí)現(xiàn)輸入和對(duì)輸出的檢查。但是對(duì)于圖形化的界面,人的參與似乎變得不可缺少。有一些界面自動(dòng)化的測(cè)試工具,如WinRunner, 這些工具可以記錄下測(cè)試人員的操作成為腳本,然后通過(guò)回放這些腳本,就可以實(shí)現(xiàn)操作的自動(dòng)化。針對(duì)嵌入式設(shè)備,有Test Quest可以使用,通過(guò)在設(shè)備中運(yùn)行一個(gè)類似遠(yuǎn)程桌面的Agent,PC端的測(cè)試工具可以用圖像識(shí)別的方法識(shí)別出不同的組件,并發(fā)送相應(yīng)用戶的輸入。此類工具的基本工作原理如圖:
但是這個(gè)過(guò)程在實(shí)際中存在三個(gè)問(wèn)題:
-
可靠性差,經(jīng)常中斷運(yùn)行。要寫(xiě)一個(gè)可靠的腳本甚至比開(kāi)發(fā)軟件還要困難。比如,按下一個(gè)按鈕,有時(shí)候一個(gè)對(duì)話框立刻就出現(xiàn),有時(shí)候可能要好幾秒,有時(shí)候甚至不出現(xiàn),操作錄制工具不能自動(dòng)實(shí)現(xiàn)這些判斷,而需要手動(dòng)修改。
-
對(duì)操作結(jié)果的判斷很困難,尤其是非標(biāo)準(zhǔn)的控件。
-
當(dāng)界面修改后,原有代碼很容易失效
要應(yīng)用基于圖形界面的自動(dòng)化測(cè)試工具,架構(gòu)師在架構(gòu)的時(shí)候應(yīng)該考慮:
-
界面風(fēng)格如何保持一致。應(yīng)當(dāng)由架構(gòu),而非程序員決定架構(gòu)的風(fēng)格。包括布局,控件大小,相對(duì)位置,文字,對(duì)操作的響應(yīng)方式,超時(shí)時(shí)長(zhǎng),等等。
-
如何在最合適測(cè)試工具的界面和用戶喜歡的界面之中折中。比如,Test Quest是基于圖像識(shí)別的,那么黑白兩色的界面是最有利的,而用戶喜歡的漸進(jìn)色就非常不利。也許讓界面具備自動(dòng)的切換能力最好。
對(duì)于已經(jīng)完成的產(chǎn)品,如果架構(gòu)沒(méi)有為自動(dòng)化測(cè)試做過(guò)考慮,所能應(yīng)用的范圍就非常有限,不過(guò)還是有一些思路可以供參考:
-
實(shí)現(xiàn)小規(guī)模的自動(dòng)化腳本。針對(duì)一個(gè)具體的操作流程進(jìn)行測(cè)試,而不是試圖用一個(gè)腳本測(cè)試整個(gè)軟件。一系列的小測(cè)試腳本組成了一個(gè)集合,覆蓋系統(tǒng)的一部分功能。這些測(cè)試腳本可以都以軟件啟動(dòng)時(shí)的狀態(tài)作為基準(zhǔn),所以在狀態(tài)處理上會(huì)比較簡(jiǎn)單
-
”猴子測(cè)試”有一定的價(jià)值。所謂猴子測(cè)試,就是隨機(jī)操作鼠標(biāo)和鍵盤(pán)。這種測(cè)試完全不理解軟件的功能,可以發(fā)現(xiàn)一些正常測(cè)試無(wú)法發(fā)現(xiàn)的錯(cuò)誤。據(jù)微軟內(nèi)部的資料,微軟的一些產(chǎn)品15%的錯(cuò)誤是由“猴子測(cè)試”發(fā)現(xiàn)的。
總的來(lái)講,基于界面的自動(dòng)化測(cè)試是不成熟的。對(duì)架構(gòu)師而言一定要避免功能只能通過(guò)界面才能訪問(wèn)。要讓界面僅僅是界面,而軟件大部分的功能是獨(dú)立于界面并可以通過(guò)其他方式訪問(wèn)的。上面框架的例子中的設(shè)計(jì)就體現(xiàn)了這一點(diǎn)。
思考:如何讓界面具有自我測(cè)試功能?
6.3.2. 基于消息的自動(dòng)化測(cè)試
如果軟件對(duì)外提供基于消息的接口,自動(dòng)化測(cè)試就會(huì)變得簡(jiǎn)單的多。上面已經(jīng)提到了固件的TL1接口。對(duì)于界面部分,則應(yīng)該在設(shè)計(jì)的時(shí)候,將純粹的“界面”獨(dú)立出來(lái),讓它盡可能的薄,而其他部分依然可以基于消息提供服務(wù)。
在消息的基礎(chǔ)上,用腳本語(yǔ)言包裝成函數(shù)的形式,可以很容易的調(diào)用,并覆蓋消息的各種參數(shù)組合,從而提高測(cè)試的覆蓋率。關(guān)于如何將消息包裝為腳本,可以參考SOAP的實(shí)現(xiàn)。如果使用的不是XML,也可以自己實(shí)現(xiàn)類似的自動(dòng)代碼生成。
這些測(cè)試腳本應(yīng)該由開(kāi)發(fā)人員撰寫(xiě),每當(dāng)實(shí)現(xiàn)了一個(gè)新的接口(也就是一條新的消息),就應(yīng)該撰寫(xiě)相應(yīng)的測(cè)試腳本,并作為項(xiàng)目的一部分保存在代碼庫(kù)中。當(dāng)需要執(zhí)行回歸測(cè)試的時(shí)候,只要運(yùn)行一遍測(cè)試腳本即可,大大提高了回歸測(cè)試的效率。
所以,為了實(shí)現(xiàn)軟件的自動(dòng)化測(cè)試,提供基于消息的接口是一個(gè)很好的辦法,這讓我們可以在軟件之外獨(dú)立的編寫(xiě)測(cè)試腳本。在設(shè)計(jì)的時(shí)候可以考慮這個(gè)因素,適當(dāng)?shù)脑黾榆浖⒌闹С?。?dāng)然,TL1只是一個(gè)例子,根據(jù)項(xiàng)目的需要,可以選擇任何適合的協(xié)議,如SOAP。
6.3.3. 自動(dòng)化測(cè)試框架
在編寫(xiě)自動(dòng)化測(cè)試腳本的時(shí)候,有很多的工作是重復(fù)的,比如建立socket連接,日志,錯(cuò)誤處理,報(bào)表生成等。同時(shí),對(duì)于測(cè)試人員來(lái)說(shuō),這些工作可能是比較困難的。因此,設(shè)計(jì)一個(gè)框架,實(shí)現(xiàn)并隱藏這些重復(fù)和復(fù)雜的技術(shù),讓測(cè)試腳本的編寫(xiě)者將注意力集中在具體的測(cè)試邏輯上。
這樣一個(gè)框架應(yīng)該實(shí)現(xiàn)以下功能:
-
完成連接的初始化等基礎(chǔ)工作。
-
捕獲所有的錯(cuò)誤,保證Test Case中的錯(cuò)誤不會(huì)打斷后續(xù)的Test Case執(zhí)行。
-
自動(dòng)檢測(cè)和執(zhí)行Test Case。新增的Test Case是獨(dú)立的腳本文件,無(wú)須修改框架的代碼或者配置。
-
消息編解碼,并以函數(shù)的方式提供給Test Case編寫(xiě)者調(diào)用。
-
方便的工具,如報(bào)表,日志等。
-
自動(dòng)統(tǒng)計(jì)Test Case的運(yùn)行結(jié)果并生成報(bào)告。
自動(dòng)化測(cè)試框架的思路和一般的軟件框架是一致的,就是避免重復(fù)勞動(dòng),降低開(kāi)發(fā)難度。
下圖是一個(gè)自動(dòng)化測(cè)試框架的結(jié)構(gòu)圖:
每個(gè)Test Case都必須定義一個(gè)規(guī)定的Run函數(shù),框架將依次調(diào)用,并提供相應(yīng)的庫(kù)函數(shù)供Test Case用來(lái)發(fā)送命令和獲得結(jié)果。這樣,測(cè)試用例的編寫(xiě)者就只需要將注意力集中在測(cè)試本身。舉例:
def run():
open_laser()
assert(get_laser_state() == ON)
insert_error(BIT_ERROR)
assert(get_error_bit() == BIT_ERROR)
測(cè)試用例的編寫(xiě)者擁有的知識(shí)是“必須先打開(kāi)激光器然后才能向線路上插入錯(cuò)誤”。而架構(gòu)師能提供的是消息收發(fā),編解碼,錯(cuò)誤處理,報(bào)表生成等,并將這些為測(cè)試用例編寫(xiě)者隔離。
問(wèn)題: open_laser, get_laser_state這些函數(shù)是誰(shuí)寫(xiě)的?
問(wèn)題:如何進(jìn)一步實(shí)現(xiàn)知識(shí)的解耦?能否有更方便的語(yǔ)言來(lái)編寫(xiě)TestCase?
6.3.4. 回歸測(cè)試
有了自動(dòng)化的測(cè)試腳本和框架,回歸測(cè)試就變得很簡(jiǎn)單了。每當(dāng)有新版本發(fā)布時(shí),只需運(yùn)行一遍現(xiàn)有的Test Case,分析測(cè)試報(bào)告,如果有測(cè)試失敗的Case則回歸測(cè)試失敗,需要重新修改,直到所有的Case完全通過(guò)。完整的回歸測(cè)試是軟件質(zhì)量的重要保證。
6.4. 集成測(cè)試
集成測(cè)試要驗(yàn)證的是系統(tǒng)各個(gè)組成模塊的接口是否工作正常。這是比系統(tǒng)測(cè)試更低層的測(cè)試,通常由開(kāi)發(fā)人員和測(cè)試人員共同完成。
例如在一個(gè)典型的嵌入式系統(tǒng)中,F(xiàn)PGA,固件和界面是常見(jiàn)的三個(gè)模塊。模塊本身還可以劃分為更小的模塊,從而降低復(fù)雜度。嵌入式軟件模塊測(cè)試的常見(jiàn)問(wèn)題是硬件沒(méi)有固件則無(wú)法工作,固件沒(méi)有界面就無(wú)法驅(qū)動(dòng);反過(guò)來(lái),界面沒(méi)有固件不能完整運(yùn)行,固件沒(méi)有硬件甚至無(wú)法運(yùn)行。于是沒(méi)有經(jīng)過(guò)測(cè)試的模塊直到集成的時(shí)候才能完整運(yùn)行,發(fā)現(xiàn)問(wèn)題后需要考慮所有模塊的問(wèn)題,定位和解決的代價(jià)都很大。假設(shè)有模塊A和B,各有十個(gè)bug。如果都沒(méi)有經(jīng)過(guò)模塊測(cè)試直接集成,可以認(rèn)為排錯(cuò)的工作量相當(dāng)于10*10等于100。
所以,在設(shè)計(jì)一個(gè)模塊的時(shí)候,首先要考慮,這個(gè)模塊如何單獨(dú)測(cè)試?比如,如果界面和固件之間是通過(guò)SOCKET通信的,那么就可以開(kāi)發(fā)一個(gè)模擬固件,在同樣的端口上提供服務(wù)。這個(gè)模擬固件不執(zhí)行實(shí)際的操作,但是會(huì)響應(yīng)界面的請(qǐng)求并返回模擬的結(jié)果。并且返回的結(jié)果可以覆蓋到各種典型的情況,包括錯(cuò)誤的情況。使用這樣的技術(shù),界面部分幾乎可以得到100%的驗(yàn)證,在集成階段遇到錯(cuò)誤的大大減少。
對(duì)固件而言,因?yàn)樘幱谙到y(tǒng)的中間,所以問(wèn)題復(fù)雜一些。一方面,要讓固件可以通過(guò)GUI以外的途徑被調(diào)用;另一方面則要模擬硬件的功能。對(duì)于第一點(diǎn),在設(shè)計(jì)的時(shí)候,要讓接口和實(shí)現(xiàn)分離。接口可以隨意的更換,比如和GUI的接口也許是JSON,同時(shí)還可以提供telnet的TL1接口,但是實(shí)現(xiàn)是完全一樣的。這樣,在和GUI集成之前,就可以通過(guò)TL1進(jìn)行完全的測(cè)試固件。對(duì)于第二點(diǎn),則應(yīng)該在設(shè)計(jì)的時(shí)候提取出硬件抽象層,讓固件的主要實(shí)現(xiàn)和寄存器,內(nèi)存地址等因素隔離開(kāi)來(lái)。在沒(méi)有硬件或者硬件設(shè)計(jì)未定的時(shí)候?qū)崿F(xiàn)一個(gè)硬件模擬層,來(lái)保證固件可以完整運(yùn)行并測(cè)試。
6.5. 單元測(cè)試
單元測(cè)試是軟件測(cè)試的最基本單位,是由開(kāi)發(fā)人員執(zhí)行以保證其所開(kāi)發(fā)代碼正確的過(guò)程。開(kāi)發(fā)人員應(yīng)該提交經(jīng)過(guò)測(cè)試的代碼。未經(jīng)單元測(cè)試的代碼在進(jìn)入軟件后,不僅發(fā)現(xiàn)問(wèn)題后很難定位,而且通過(guò)系統(tǒng)測(cè)試是很難做到對(duì)代碼分支的完全覆蓋的。TDD就是基于這個(gè)層次的開(kāi)發(fā)模式。
單元測(cè)試的粒度一般是函數(shù)或者類,例如下面這個(gè)常用函數(shù):
int atoi(const char *nptr);
這是一個(gè)功能非常單一的函數(shù),所以單元測(cè)試對(duì)它非常有效。可以通過(guò)單元測(cè)試驗(yàn)證下列情況:
-
一般正常調(diào)用,如”9”,”1000”,”-1”等
-
空的nptr指針
-
非數(shù)字字符串,”abc”,”@#!123”,”123abc”
-
帶小數(shù)點(diǎn)的字符串, “1.1”,”0.111”,”.123”
-
超長(zhǎng)字符串
-
超大數(shù)字,”999999999999999999999999999”
-
超過(guò)一個(gè)的-號(hào)和位置錯(cuò)誤的-號(hào),”—1”,”-1-“,”-1-2”
如果atoi通過(guò)了以上測(cè)試,我們就可以放心的將它集成到軟件中去了。由它再引發(fā)問(wèn)題的概率就很小了(不是完全沒(méi)有,因?yàn)槲覀儾荒鼙闅v所有可能,只是挑選有代表性的異常情況進(jìn)行測(cè)試)。
以上的例子可以說(shuō)是單元測(cè)試的典范,但實(shí)際中卻常常不是這么回事。我們常常發(fā)現(xiàn)寫(xiě)好的函數(shù)很難做單元測(cè)試,不僅工作量很大,效果也不見(jiàn)得好。其根本的原因是,函數(shù)沒(méi)有遵循好一些原則:
-
單一功能
-
低耦合
反觀atoi的例子,功能單一明確,和其他函數(shù)幾乎沒(méi)有任何耦合(我上面并沒(méi)有寫(xiě)atoi的代碼實(shí)現(xiàn),大家可以自己實(shí)現(xiàn),希望是0耦合)。
下面我舉一個(gè)實(shí)際中的例子。
這是一個(gè)簡(jiǎn)單的TL1命令發(fā)送和解析軟件,功能需求描述如下:
ü 通過(guò)telnet與TL1服務(wù)器通訊
ü 發(fā)送TL1命令給TL1服務(wù)器
ü 解析TL1服務(wù)器的響應(yīng)
TL1是通訊行業(yè)廣泛使用的一種協(xié)議,為了給不熟悉TL1的朋友簡(jiǎn)化問(wèn)題,我定義了一個(gè)簡(jiǎn)化的格式:
CMDPAYLOAD;
CMD - 命令的名字,可以是任意字母開(kāi)頭,由字母和下劃線組成的字符串
CTAG - 一個(gè)數(shù)字,用于標(biāo)志命令的序號(hào)
PAYLOAD - 可以是任意格式的內(nèi)容
; - 結(jié)束符
相應(yīng)的,TL1服務(wù)器的回應(yīng)也有格式:
DATE
CTAG COMPLD
PAYLOAD
;
DATE – 日期和時(shí)間
CTAG – 一個(gè)數(shù)字,和TL1 命令所攜帶的CTAG一樣
COMPLD – 表明命令執(zhí)行成功
PAYLOAD - 返回的結(jié)果,可以是任何格式的內(nèi)容
; - 結(jié)束符
舉例:
命令:GET-IP-CONFIG;
結(jié)果:
2008-7-19 1100
1 COMPLD
ip address: 192.168.1.200
gate way: 192.168.1.1
dns: 192.168.1.3
;
命令:SET-IP-CONFIG172.31.2.100,172.31.2.1,172.31.2.3;
結(jié)果:
2008-7-19 1105
2 COMPLD
;
軟件的最上層可能是這樣的:
Dict* ipconf = GET_IP_CONFIG();
ipconf->set(“ipaddr”,”172.31.2.100)
ipconf->set(“gateway”,”172.3.2.1”)
ipconf->set(“dns”,”172.31.2.1”)
SET_IP_CONFIG(ipconf);
以GET_IP_CONFIG為例,這個(gè)函數(shù)應(yīng)該完成的功能包括:
ü 建立telnet連接,如果連接尚未建立
ü 構(gòu)造TL1命令字符串
ü 發(fā)送
ü 接收反饋
ü 解析反饋,并給IP_CONF結(jié)構(gòu)復(fù)制
ü 返回
我們當(dāng)然不希望每個(gè)這樣的函數(shù)都重復(fù)實(shí)現(xiàn)這些功能,所以我們定義了幾個(gè)模塊:
-
Telnet 連接管理
-
TL1命令構(gòu)造
-
TL1 結(jié)果解析
這里我們來(lái)分析TL1結(jié)果解析,假設(shè)設(shè)計(jì)為一個(gè)函數(shù),函數(shù)的原型如下:
Dict* TL1Parse(const char* tl1response)
這個(gè)函數(shù)的功能是接受一個(gè)字符串,如果它是一個(gè)合法且已知的TL1回應(yīng),則將其中的結(jié)果提取出來(lái),放入一個(gè)字典對(duì)象中。
這本來(lái)會(huì)是一個(gè)很便于進(jìn)行單元測(cè)試的例子:輸入各種字符串,檢查返回結(jié)果是否正確即可。但是在這個(gè)軟件中,有一個(gè)很特殊的問(wèn)題:
TL1Parse在解析一個(gè)字符串時(shí),它必須要知道當(dāng)前要處理的是哪條命令的回應(yīng)。但是請(qǐng)注意,在TL1的回應(yīng)中,是不包括命令的名字的。唯一的辦法是使用CTAG,這個(gè)命令和回應(yīng)一一對(duì)應(yīng)的數(shù)字。Tl1Parse首先提取出CTAG來(lái),然后查找使用這個(gè)CTAG的是什么命令。這里產(chǎn)生了一個(gè)對(duì)外調(diào)用,也就是耦合。
有一個(gè)對(duì)象維護(hù)了一個(gè)CTAG和命令名字對(duì)應(yīng)關(guān)系的表,通過(guò)CTAG,可以查詢到對(duì)應(yīng)的命令名,從而知道如何解析這個(gè)TL1 response.
如此一來(lái),TL1Parse就無(wú)法進(jìn)行單元測(cè)試了,至少不能輕易的進(jìn)行。通常的樁函數(shù)的辦法都不好用了。
怎么辦?
重新設(shè)計(jì),消除耦合。
將TL1Parse拆分為兩個(gè)函數(shù):
Tl1_header TL1_get_header(const char* tl1response)
Dict* TL1_parse_payload(const char* tl1name ,const char* tl1payload)
這兩個(gè)函數(shù)都可以單獨(dú)進(jìn)行完整的單元測(cè)試。而這兩個(gè)函數(shù)的代碼基本就是TL1Parse切分了一下,但是其可測(cè)試性得到了很大的提高,得到一個(gè)可靠的解析器的可能性自然也大大提升了。
這個(gè)例子演示了如何通過(guò)設(shè)計(jì)來(lái)提高代碼的可測(cè)試性—這里是單元測(cè)試。一個(gè)隨意設(shè)計(jì),隨意實(shí)現(xiàn)的軟件要進(jìn)行單元測(cè)試將會(huì)是一場(chǎng)噩夢(mèng),只有在設(shè)計(jì)的時(shí)候就考慮到單元測(cè)試的需要,才能真正的進(jìn)行單元測(cè)試。
6.5.1. 圈復(fù)雜度測(cè)量
模塊的復(fù)雜度直接影響了單元測(cè)試的覆蓋率。最著名的度量代碼復(fù)雜度的方法是圈復(fù)雜度測(cè)量。
計(jì)算公式:V(F)=e-n+2。其中e是流程圖中的邊的數(shù)量,n是節(jié)點(diǎn)數(shù)量。簡(jiǎn)單的算法是統(tǒng)計(jì)如 if、while、do和switch 中的 case 語(yǔ)句數(shù)加1。適合于單元測(cè)試的代碼的復(fù)雜度一般認(rèn)為不應(yīng)該超過(guò)10。
6.5.2. 扇入扇出測(cè)量
扇入是指一個(gè)模塊被其他模塊所引用。扇出是指一個(gè)模塊引用其他模塊。我們都知道好的設(shè)計(jì)應(yīng)該是高內(nèi)聚低耦合的,也就是高扇入低扇出。一個(gè)扇出超過(guò)7的模塊一般認(rèn)為是設(shè)計(jì)欠佳的。扇出過(guò)大的模塊進(jìn)行單元測(cè)試不論從樁設(shè)置還是覆蓋率上都是困難的。將系統(tǒng)的傳出耦合和傳入耦合的數(shù)量結(jié)合起來(lái),形成另一個(gè)度量:不穩(wěn)定性。
不穩(wěn)定性 = 扇出 / (扇入 + 扇出)
這個(gè)值的范圍從0到1。值越接近1,它就越不穩(wěn)定。在設(shè)計(jì)和實(shí)現(xiàn)架構(gòu)時(shí),應(yīng)當(dāng)盡量依賴穩(wěn)定的包,因?yàn)檫@些包不太可能更改。相反的,依賴一個(gè)不穩(wěn)定的包,發(fā)生更改時(shí)間接受到傷害的可能性就更大。
6.5.3. 框架對(duì)單元測(cè)試的意義
框架的應(yīng)用在很大程度上可以幫助進(jìn)行單元測(cè)試。因?yàn)槎伍_(kāi)發(fā)者被限定實(shí)現(xiàn)特定的接口,而這些接口勢(shì)必都是功能明確,簡(jiǎn)單,低耦合的。之前的框架示例代碼也演示了這一點(diǎn)。這再次說(shuō)明了,由高水平的工程師設(shè)計(jì)出的框架,可以強(qiáng)制初級(jí)工程師產(chǎn)生高質(zhì)量的代碼。
7. 維護(hù)架構(gòu)的一致性
在實(shí)際的開(kāi)發(fā)中,代碼偏離精心設(shè)計(jì)的架構(gòu)是很常見(jiàn)的事情,比如下圖示例了一個(gè)嵌入式設(shè)備中設(shè)計(jì)的MVC模式:
View依賴于Controller和Model, Controller依賴于Model,Model作為底層服務(wù)提供者,不依賴View或者Controller. 這是一個(gè)適用的架構(gòu),可以在相當(dāng)程度上分離業(yè)務(wù),數(shù)據(jù)和界面。但是,某個(gè)程序員在實(shí)現(xiàn)時(shí),使用了一個(gè)從Model到View的調(diào)用,破壞了架構(gòu)。
這種現(xiàn)象通常發(fā)生在產(chǎn)品的維護(hù)階段,有時(shí)也發(fā)生在架構(gòu)的實(shí)現(xiàn)階段。為了增加一個(gè)功能或者修正一個(gè)錯(cuò)誤,程序員由于不理解原有架構(gòu)的思路,或者只是單純的偷懶,走了“捷徑”。如果這樣的實(shí)現(xiàn)不能及時(shí)發(fā)現(xiàn)并糾正,設(shè)計(jì)良好的架構(gòu)就會(huì)被漸漸破壞,也就是我們常說(shuō)的“架構(gòu)”腐爛了。通常一個(gè)有一定年齡的軟件產(chǎn)品的架構(gòu)都有這個(gè)問(wèn)題。如何監(jiān)視并防止這種問(wèn)題,有技術(shù)上的和管理上的手段。
技術(shù)上,借助工具,可以對(duì)系統(tǒng)組件的依賴進(jìn)行分析,架構(gòu)的外在表現(xiàn)最重要的就是各個(gè)部分的耦合關(guān)系。有一些工具可以統(tǒng)計(jì)軟件組件的扇入和扇出??梢杂眠@種工具編寫(xiě)測(cè)試代碼,對(duì)組件的扇出進(jìn)行檢測(cè),一旦發(fā)現(xiàn)測(cè)試失敗,就說(shuō)明架構(gòu)遭到了破壞。這種檢查可以集成在一些IDE中, 在編譯時(shí)同步進(jìn)行,或者在check in的時(shí)候進(jìn)行。更高級(jí)的工具可以對(duì)代碼進(jìn)行反向工程生成UML,可以提供更進(jìn)一步的信息。但通常對(duì)扇入扇出做檢查就可以了。
通過(guò)設(shè)置代碼檢視的開(kāi)發(fā)流程,對(duì)程序員check in的代碼進(jìn)行評(píng)審,也可以防止此類問(wèn)題。代碼檢視是開(kāi)發(fā)中非常重要的一環(huán),它屬于開(kāi)發(fā)后期階段用來(lái)防止壞的代碼進(jìn)入系統(tǒng)的重要手段。代碼檢視通常要關(guān)注以下問(wèn)題:
-
是否正確完整的完成了需求
-
是否遵循了系統(tǒng)的架構(gòu)
-
代碼的可測(cè)試性
-
錯(cuò)誤處理是否完備
-
代碼規(guī)范
代碼檢視通常以會(huì)議的形式進(jìn)行,時(shí)間點(diǎn)設(shè)置在項(xiàng)目階段性完成,需要check in代碼時(shí)。對(duì)于迭代式開(kāi)發(fā),則可以在一個(gè)迭代周期結(jié)束前組織。參與人員包括架構(gòu)師,項(xiàng)目經(jīng)理,項(xiàng)目成員,其他項(xiàng)目的資深工程師等。一般時(shí)間不要太長(zhǎng),以不超過(guò)2個(gè)小時(shí)為宜。會(huì)議前2天左右發(fā)出會(huì)議通知和相關(guān)文檔代碼,與會(huì)者必須先了解會(huì)議內(nèi)容,進(jìn)行準(zhǔn)備。會(huì)議中,由代碼的作者首先講解代碼需要實(shí)現(xiàn)的功能,自己的實(shí)現(xiàn)思路。然后展示代碼。與會(huì)者根據(jù)自己的經(jīng)驗(yàn)提出各種問(wèn)題和改進(jìn)意見(jiàn)。這種會(huì)議最忌諱的是讓作者感到被指責(zé)或者輕視,所以,會(huì)議組織者要首先定義會(huì)議的基調(diào):會(huì)議成功與否的標(biāo)準(zhǔn)不是作者的代碼質(zhì)量如何,而是與會(huì)者是否提供了有益的建議。會(huì)后由作者給與會(huì)者打分,而不是反之。
8. 一個(gè)實(shí)際嵌入式系統(tǒng)架構(gòu)的演化
上世紀(jì)九十年代,互聯(lián)網(wǎng)的極速發(fā)展讓通訊測(cè)試設(shè)備也得到了極大的發(fā)展。那個(gè)年代,能夠?qū)崿F(xiàn)某種測(cè)量的硬件是競(jìng)爭(zhēng)的核心,軟件的目的僅僅是驅(qū)動(dòng)硬件運(yùn)行起來(lái),再提供一個(gè)簡(jiǎn)單的界面。所以,最初的產(chǎn)品的軟件結(jié)構(gòu)非常簡(jiǎn)單,類似前面的城鐵門(mén)禁系統(tǒng)。
優(yōu)點(diǎn):程序簡(jiǎn)單明了的實(shí)現(xiàn)了用戶的需求,一個(gè)程序員就可以全部搞定。
缺點(diǎn):完全沒(méi)有劃分模塊,底層上層耦合嚴(yán)重。
8.1. 數(shù)據(jù)處理
用戶要求能將測(cè)量結(jié)果保存下來(lái),并可以重新打開(kāi)。數(shù)據(jù)存儲(chǔ)模塊和界面被獨(dú)立出來(lái)。
依然保持上面的主邏輯,但是界面部分不僅可以顯示實(shí)時(shí)的數(shù)據(jù),也可以從ResultManager中讀取數(shù)據(jù)來(lái)顯示。
優(yōu)點(diǎn):數(shù)據(jù)和界面分離的雛形初步顯現(xiàn)
缺點(diǎn):ResultManager只是作為一個(gè)工具存在,負(fù)責(zé)保存和裝載歷史數(shù)據(jù)。界面和數(shù)據(jù)的來(lái)源依然耦合的很緊。不同的界面需要的不同數(shù)據(jù)都是通過(guò)硬編碼判斷的。
8.2. 窗口管理
隨著功能不斷復(fù)雜,界面窗口越來(lái)越多,原來(lái)靠一個(gè)類來(lái)繪制各種界面的方式已經(jīng)不能承受。于是窗口的概念被引入。每個(gè)界面都被視為一個(gè)窗口,窗口中的元素為控件。窗口的打開(kāi),關(guān)閉,隱藏則由窗口管理器負(fù)責(zé)。
優(yōu)點(diǎn):界面功能以窗口的單位分離,不再是一個(gè)超大的集合。
缺點(diǎn):雖然有了窗口管理器,但是界面依然是直接和底層耦合的,依然是大循環(huán)結(jié)構(gòu)。
8.3. MVC模式
隨著規(guī)模進(jìn)一步擴(kuò)大,最初的大循環(huán)結(jié)構(gòu)終于無(wú)法滿足日益復(fù)雜的需求了。標(biāo)準(zhǔn)的MVC模式被引入,經(jīng)歷了一次大的重構(gòu)。
數(shù)據(jù)中心作為Model被獨(dú)立出來(lái),保存著當(dāng)前最新的數(shù)據(jù)。View被放在了獨(dú)立的任務(wù)中執(zhí)行,定期從DataCenter輪詢數(shù)據(jù)。用戶的操作通過(guò)View發(fā)送給Controller,進(jìn)一步調(diào)用硬件驅(qū)動(dòng)執(zhí)行。硬件執(zhí)行的結(jié)果從驅(qū)動(dòng)到Controller更新到DataCenter中。界面,數(shù)據(jù),命令三者基本解耦。ResultManager成為DataCenter的一個(gè)組件,View不再直接與其通訊。
MVC模式的引入,第一次讓這個(gè)產(chǎn)品了有真正意義上職責(zé)明晰,功能獨(dú)立的架構(gòu)。
8.4. 大量類似模塊,低效的復(fù)用
到上一步,作為一個(gè)單獨(dú)的嵌入式設(shè)備,其架構(gòu)基本可以滿足需求。但是隨著市場(chǎng)的擴(kuò)展,越來(lái)越多的設(shè)備被設(shè)計(jì)出來(lái)。這些設(shè)備雖然執(zhí)行的具體測(cè)量任務(wù)不同,但是他們都有著同樣的操作方式,類似的界面,更主要的是,它們面臨的問(wèn)題領(lǐng)域是相同的。長(zhǎng)期以來(lái),復(fù)制和粘貼是唯一的復(fù)用方式,甚至類名變量名都來(lái)不及改。一個(gè)錯(cuò)誤在一個(gè)設(shè)備上被修正,同樣一段代碼的錯(cuò)誤在其他設(shè)備上卻來(lái)不及修改。而隨著團(tuán)隊(duì)規(guī)模的擴(kuò)大,甚至MVC的基本架構(gòu)在一些新設(shè)備上都沒(méi)能遵守。
最終框架被引入了這個(gè)系列的產(chǎn)品??蚣艽_定了如下內(nèi)容:
-
MVC模式的基本架構(gòu)
-
窗口管理器和組件布局算法
-
多國(guó)語(yǔ)言方案(字符串管理器)
-
日志系統(tǒng)
-
內(nèi)存分配器和內(nèi)存泄露檢測(cè)
8.5. 遠(yuǎn)程控制
客戶希望將設(shè)備固定安放在網(wǎng)絡(luò)的某個(gè)位置,作為“探針”使用,在辦公室通過(guò)遠(yuǎn)程控制來(lái)訪問(wèn)這個(gè)設(shè)備。這對(duì)于原本是作為純手持設(shè)備設(shè)計(jì)的系統(tǒng)又是一個(gè)挑戰(zhàn)。幸運(yùn)的是,MVC架構(gòu)具有相當(dāng)?shù)膹椥?,早期的投入獲得了回報(bào)。
TL1 Server 對(duì)外提供基于Telnet的遠(yuǎn)程控制接口。在系統(tǒng)內(nèi)部,它的位置相當(dāng)于View,只和原有的Controller和DataCenter通訊。
8.6. 自動(dòng)化的TL1解釋器
由于TL1命令相當(dāng)多,而TL1又往往不是客戶的第一需求,很多設(shè)備的TL1命令開(kāi)始不完整。究其原因,還是手寫(xiě)TL1命令的解釋器太累。后來(lái)通過(guò)引入Bison和Flex,這個(gè)問(wèn)題有所改善,但還是不足。自動(dòng)化代碼生成在這個(gè)階段被引入。通過(guò)以如下的格式定義TL1,工具可以自動(dòng)生成TL1的編碼和解碼器代碼。
CMD_NAME
{
cmd = “SET-TIME-CONFIG::::,,,,,[]”
year = 1970..2100
month = 1..12
day = 1..31
hour = 0..23
minute = 0..59
second = 0..59
}
8.7. 測(cè)試的難題
經(jīng)過(guò)數(shù)十年的積累,產(chǎn)品已經(jīng)成為一個(gè)系列,幾十種設(shè)備。大部分設(shè)備進(jìn)入了維護(hù)期,經(jīng)常有客戶提一些小的改進(jìn),或者要求修正一下缺陷。繁重的手工回歸測(cè)試成為了噩夢(mèng)。
基于TL1的自動(dòng)化測(cè)試極大的解放了測(cè)試人員。通過(guò)在PC上運(yùn)行的測(cè)試腳本,回歸測(cè)試變得簡(jiǎn)單而可靠。唯一不足的是界面部分無(wú)法驗(yàn)證。
基于Test Quest的自動(dòng)化工具需要在設(shè)備運(yùn)行的pSOS系統(tǒng)上開(kāi)發(fā)一個(gè)類似遠(yuǎn)程桌面的軟件,而這在pSOS上并非易事。不過(guò)好消息是,由于框架固定了界面的風(fēng)格和布局算法,基于Test Quest的自動(dòng)化工具會(huì)有很高的識(shí)別效率。
8.8. 小結(jié)
從這個(gè)實(shí)際的嵌入式產(chǎn)品重構(gòu)的歷程可以看出,第三步引入MVC模式和第四步的框架化是非常關(guān)鍵的。成熟的MVC模式保證了后續(xù)一系列的可擴(kuò)充性,而框架則保證了這個(gè)架構(gòu)的在所有產(chǎn)品中的準(zhǔn)確重用。
9. 總結(jié)
本文是針對(duì)嵌入式軟件開(kāi)發(fā)的特點(diǎn),討論架構(gòu)設(shè)計(jì)的思路和方法。試圖給大家提供一種思想,啟發(fā)大家的思維。框架,自動(dòng)化代碼生成和測(cè)試驅(qū)動(dòng)的架構(gòu)是核心內(nèi)容,其中框架又是貫穿始終的要素。有人問(wèn)我,什么是架構(gòu)師,怎么樣才能成為架構(gòu)師?我回答說(shuō):編碼,編碼,再編碼;改錯(cuò),改錯(cuò),再改錯(cuò)。當(dāng)你覺(jué)得厭煩的時(shí)候,停下來(lái)想想,怎么才能更快更好的完成這些工作?架構(gòu)師就是在實(shí)踐中產(chǎn)生的,架構(gòu)師來(lái)自于那些勤于思考,懶于重復(fù)的人。
-
嵌入式系統(tǒng)
+關(guān)注
關(guān)注
41文章
3569瀏覽量
129246 -
自動(dòng)化
+關(guān)注
關(guān)注
29文章
5519瀏覽量
79113 -
智能汽車
+關(guān)注
關(guān)注
30文章
2784瀏覽量
107150
原文標(biāo)題:深度:智能汽車-嵌入式系統(tǒng)的軟件架構(gòu)設(shè)計(jì)!
文章出處:【微信號(hào):智能汽車電子與軟件,微信公眾號(hào):智能汽車電子與軟件】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論