1. 前言
嵌入式是軟件設(shè)計領(lǐng)域的一個分支,它自身的諸多特點決定了系統(tǒng)架構(gòu)師的選擇,同時它的一些問題又具有相當?shù)耐ㄓ眯?,可以推廣到其他的領(lǐng)域。
提起嵌入式軟件設(shè)計,傳統(tǒng)的印象是單片機,匯編,高度依賴硬件。傳統(tǒng)的嵌入式軟件開發(fā)者往往只關(guān)注實現(xiàn)功能本身,而忽視諸如代碼復用,數(shù)據(jù)和界面分離,可測試性等因素。從而導致嵌入式軟件的質(zhì)量高度依賴開發(fā)者的水平,成敗系之一身。隨著嵌入式軟硬件的飛速發(fā)展,今天的嵌入式系統(tǒng)在功能,規(guī)模和復雜度各方面都有了極大的提升。比如,Marvell公司的PXA3xx系列的最高主頻已經(jīng)達到800Mhz,內(nèi)建USB,WIFI,2D圖形加速,32位DDR內(nèi)存。在硬件上,今天的嵌入式系統(tǒng)已經(jīng)達到甚至超過了數(shù)年前的PC平臺。在軟件方面,完善的操作系統(tǒng)已經(jīng)成熟,比如Symbian, Linux, WinCE。基于完善的操作系統(tǒng),諸如字處理,圖像,視頻,音頻,游戲,網(wǎng)頁瀏覽等各種應(yīng)用程序?qū)映霾桓F,其功能性和復雜度比諸PC軟件不遑多讓。原來多選用專用硬件和專用系統(tǒng)的一些商業(yè)設(shè)備公司也開始轉(zhuǎn)換思路,以出色而廉價的硬件和完善的操作系統(tǒng)為基礎(chǔ),用軟件的方式代替以前使用專有硬件實現(xiàn)的功能,從而實現(xiàn)更低的成本和更高的可變更,可維護性。
2.決定架構(gòu)的因素和架構(gòu)的影響
架構(gòu)不是一個孤立的技術(shù)的產(chǎn)物,它受多方面因素的影響。同時,一個架構(gòu)又對軟件開發(fā)的諸多方面造成影響。
下面舉一個具體的例子。
摩托車的發(fā)動機在出廠前必須通過一系列的測試。在流水線上,發(fā)動機被送到每個工位上,由工人進行諸如轉(zhuǎn)速,噪音,振動等方面的測試。要求實現(xiàn)一個嵌入式設(shè)備,具備以下基本功能:
安裝在工位上,工人上班前開啟并登錄。
通過傳感器自動采集測試數(shù)據(jù),并顯示在屏幕上。
記錄所有的測試結(jié)果,并提供統(tǒng)計功能。比如次品率。
如果你是這個設(shè)備的架構(gòu)師,哪些問題是在設(shè)計架構(gòu)的時候應(yīng)該關(guān)注的呢?
2.1. 常見的誤解
2.1.1. 小型的系統(tǒng)不需要架構(gòu)
有相當多的嵌入式系統(tǒng)規(guī)模都較小,一般是為了某些特定的目的而設(shè)計的。受工程師認識,客戶規(guī)模和項目進度的影響,經(jīng)常不做任何架構(gòu)設(shè)計,直接以實現(xiàn)功能為目標進行編碼。這種行為表面上看滿足了進度,成本,功能各方面的需求,但是從長遠來看,在擴展和維護上付出的成本,要遠遠高于最初節(jié)約的成本。如果系統(tǒng)的最初開發(fā)者繼續(xù)留在組織內(nèi)并負責這個項目,那么可能一切都會正常,一旦他離開,后續(xù)者因為對系統(tǒng)細節(jié)的理解不足,就可能引入更多的錯誤。要注意,嵌入式系統(tǒng)的變更成本要遠遠高于一般的軟件系統(tǒng)。好的軟件架構(gòu),可以從宏觀和微觀的不同層次上描述系統(tǒng),并將各個部分隔離,從而使新特性的添加和后續(xù)維護變得相對簡單。
舉一個城鐵刷卡機的例子,這個例子在前面的課程中出現(xiàn)過。簡單的城鐵刷卡機只需要實現(xiàn)如下功能:
一個While循環(huán)足以實現(xiàn)這個系統(tǒng),直接就可以開始編碼調(diào)試。但是從一個架構(gòu)師的角度,這里有沒有值得抽象和剝離的部分呢?
計費系統(tǒng)。計費系統(tǒng)是必須抽象的,比如從單次計費到按里程計費。
傳感器系統(tǒng)。傳感器包括磁卡感應(yīng)器,投幣器等。設(shè)備可能更換。
故障處理和恢復??紤]到較高的可靠性和較短的故障恢復時間,這部分有必要單獨設(shè)計。
未來很可能出現(xiàn)的需求變更:
操作界面。是否需要抽象出專門的Model來?以備將來實現(xiàn)View。
數(shù)據(jù)統(tǒng)計。是否需要引入關(guān)系型數(shù)據(jù)庫?
如果直接以上面的流程圖編碼,當出現(xiàn)變更后,有多少代碼可以復用?
不過,也不要因此產(chǎn)生過度的設(shè)計。架構(gòu)應(yīng)當立足滿足當前需求,并適當?shù)目紤]重用和變更。
2.1.2. 敏捷開發(fā)不需要架構(gòu)
極限編程,敏捷開發(fā)的出現(xiàn)使一些人誤以為軟件開發(fā)無需再做架構(gòu)了。這是一個很大的誤解。敏捷開發(fā)是在傳統(tǒng)瀑布式開發(fā)流程出現(xiàn)明顯弊端后提出的解決方案,所以它必然有一個更高的起點和對開發(fā)更嚴格的要求。而不是倒退到石器時代。事實上,架構(gòu)是敏捷開發(fā)的一部分,只不過在形式上,敏捷開發(fā)推薦使用更高效,簡單的方式來做設(shè)計。比如畫在白板上然后用數(shù)碼相機拍下的UML圖;用用戶故事代替用戶用例等。測試驅(qū)動的敏捷開發(fā)更是強迫工程師在寫實際代碼前設(shè)計好組件的功能和接口,而不是直接開始寫代碼。敏捷開發(fā)的一些特征:
針對比傳統(tǒng)開發(fā)流程更大的系統(tǒng)
承認變化,迭代架構(gòu)
簡潔而不混亂
強調(diào)測試和重構(gòu)
2. 嵌入式環(huán)境下軟件設(shè)計的特點
要談嵌入式的軟件架構(gòu),首先必須了解嵌入式軟件設(shè)計的特點。
2.1. 和硬件密切相關(guān)
嵌入式軟件普遍對硬件有著相當?shù)囊蕾囆?。這體現(xiàn)在幾個方面:
一些功能只能通過硬件實現(xiàn),軟件操作硬件,驅(qū)動硬件。
硬件的差異/變更會對軟件產(chǎn)生重大影響。
沒有硬件或者硬件不完善時,軟件無法運行或無法完整運行。
這些特點導致幾方面的后果:
軟件工程師對硬件的理解和熟練程度會很大程度的決定軟件的性能/穩(wěn)定性等非功能性指標,而這部分一向是相對復雜的,需要資深的工程師才能保證質(zhì)量。
軟件對硬件設(shè)計高度依賴,不能保持相對穩(wěn)定,可維護性和可重用性差
軟件不能離開硬件單獨測試和驗證,往往需要和硬件驗證同步進行,造成進度前松后緊,錯誤定位范圍擴大。
針對這些問題,有幾方面的解決思路:
用軟件實現(xiàn)硬件功能。選用更強大的處理器,用軟件來實現(xiàn)部分硬件功能,不僅可以降低對硬件的依賴,在響應(yīng)變化,避免對特定型號和廠商的依賴方面都很有好處。這在一些行業(yè)里已經(jīng)成為了趨勢。在PC平臺也經(jīng)歷了這樣的過程,比如早期的漢卡。
將對硬件的依賴獨立成硬件抽象層,盡可能使軟件的其他部分硬件無關(guān),并可以脫離硬件運行。一方面將硬件變更甚至換件的風險控制在有限的范圍內(nèi),另一方面提高軟件部分的可測試性。
2.2. 穩(wěn)定性要求高
大部分嵌入式軟件都對程序的長期穩(wěn)定運行有較高的要求。比如手機經(jīng)常幾個月開機,通訊設(shè)備則要求24*7正常運行,即使是通訊上的測試設(shè)備也要求至少正常運行8小時。為了穩(wěn)定性的目標,有一些比較常用的設(shè)計手段:
將不同的任務(wù)分布在獨立的進程中。良好的模塊化設(shè)計是關(guān)鍵
Watch Dog, Heart beat,重新啟動失效的進程。
完善而統(tǒng)一的日志系統(tǒng)以快速定位問題。嵌入式設(shè)備一般缺乏有力的調(diào)試器,日志系統(tǒng)尤其重要。
將錯誤孤立在最小的范圍內(nèi),避免錯誤的擴散和連鎖反應(yīng)。核心代碼要經(jīng)過充分的驗證,對非核心代碼,可以在監(jiān)控或者沙盒中運行,避免其破壞整個系統(tǒng)。
舉例,Symbian上的GPRS訪問受不同硬件和操作系統(tǒng)版本影響,功能不是非常穩(wěn)定。其中有一個版本上當關(guān)閉GPRS連接時一定會崩潰,而且屬于known issue。將GPRS連接,HTTP協(xié)議處理,文件下載等操作獨立到一個進程中,雖然每次操作完畢該進程都會崩潰,對用戶卻沒有影響。
雙備份這樣的手段較少采用
2.3. 內(nèi)存不足
雖然當今的嵌入式系統(tǒng)的內(nèi)存比之以K計數(shù)的時代已經(jīng)有了很大的提高,但是隨著軟件規(guī)模的增長,內(nèi)存不足的問題依然時時困擾著系統(tǒng)架構(gòu)師。有一些原則,架構(gòu)師在進行設(shè)計決策的時候可以參考:
2.3.1. 虛擬內(nèi)存技術(shù)
有一些嵌入式設(shè)備需要處理巨大的數(shù)據(jù)量,而這些數(shù)據(jù)不可能全部裝入內(nèi)存中。一些嵌入式操作系統(tǒng)不提供虛擬內(nèi)存技術(shù),比如WinCE4.2每個程序最多只能使用32M內(nèi)存。對這樣的應(yīng)用,架構(gòu)師應(yīng)該特別設(shè)計自己的虛擬內(nèi)存技術(shù)。所謂的虛擬內(nèi)存技術(shù)的核心是,將暫時不太可能使用的數(shù)據(jù)移出內(nèi)存。這涉及到一些技術(shù)點:
引用計數(shù),正在使用的數(shù)據(jù)不能移出。
使用預測,預測下一個階段某個數(shù)據(jù)的使用可能性。基于預測移出數(shù)據(jù)或者提前裝入數(shù)據(jù)。
占位數(shù)據(jù)/對象。
高速緩存。在復雜數(shù)據(jù)結(jié)果下緩存高頻率使用的數(shù)據(jù),直接訪問。
快速的持久化和裝載。
下圖是一個全國電信機房管理系統(tǒng)的界面示意圖:
每個節(jié)點下都有大量的數(shù)據(jù)需要裝載,可以使用上述技術(shù)將內(nèi)存占用降到最低。
2.3.2. 兩段式構(gòu)造
在內(nèi)存有限的系統(tǒng)里,對象構(gòu)造失敗是必須要處理的問題,失敗的原因中最常見的則是內(nèi)存不足(實際上這也是對PC平臺的要求,但是在實際中往往忽略,因為內(nèi)存實在便宜)。兩段式構(gòu)造就是一種常用而有效的設(shè)計。舉例來說:
CMySimpleClass: class CMySimpleClass { public: CMySimpleClass(); ~CMySimpleClass(); ... private: int SomeData; }; CMyCompoundClass: class CMyCompoundClass { public: CMyCompoundClass(); ~CMyCompoundClass(); ... private: CMySimpleClass* iSimpleClass; }; 在CMyCompoundClass的構(gòu)造函數(shù)里初始化iSimpleClass對象。 CMyCompoundClass::CMyCompoundClass() { iSimpleClass = new CMySimpleClass; }
當創(chuàng)建CMyCompoundClass的時候會發(fā)生什么呢?
CMyCompoundClass* myCompoundClass = new CMyCompoundClass;
為CMyCompoundClass的對象分配內(nèi)存
調(diào)用CMyCompoundClass對象的構(gòu)造函數(shù)
在構(gòu)造函數(shù)中創(chuàng)建一個CMySimpleClass的實例
構(gòu)造函數(shù)結(jié)束返回
一切看起來都很簡單,但是如果第三步創(chuàng)建CMySimpleClass對象的時候發(fā)生內(nèi)存不足的錯誤怎么辦呢?構(gòu)造函數(shù)無法返回任何錯誤信息以提示調(diào)用者構(gòu)造沒有成功。調(diào)用者于是獲得了一個指向CMyCompoundClass的指針,但是這個對象并沒有構(gòu)造完整。
如果在構(gòu)造函數(shù)中拋出異常會怎么樣呢?這是個著名的噩夢,因為析構(gòu)函數(shù)不會被調(diào)用,在創(chuàng)建CMySimpleClass對象之前如果分配了資源就會泄露。關(guān)于在構(gòu)造函數(shù)中拋出異常可以單講一個小時,但是有一個建議是:盡量避免在構(gòu)造函數(shù)中拋出異常。
所以,使用兩段式構(gòu)造法是一個更好的選擇。簡單的說,就是在構(gòu)造函數(shù)避免任何可能產(chǎn)生錯誤的動作,比如分配內(nèi)存,而把這些動作放在構(gòu)造完成之后,調(diào)用另一個函數(shù)。比如:
AddressBook* book = new AddressBook() If(!book->Construct()) { delete book; book = NULL; }
這樣可以保證當Construct不成功的時候釋放已經(jīng)分配的資源。
在最重要的手機操作系統(tǒng)Symbian上,二段式構(gòu)造法普遍使用。
2.3.3. 內(nèi)存分配器
不同的系統(tǒng)有著不同的內(nèi)存分配的特點。有些要求分配很多小內(nèi)存,有的則需要經(jīng)常增長已經(jīng)分配的內(nèi)存。一個好的內(nèi)存分配器對嵌入式的軟件的性能有時具有重大的意義。應(yīng)該在系統(tǒng)設(shè)計時保證整個系統(tǒng)使用統(tǒng)一的內(nèi)存分配器,并且可以隨時更換。
2.3.4. 內(nèi)存泄漏
內(nèi)存泄漏對嵌入式系統(tǒng)有限的內(nèi)存是非常嚴重的。通過使用自己的內(nèi)存分配器,可以很容易的跟蹤內(nèi)存的分配釋放情況,從而檢測出內(nèi)存泄漏的情況。
2.4. 處理器能力有限,性能要求高
這里不討論實時系統(tǒng),那是一塊很大的專業(yè)話題。對一般的嵌入式系統(tǒng)而言,由于處理器能力有限,要特別注意性能的問題。一些很好的架構(gòu)設(shè)計由于不能滿足性能要求,最終導致整個項目的失敗。
2.4.1. 抵御新技術(shù)的誘惑
架構(gòu)師必須明白,新技術(shù)常常意味著復雜和更低的性能。即使這不是絕對的,由于嵌入式系統(tǒng)硬件性能所限,彈性較低。一旦發(fā)現(xiàn)新技術(shù)有和當初設(shè)想不同之處,就更難通過修改來適應(yīng)。比如GWT技術(shù)。這是Google推出的Ajax開發(fā)工具,它可以讓程序員像開發(fā)一個桌面應(yīng)用程序一樣開發(fā)Web的Ajax程序。這使得在嵌入式系統(tǒng)上用一套代碼實現(xiàn)遠程和本地操作界面成為了很容易的一件事。但是在嵌入式設(shè)備上運行B-S結(jié)構(gòu)的應(yīng)用,性能上是一個很大的挑戰(zhàn)。同時,瀏覽器兼容方面的問題也很嚴重,GWT目前的版本還不夠完善。
事實證明,嵌入式的遠程控制方案還是要采用Activex,VNC或者其他的方案。
2.4.2. 不要有太多的層次
分層結(jié)構(gòu)有利于清晰的劃分系統(tǒng)職責,實現(xiàn)系統(tǒng)的解耦,但是每多一個層次,就意味著性能的一次損失。尤其是當層和層之間需要傳遞大量數(shù)據(jù)的時候。對嵌入式系統(tǒng)而言,在采用分層結(jié)構(gòu)時要控制層次數(shù)量,并且盡量不要傳遞大量數(shù)據(jù),尤其是在不同進程的層次之間。如果一定要傳遞數(shù)據(jù),要避免大量的數(shù)據(jù)格式轉(zhuǎn)換,如XML到二進制,C++結(jié)構(gòu)到Python結(jié)構(gòu)。
嵌入式系統(tǒng)能力有限,一定要將有限的能力用在系統(tǒng)的核心功能上。
2.5. 存儲設(shè)備易損壞,速度較慢
受體積和成本的限制,大部分的嵌入式設(shè)備使用諸如Compact Flash, SD, mini SD, MMC等作為存儲設(shè)備。這些設(shè)備雖然有著不擔心機械運動損壞的優(yōu)點,但是其本身的使用壽命都比較短暫。比如,CF卡一般只能寫100萬次。而SD更短,只有10萬次。對于像數(shù)碼相機這樣的應(yīng)用,也許是足夠的。但是對于需要頻繁擦寫磁盤的應(yīng)用,比如歷史數(shù)據(jù)庫,磁盤的損壞問題會很快顯現(xiàn)。比如有一個應(yīng)用式每天向CF卡上寫一個16M的文件,文件系統(tǒng)是FAT16, 每簇大小是2K,那么寫完這個16M的文件,分區(qū)表需要寫8192次,于是一個100萬次壽命的CF實際能夠工作的時間是1000000/8192 = 122天。而損壞的時候,CF卡的其他絕大部分地方的使用次數(shù)不過萬分之一。
除了因為靜態(tài)的文件分區(qū)表等區(qū)塊被頻繁的讀寫而提前損壞,一些嵌入式設(shè)備還要面對直接斷電的挑戰(zhàn),這會在存儲設(shè)備上產(chǎn)生不完整的數(shù)據(jù)。
2.5.1. 損耗均衡
損耗均衡的基本思路是平均地使用存儲器上的各個區(qū)塊。需要維護一張存儲器區(qū)塊使用情況的表,這個表包括區(qū)塊的偏移位置,當前是否可用,以及已經(jīng)擦寫地次數(shù)。當有新的擦寫請求的時候,根據(jù)以下原則選擇區(qū)塊:
盡量連續(xù)
擦寫次數(shù)最少
即使是更新已經(jīng)存在的數(shù)據(jù),也會使用以上原則分配新的區(qū)塊。同樣,這張表的存放位置也不能是固定不變的,否則這張表所占據(jù)的區(qū)塊就會最先損壞。當要更新這張表的時候,同樣要使用以上算法分配區(qū)塊。
如果存儲器上有大量的靜態(tài)數(shù)據(jù),那么上述算法就只能針對剩下的空間生效,這種情況下還要實現(xiàn)對這些靜態(tài)數(shù)據(jù)的搬運的算法。但是這種算法會降低寫操作的性能,也增加了算法的復雜度。一般都只使用動態(tài)均衡算法。
目前比較成熟的損耗均衡的文件系統(tǒng)有JFFS2, 和 YAFFS。也有另一種思路就是在FAT16等傳統(tǒng)文件系統(tǒng)上實現(xiàn)損耗均衡,只要事先分配一塊足夠大的文件,在文件內(nèi)部實現(xiàn)損耗均衡算法。不過必須修改FAT16的代碼,關(guān)閉對最后修改時間的更新。
現(xiàn)在的CF卡和SD卡有的已經(jīng)在內(nèi)部實現(xiàn)了損耗均衡,這種情況下就不需要軟件實現(xiàn)了。
2.5.2. 錯誤恢復
如果在向存儲器寫數(shù)據(jù)的時候發(fā)生斷電或者被拔出,那么所寫的區(qū)域的數(shù)據(jù)就處于未知的狀態(tài)。在一些應(yīng)用中,這會導致不完整的文件,而在另一些應(yīng)用中,則會導致系統(tǒng)失敗。所以對這類錯誤的恢復也是嵌入式軟件設(shè)計必須考慮的。常用的思路有兩種:
日志型的文件系統(tǒng)
這種文件系統(tǒng)并不是直接存儲數(shù)據(jù),而是一條條的日志,所以當發(fā)生斷電的時候,總可以恢復到之前的狀態(tài)。這類文件系統(tǒng)的代表如ext3。
雙備份
雙備份的思路更簡單,所有的數(shù)據(jù)都寫兩份。每次交替使用。文件分區(qū)表也必須是雙備份的。假設(shè)有數(shù)據(jù)塊A,A1是他的備份塊,在初始時刻和A的內(nèi)容是一致的。在分區(qū)表中,F(xiàn)指向數(shù)據(jù)塊A,F(xiàn)1是他的備份塊。當修改文件時,首先修改數(shù)據(jù)塊A1的內(nèi)容,如果此時斷電,A1的內(nèi)容錯誤,但因為F指向的是完好的A,所以數(shù)據(jù)沒有損壞。如果A1修改成功,則修改F1的內(nèi)容,如果此時斷電,因為F是完好的,所以依然沒有問題。
現(xiàn)在的Flash設(shè)備,有的已經(jīng)內(nèi)置錯誤檢測和錯誤校正技術(shù),可以保證在斷電時數(shù)據(jù)的完整。還有的包括自動的動態(tài)/靜態(tài)損耗均衡算法和壞塊處理,完全無須上層軟件額外對待,可以當作硬盤使用。所以,硬件越發(fā)達,軟件就會越可靠,技術(shù)不斷的進步,將讓我們可以把更多的精力投入到軟件功能的本身,這是發(fā)展的趨勢。
2.6. 故障成本高昂
嵌入式產(chǎn)品都是軟硬件一起銷售的給用戶的,所以這帶來了一個純軟件所不具備的問題,那就是當產(chǎn)品發(fā)生故障時,如果需要返廠才能修復,則成本就很高。嵌入式設(shè)備常見有以下的幾類故障:
a) 數(shù)據(jù)故障。由于某些原因?qū)е聰?shù)據(jù)不能讀出或者不一致。比如斷電引起的數(shù)據(jù)庫錯誤。
b) 軟件故障。軟件本身的缺陷,需要通過發(fā)布補丁程序或者新版本的軟件修正。
c) 系統(tǒng)故障。比如用戶下載了錯誤的系統(tǒng)內(nèi)核,導致系統(tǒng)無法啟動。
d) 硬件故障。這種故障只有返廠,不屬于我們的討論范圍。
針對前三類故障,要盡可能保證客戶自己,或者現(xiàn)場技術(shù)人員就可以解決。從架構(gòu)的角度考慮,如下原則可以參考:
a) 使用具備錯誤恢復能力的數(shù)據(jù)管理設(shè)計。當數(shù)據(jù)發(fā)生錯誤時,用戶可以接受的處理依次是:
i. 錯誤被糾正,所有數(shù)據(jù)有效
ii. 錯誤發(fā)生時的數(shù)據(jù)(可能不完整)丟失,之前的數(shù)據(jù)有效。
iii. 所有數(shù)據(jù)丟失
iv. 數(shù)據(jù)引擎崩潰無法繼續(xù)工作
一般而言,滿足第二個條件即可。(日志,事務(wù),備份,錯誤識別)
b) 將應(yīng)用程序和系統(tǒng)分離。應(yīng)用程序應(yīng)該放置在可插拔的Flash卡上,可以通過讀卡器進行文件復制升級。非必要的情況不要使用專用應(yīng)用軟件來升級應(yīng)用程序。
c) 要有“安全模式”。即當主系統(tǒng)被損壞后,設(shè)備依然可以啟動,重新升級系統(tǒng)。常見的uboot可以保證這一點,在系統(tǒng)損壞后,可以進入uboot通過tftp重新升級。
3. 軟件框架
在桌面系統(tǒng)和網(wǎng)絡(luò)系統(tǒng)上,框架是普遍應(yīng)用的,比如著名的ACE, MFC, Ruby On Rails等。而在嵌入式系統(tǒng)中,框架則是很少使用的。究其原因,大概是認為嵌入式系統(tǒng)簡單,沒有重復性,過于注重功能的實現(xiàn)和性能的優(yōu)化。在前言中我們已經(jīng)提到,現(xiàn)在的嵌入式發(fā)展趨勢是向著復雜化,大型化,系列化發(fā)展的。所以,在嵌入式下設(shè)計軟件框架也是很有必要,也很有價值的。
3.1. 嵌入式軟件架構(gòu)面臨的問題
前面我們講到,嵌入式系統(tǒng)軟件架構(gòu)所面臨的一些問題,其中很重要的一點是,對硬件的依賴和硬件相關(guān)軟件的復雜性。還包括嵌入式軟件在穩(wěn)定性和內(nèi)存占用等方面的苛刻要求。如果團隊中的每個人都是這些方面高手的話,也許有可能開發(fā)出高質(zhì)量的軟件,但事實是一個團隊中可能只有一兩個資深人員,其他大部分都是初級工程師。人人都去和硬件打交道,都負責穩(wěn)定性,性能等等指標的話,是很難保證最終產(chǎn)品質(zhì)量的。如果組件團隊時都是精通硬件等底層技術(shù)的人才,又很難設(shè)計出在可用性,擴展性等方面出色的軟件。術(shù)業(yè)有專攻,架構(gòu)師的選擇決定著團隊的組成方式。
同時,嵌入式軟件開發(fā)雖然復雜,但是也存在大量的重用的可能性。如何重用,又如何應(yīng)對將來的變更?
所以,如何將復雜性對大多數(shù)人屏蔽,如何將關(guān)注點分離,如何保證系統(tǒng)的關(guān)鍵非功能指標,是嵌入式軟件架構(gòu)設(shè)計師應(yīng)該解決的問題。一種可能的解決方案就是軟件框架。
3.2. 什么是框架
框架是在一個給定的問題領(lǐng)域內(nèi),為了重用和應(yīng)對未來需求變化而設(shè)計的軟件半成品??蚣軓娬{(diào)對特定領(lǐng)域的抽象,包含大量的專業(yè)領(lǐng)域知識,以縮短軟件的開發(fā)周期,提高軟件質(zhì)量為目的。使用框架的二次開發(fā)者通過重寫子類或組裝對象的方式來實現(xiàn)特殊的功能。
3.2.1. 軟件復用的層次
復用是在我們經(jīng)常談到的話題,“不要重復發(fā)明輪子”也是耳熟能詳?shù)慕錀l。不過對于復用的理解實際上是有很多個層次的。
最基礎(chǔ)的復用是復制粘貼。某個功能以前曾經(jīng)實現(xiàn)過,再次需要的時候就復制過來,修改一下就可以使用。經(jīng)驗豐富的程序員一般都會有自己的程序庫,這樣他們實現(xiàn)的時候就會比新的程序員快。復制粘貼的缺點是代碼沒有經(jīng)過抽象,往往并不完全的適用,所以需要進行修改,經(jīng)過多次復用后,代碼將會變得混亂,難以理解。很多公司的產(chǎn)品都有這個問題,一個產(chǎn)品的代碼從另一個產(chǎn)品復制而來,修改一下就用,有時候甚至類名變量名都不改。按照“只有為復用設(shè)計的代碼才能真正復用”的標準,這稱不上是復用,或者說是低水平的復用。
更高級的復用是則是庫。這種功能需要對經(jīng)常使用的功能進行抽象,提取出其中恒定不變的部分,以庫的形式提供給二次開發(fā)程序員使用。因為設(shè)計庫的時候不知道二次開發(fā)者會如何使用,所以對設(shè)計者有著很高的要求。這是使用最廣泛的一種復用,比如標準C庫,STL庫?,F(xiàn)在非常流行的Python語言的重要優(yōu)勢之一就是其庫支持非常廣泛,相反C++一直缺少一個強大統(tǒng)一的庫支持,成為短板。在公司內(nèi)部的開發(fā)中總結(jié)常用功能并開發(fā)成庫是很有價值的,缺點是對庫的升級會影響到很多的產(chǎn)品,必須慎之又慎。
框架是另一種復用。和庫一樣,框架也是對系統(tǒng)中不變的部分進行抽象并加以實現(xiàn),由二次開發(fā)者實現(xiàn)其他變化的部分。典型的框架和庫的最大的區(qū)別是,庫是靜態(tài)的,由二次開發(fā)者調(diào)用的;框架是活著的,它是主控者,二次開發(fā)者的代碼必須符合框架的設(shè)計,由框架決定在何時調(diào)用。
舉個例子,一個網(wǎng)絡(luò)應(yīng)用總是要涉及到連接的建立,數(shù)據(jù)收發(fā)和連接的關(guān)閉。以庫的形式提供是這樣的:
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(); };
框架會在“適當”的時機創(chuàng)建mycomm對象,并查詢host和port,然后建立連接。在連接建立后,調(diào)用onconnected()接口,給二次開發(fā)者提供進行處理的機會。當數(shù)據(jù)到達的時候調(diào)用ondataarrived接口讓二次開發(fā)者處理。這是好萊塢原則,“不要來找我們,我們會去找你”。
當然,一個完整的框架通常也要提供各種庫供二次開發(fā)者使用。比如MFC提供了很多的庫,如CString, 但本質(zhì)上它是一個框架。比如實現(xiàn)一個對話框的OnInitDialog接口,就是由框架規(guī)定的。
3.2.2. 針對高度特定領(lǐng)域的抽象
和庫比較起來,框架是更針對特定領(lǐng)域的抽象。庫,比如C庫,是面向所有的應(yīng)用的。而框架相對來說則要狹窄的多。比如MFC提供的框架只適合于Windows平臺的桌面應(yīng)用程序開發(fā),ACE則是針對網(wǎng)絡(luò)應(yīng)用開發(fā)的框架,Ruby On Rails是為快速開發(fā)web站點設(shè)計的。
越是針對特定的領(lǐng)域,抽象就可以做的越強,二次開發(fā)就可以越簡單,因為共性的東西越多。比如我們上面談到嵌入式系統(tǒng)軟件開發(fā)的諸多特點,這就是特定領(lǐng)域的共性,就屬于可以抽象的部分。具體到實際的嵌入式應(yīng)用,又會有更多的共性可以抽象。
框架的設(shè)計目的是總結(jié)特定領(lǐng)域的共性,以框架的方式實現(xiàn),并規(guī)定二次開發(fā)者的實現(xiàn)方式,從而簡化開發(fā)。相應(yīng)的,針對一個領(lǐng)域開發(fā)的框架就不能服務(wù)于另一個領(lǐng)域。對企業(yè)而言,框架是一種極好的積累知識,降低成本的技術(shù)手段。
3.2.3. 解除耦合和應(yīng)對變化
框架設(shè)計的一個重要目的就是應(yīng)對變化。應(yīng)對變化的本質(zhì)就是解耦。從架構(gòu)師的角度看,解耦可以分為三種:
邏輯解耦。邏輯解耦是將邏輯上不同的模塊抽象并分離處理。如數(shù)據(jù)和界面的解耦。這也是我們最常做的解耦。
知識解耦。知識解耦是通過設(shè)計讓掌握不同知識的人僅僅通過接口工作。典型的如測試工程師所掌握的專業(yè)知識和開發(fā)工程師所掌握的程序設(shè)計和實現(xiàn)的知識。傳統(tǒng)的測試腳本通常是將這二者合二為一的。所以要求測試工程師同時具備編程的能力。通過適當?shù)姆绞剑梢宰寽y試工程師以最簡單的方式實現(xiàn)他的測試用例,而開發(fā)人員編寫傳統(tǒng)的程序代碼來執(zhí)行這些用例。
變與不變的解耦。這是框架的重要特征??蚣芡ㄟ^對領(lǐng)域知識的分析,將共性,也就是不變的內(nèi)容固定下來,而將可能發(fā)生變化的部分交給二次開發(fā)者實現(xiàn)。
3.2.4. 框架可以實現(xiàn)和規(guī)定非功能性需求
非功能性需求是指如性能,可靠性,可測試性,可移植性等。這些特性可以通過框架來實現(xiàn)。以下我們一一舉例。
性能。對性能的優(yōu)化最忌諱的就是普遍優(yōu)化。系統(tǒng)的性能往往取決于一些特定的點。比如在嵌入式系統(tǒng)中,對存儲設(shè)備的訪問是比較慢的。如果開發(fā)者不注意這方面的問題,頻繁的讀寫存儲設(shè)備,就會造成性能下降。如果對存儲設(shè)備的讀寫由框架設(shè)計,二次開發(fā)者只作為數(shù)據(jù)的提供和處理者,那么就可以在框架中對讀寫的頻率進行調(diào)節(jié),從而達到優(yōu)化性能的目的。由于框架都是單獨開發(fā)的,完成后供廣泛使用,所以就有條件對關(guān)鍵的性能點進行充分的優(yōu)化。
可靠性。以上面的網(wǎng)絡(luò)通訊程序為例,由于框架負責了連接的創(chuàng)建和管理,也處理了各種可能的網(wǎng)絡(luò)錯誤,具體的實現(xiàn)者無須了解這方面的知識,也無須實現(xiàn)這方面錯誤處理的代碼,就可以保證整個系統(tǒng)在網(wǎng)絡(luò)通訊方面的可靠性。以框架的方式設(shè)計在可靠性方面的最大優(yōu)勢就是:二次開發(fā)的代碼是在框架的掌控之內(nèi)運行的。一方面框架可以將容易出錯的部分實現(xiàn),另一方面對二次開發(fā)的代碼產(chǎn)生的錯誤也可以捕獲和處理。而庫則不能代替使用者處理錯誤。
可測試性??蓽y試性是軟件架構(gòu)需要考慮的一個重要方面。下面的章節(jié)會講到,軟件的可測試性是由優(yōu)良的設(shè)計來保證的。一方面,由于框架規(guī)定了二次開發(fā)的接口,所以可以迫使二次開發(fā)者開發(fā)出便于進行單元測試的代碼。另一方面,框架也可以在系統(tǒng)測試的層面上提供易于實現(xiàn)自動化測試和回歸測試的設(shè)計,例如統(tǒng)一提供的TL1接口。
可移植性。如果軟件的可移植性是軟件設(shè)計的目標,框架設(shè)計者可以在設(shè)計階段來保證這一點。一種方式是通過跨平臺的庫來屏蔽系統(tǒng)差異,另一種可能的方式更加極端,基于框架的二次開發(fā)可以是腳本化的。組態(tài)軟件是這方面的一個例子,在PC上組態(tài)的工程,也可以在嵌入式設(shè)備上運行。
3.3. 一個框架設(shè)計的實例
3.3.1. 基本架構(gòu)
3.3.2. 功能特點
上面是一個產(chǎn)品系列的架構(gòu)圖,其特點是硬件部分是模塊化的,可以隨時插拔。不同的硬件應(yīng)用于不同的通訊測試場合。比如光通訊測試,xDSL測試,Cable Modem測試等等。針對不同的硬件,需要開發(fā)不同的固件和軟件。固件層的功能主要是通過USB接口接收來自軟件的指令,并讀寫相應(yīng)的硬件接口,再進行一些計算后,將結(jié)果返回給軟件。軟件運行在WinCE平臺,除了提供一個觸摸式的圖形化界面外,還對外提供基于XML(SOAP)接口和TL1接口。為了實現(xiàn)自動化測試,還提供了基于Lua的腳本語言接口。整個產(chǎn)品系列有幾十個不同的硬件模塊,相應(yīng)的需要開發(fā)幾十套軟件。這些軟件雖然服務(wù)于不同的硬件,但是彼此之間有著高度的相似性。所以,選擇先開發(fā)一個框架,再基于框架開發(fā)具體的模塊軟件成了最優(yōu)的選擇。
3.3.3. 分析
軟件部分的結(jié)構(gòu)分析如下:
系統(tǒng)分為軟件,固件和硬件三大塊。軟件和固件運行在兩塊獨立的板子上,有各自的處理器和操作系統(tǒng)。硬件則插在固件所在的板子上,是可以替換的。
軟件和固件其實都是軟件,下面我們分別分析。
軟件
軟件的主要工作是提供各種用戶界面。包括本地圖形化界面,SOAP訪問界面,TL1訪問界面。
整個軟件部分分為五大部分:
通訊層
協(xié)議層
圖形界面
SOAP服務(wù)器
TL1服務(wù)器
通訊層要屏蔽用戶對具體通信介質(zhì)和協(xié)議的了解,無論是USB還是socket,對上層都不產(chǎn)生影響。通訊層負責提供可靠的通訊服務(wù)和適當?shù)腻e誤處理。通過配置文件,用戶可以改變所使用的通訊層。
協(xié)議層的目的是將數(shù)據(jù)進行編碼和解碼。編碼的產(chǎn)生物是可以在通訊層發(fā)送的流,按照嵌入式軟件的特點,我們選擇二進制作為流的格式。解碼的產(chǎn)生物是多種的,既有供界面使用的C Struct,也可以是XML數(shù)據(jù),還可以是Lua的數(shù)據(jù)結(jié)構(gòu)(tablegt)。如果需要,還可以產(chǎn)生JSON,TL1,Python數(shù)據(jù),TCL數(shù)據(jù)等等。這一層在框架中是通過機器自動生成的,我們后面會講到。
內(nèi)存數(shù)據(jù)庫,SOAP Server和TL1 Server都是協(xié)議層的用戶。圖形界面通過讀寫內(nèi)存數(shù)據(jù)庫和底層通訊。
圖形界面是框架設(shè)計的重點之一,原因是這里工作量最大,重復而無聊的工作最多。
讓我們分析一下在圖形界面開發(fā)工作中最主要的事情是什么。
收集用戶輸入的數(shù)據(jù)和命令
將數(shù)據(jù)和命令發(fā)給底層
接收底層反饋
將數(shù)據(jù)顯示在界面上
同時有一些庫用來進一步簡化開發(fā):
這是一個簡化的例子,但是很好的說明了框架的特點:
客戶代碼必須按照規(guī)定的接口實現(xiàn)
框架在適當?shù)臅r候調(diào)用客戶實現(xiàn)的接口
每個接口都被設(shè)計為只完成特定的單一功能
將各個步驟有機的串起來是框架的事,二次開發(fā)者不知道,也無須知道。
通常都要有附帶的庫。
固件
固件的主要工作是接受來自軟件的命令,驅(qū)動硬件工作;獲取硬件的狀態(tài),進行一定的計算后返回給軟件。早期的固件是很薄的一層,因為絕大部分工作是由硬件完成的,固件只起到一個中轉(zhuǎn)通訊的作用。隨著時代發(fā)展,現(xiàn)在的固件開始承擔越來越多原來由硬件完成的工作。
整個固件部分分為五大部分:
硬件抽象層,提供對硬件的訪問接口
互相獨立的任務(wù)群
任務(wù)/消息派發(fā)器
協(xié)議層
通訊層
針對不同的設(shè)備,工作量集中在硬件抽象層和任務(wù)群上。硬件抽象層是以庫的形式提供的,由對硬件最熟悉,經(jīng)驗最豐富的工程師來實現(xiàn)。任務(wù)群則由一系列的任務(wù)組成,他們分別代表不同的業(yè)務(wù)應(yīng)用。比如測量誤碼率。這部分由相對經(jīng)驗較少的工程師來實現(xiàn),他們的主要工作是實現(xiàn)規(guī)定的接口,按照標準化文檔定義的方式實現(xiàn)算法。
任務(wù)定義了如下接口,由具體開發(fā)者來實現(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ù)的實現(xiàn)者所關(guān)注的最重要的事情就是實現(xiàn)這幾個接口。其他如硬件的初始化,消息的收發(fā),編碼解碼,結(jié)果的上報等等事情都由框架進行了處理。避免了每個工程師都必須處理從上到下的所有方面。并且這樣的任務(wù)代碼還有很高的重用性,比如是在以太網(wǎng)上還是在Cable Modem上實現(xiàn)PING的算法都是一樣的。
3.3.4. 實際效果
在實際項目中,框架大大降低了開發(fā)難度。對軟件部分尤其明顯,由實習生即可完成高質(zhì)量的界面開發(fā),開發(fā)周期縮短50%以上。產(chǎn)品質(zhì)量大大提升。對固件部分的貢獻在于降低了對精通底層硬件的工程師的需要,一般的工程師熟知測量算法即可。同時,框架的存在保證了性能,穩(wěn)定和可測試性等要素。
3.4. 框架設(shè)計中的常用模式
3.4.1. 模板方法模式
模板方法模式是框架中最常用的設(shè)計模式。其根本的思路是將算法由框架固定,而將算法中具體的操作交給二次開發(fā)者實現(xiàn)。例如一個設(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ù),二次開發(fā)者創(chuàng)建一個繼承于CBaseDevice的子類,具體來實現(xiàn)這兩個接口??蚣芏x了調(diào)用的次序和錯誤的處理方式,二次開發(fā)者無須關(guān)心,也無權(quán)決定。
3.4.2. 創(chuàng)建型模式
由于框架通常都涉及到各種不同子類對象的創(chuàng)建,創(chuàng)建型模式是經(jīng)常使用的。例如一個繪圖軟件的框架,有一個基類定義了圖形對象的接口,基于它可以派生出橢圓,矩形,直線各種子類。當用戶繪制一個圖形時,框架就要實例化該子類。這時候可以用工廠方法,原型方法等等。
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ù)和界面的方式。界面開發(fā)者只需要注冊需要的數(shù)據(jù),當數(shù)據(jù)變化時框架就會將數(shù)據(jù)“推”到界面。界面開發(fā)者可以無須關(guān)注數(shù)據(jù)的來源和內(nèi)部組織形式。
消息訂閱模式最常見的問題是同步模式下如何處理重入和超時。作為框架設(shè)計者,一定要考慮好這個問題。所謂重入,是二次開發(fā)者在消息的回調(diào)函數(shù)中執(zhí)行訂閱/取消訂閱的操作,這會破壞消息訂閱的機制。所謂超時是指二次開發(fā)者的消息回調(diào)函數(shù)處理時間過長,導致其他消息無法響應(yīng)。最簡單的辦法是使用異步模式,讓訂閱者和數(shù)據(jù)發(fā)布者在獨立進程/線程中運行。如果不具備此條件,則必須作為框架的重要約定,禁止二次開發(fā)者產(chǎn)生此類問題。
3.4.4. 裝飾器模式
裝飾器模式賦予了框架在后期增加功能的能力??蚣芏x裝飾器的抽象基類,而由具體的實現(xiàn)者實現(xiàn),動態(tài)地添加到框架中。
舉一個游戲中的例子,圖形繪制引擎是一個獨立的模塊,比如可以繪制人物的靜止,跑動等圖像。如果策劃決定在游戲中增加一種叫“隱身衣”的道具,要求穿著此道具的玩家在屏幕上顯示的是若有若無的半透明圖像。應(yīng)該如何設(shè)計圖像引擎來適應(yīng)后期的游戲升級呢?
當隱身衣被裝備后,就向圖像引擎添加一個過濾器。這是個極度簡化的例子,實際的游戲引擎要比這個復雜。裝飾器模式還常見用于數(shù)據(jù)的前置和后置處理上。
3.5. 框架的缺點
一個好的框架可以大大提高產(chǎn)品的開發(fā)效率和質(zhì)量,但也有它的缺點。
框架一般都比較復雜,設(shè)計和實現(xiàn)一個好的框架需要相當?shù)臅r間。所以,一般只有在框架可以被多次反復應(yīng)用的時候適合,這時候,前提投入的成本會得到豐厚的回報。
框架規(guī)定了一系列的接口和規(guī)則,這雖然簡化了二次開發(fā)工作,但同時也要求二次開發(fā)者必須記住很多規(guī)定,如果違反了這些規(guī)定,就不能正常工作。但是由于框架屏蔽了大量的領(lǐng)域細節(jié),相對而言,其學習成本還是大大降低了的。
框架的升級對已有產(chǎn)品可能會造成嚴重的影響,導致需要完整的回歸測試。對這個問題有兩個辦法。第一是對框架本身進行嚴格的測試,有必要建立完善的單元測試庫,同時開發(fā)示例項目,用來測試框架的所有功能。第二則是使用靜態(tài)鏈接,讓已有產(chǎn)品不輕易跟隨升級。當然,如果已有產(chǎn)品有較好的回歸測試手段,就更好。
性能損失。由于框架對系統(tǒng)進行了抽象,增加了系統(tǒng)的復雜性。諸如多態(tài)這樣的手段使用也會普遍的降低系統(tǒng)的性能。但是從整體上來看,框架可以保證系統(tǒng)的性能處于一個較高的水平。
4. 自動代碼生成
4.1. 機器能做的事就不要讓人來做
懶惰是程序員的美德,更是架構(gòu)師的美德。軟件開發(fā)的過程就是人告訴機器如何做事的過程。如果一件事情機器自己就可以做,那就不要讓人來做。因為機器不僅不知疲倦,而且絕不會犯錯。我們的工作是讓客戶的工作自動化,多想一點,就能讓我們自己的工作也部分自動化。極有耐心的程序員是好的,也是不好的。
經(jīng)過良好設(shè)計的系統(tǒng),往往會出現(xiàn)很多高度類似而且具有很強規(guī)律的代碼。未經(jīng)良好設(shè)計的系統(tǒng)則可能對同一類功能產(chǎn)生很多不同的實現(xiàn)。前面關(guān)于框架設(shè)計的部分已經(jīng)證明了這一點。有時候,我們更進一步,分析出這些相似代碼之中的規(guī)律,用格式化的數(shù)據(jù)來描述這些功能,而由機器來產(chǎn)生代碼。
4.2. 舉例
4.2.1. 消息的編碼和解碼
上面關(guān)于框架的實例中,可以看到消息編解碼的部分已經(jīng)被獨立出來,和其他部分沒有耦合。加上他本身的特點,非常適合進一步將其“規(guī)則化”,用機器產(chǎn)生代碼。
編碼,就是把數(shù)據(jù)結(jié)構(gòu)流化;解碼反之。以編碼為例,代碼無非是這樣的:(二進制協(xié)議)
stream << a.i; stream << a.j; stream << a.object;
(為了簡化,這里假設(shè)已經(jīng)設(shè)計了一個流對象,可以流化各種數(shù)據(jù)類型,并且已經(jīng)處理了諸如字節(jié)序轉(zhuǎn)換等問題。)
最后我們得到一個stream。大家是否已經(jīng)習慣了寫這種代碼?但是這樣的代碼不能體現(xiàn)工程師任何的創(chuàng)造性,因為我們早已經(jīng)知道有i, 有j, 還有一個object,為什么還要自己敲入這些代碼呢?如果我們分析一下a的定義,是不是就可以自動產(chǎn)生這樣的代碼呢?
struct dataA { int i; int j; struct dataB object; };
只需要一個簡單的語義分析器解析這段代碼,得到一棵關(guān)于數(shù)據(jù)類型的樹,就可以輕易的產(chǎn)生流化的代碼。這樣的分析器用Python等字符串處理能力強的語言不過兩百行左右。關(guān)于數(shù)據(jù)類型的樹類似下圖:
只要遍歷這棵樹,就可以生成所有數(shù)據(jù)結(jié)構(gòu)的流化代碼。
在上一個框架所舉例的項目中,為一個硬件模塊自動產(chǎn)生的消息編碼解碼器代碼量高達三萬行,幾乎相當于一個小軟件。由于是自動產(chǎn)生,沒有任何錯誤,為上層提供了高可靠性。
還可以用XML或者其他的格式定義數(shù)據(jù)結(jié)構(gòu),從而產(chǎn)生自動代碼。根據(jù)需要,C++/Java/Python,任何類型的都可以。如果希望提供強檢查,可以使用XSD來定義數(shù)據(jù)結(jié)構(gòu)。有一個商業(yè)化的產(chǎn)品,xBinder,很貴,很難用,還不如自己開發(fā)。(為什么難用?因為它太通用)。除了編碼為二進制格式,還可以編碼為任何你需要的格式。我們知道二進制格式雖然效率很高,但是太難調(diào)試(當然有些人看內(nèi)存里的十六進制還是很快的),所以我們可以在編碼成二進制的同時,還生成編碼為其他可閱讀的格式的代碼,比如XML。這樣,通訊使用二進制,而調(diào)試使用XML,兩全其美。產(chǎn)生二進制的代碼大概是這樣的:
Xmlbuilder.addelement(“i”,a.i); Xmlbuilder.addelement(“j”,a.j); Xmlbuilder.addelement(“object”,a.object);
同樣也很適合機器產(chǎn)生。同樣的思路可以用來讓軟件內(nèi)嵌腳本支持。這里不多說了。(內(nèi)嵌腳本支持最大的問題是在C/C++和腳本之間交換數(shù)據(jù),也是針對數(shù)據(jù)類型的大量相似代碼。)
最近Google 發(fā)布了它的protocol buffer,就是這樣的思路。目前支持C++/Python,估計很快會支持更多的語言,大家可以關(guān)注。以后就不要再手寫編碼解碼器了。
4.2.2. GUI代碼
上面的框架設(shè)計部分,我們說到框架對界面數(shù)據(jù)收集和界面更新無能為力,只能抽象出接口,由程序員具體實現(xiàn)。但是讓我們看看這些界面程序員做的事情吧。(代碼經(jīng)過簡化,可以看作偽代碼)。
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描述界面,問題是對于復雜邏輯很難)
4.2.3. 小結(jié)
由此可見,在軟件架構(gòu)的過程中,首先要遵循一般性的原則,盡量將系統(tǒng)各個功能部分獨立出來,實現(xiàn)高內(nèi)聚低耦合,進而發(fā)現(xiàn)系統(tǒng)存在的高度重復,規(guī)律性很強的代碼,進一步將他們規(guī)則化,形式化,最后用機器來產(chǎn)生這些代碼。目前這方面最成功的應(yīng)用就是消息的編解碼。對界面代碼的自動化生成有一定局限,但也可以應(yīng)用。大家在自己的工作中要擅于發(fā)現(xiàn)這樣的可能,減少工作量,提高工作效率。
4.2.4. Google Protocol Buffer
Google剛剛發(fā)布的Protocol Buffer是使用代碼自動生成的一個典范。
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的編碼格式是二進制的,同時也提供可讀的文本格式。效率高,體積小,上下兼容。目前支持Java,Python和C++,很快會支持更多的語言。
5. 面向語言編程(LOP)
5.1. 從自動化代碼生成更進一步
面向語言編程的通俗定義是:將特定領(lǐng)域的知識融合到一種專用的計算機語言當中,從而提高人與計算機交流的效率。
自動化代碼生成其實就是面向語言編程。語言不等于是編程語言,可以是圖,也可以是表,任何可以建立人和機器之間交流渠道的都是計算機語言。軟件開發(fā)歷史上的一次生產(chǎn)率的飛躍是高級語言的發(fā)明。它讓我們以更簡潔的方式實現(xiàn)更復雜的功能。但是高級語言也有它的缺點,那就是從問題領(lǐng)域到程序指令的過程很復雜。因為高級語言是為通用目的而設(shè)計的,所以離問題領(lǐng)域很遠。舉例來說,要做一個圖形界面,我可以跟另一個工程師說:這里放一個按鈕,那邊放一個輸入框,當按下按鈕的時候,就在輸入框里顯示Hello World。我甚至可以隨手給他畫出來。
對于我和他直接的交流而言,這已經(jīng)足夠了,5分鐘。但是要讓轉(zhuǎn)變?yōu)橛嬎銠C能夠理解的語言,需要多久?
如果是匯編語言?(告訴計算機如何操作寄存器和內(nèi)存)
如果是C++? (告訴計算機如何在屏幕上繪圖,如果響應(yīng)鼠標鍵盤消息)
如果有一個不錯的圖形界面庫?(告訴計算機創(chuàng)建Button,Label對象,管理這些對象,放置這些對象,處理消息)
如果有一個不錯的開發(fā)框架+IDE? (用WYSIWYG工具繪制,設(shè)計類,類的成員變量,編寫消息響應(yīng)函數(shù))
如果有一門專門做圖形界面開發(fā)的語言?
可以是這樣的:
Label l {Text=””} Button b{Text=”ok”,action=l.Text=”hello world”}
通用的計算機語言是基于變量,類,分支,循環(huán),鏈表,消息這些概念的。這些概念離問題本身有著遙遠的距離,而且表達能力非常有限。自然語言表達能力很強,但是歧義和冗余太多,無法格式化標準化。傳統(tǒng)的思想告訴我們:計算機語言就是一條條的指令,編程就是寫下這些指令。而面向語言編程的思想是,用盡量貼近問題,貼近人的思維的辦法來描述問題,從而降低從人的思想到計算機軟件轉(zhuǎn)換的難度。
舉一個游戲開發(fā)的例子?,F(xiàn)在的網(wǎng)絡(luò)游戲普遍的采用了C++或者C開發(fā)游戲引擎。而具體的游戲內(nèi)容,則是由一系列二次開發(fā)工具和語言完成的。地圖編輯器就是一種面向游戲的語言。Lua或者類似的腳本則被嵌入到游戲內(nèi)部,用來編寫武器,技能,任務(wù)等等。Lua本身不具備獨立開發(fā)應(yīng)用程序的能力,然而游戲引擎的設(shè)計者通過給Lua提供一系列的,各種層次上的接口,將領(lǐng)域知識密集的賦予了腳本,從而大大提高了游戲二次開發(fā)的效率。網(wǎng)絡(luò)游戲的鼻祖MUD則是設(shè)計了LPC來作為游戲的開發(fā)語言。MUD的引擎MudOS和LPC之間的關(guān)系如圖:
用LPC創(chuàng)建一個NPC的代碼類似如下:
inerit 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è)余游戲開發(fā)者,甚至成為很多人進入IT行業(yè)的起點。原因就是它簡單,易理解,100%為游戲開發(fā)設(shè)計。這就是LOP的魅力。
5.2. 優(yōu)勢和劣勢
LOP最重要的優(yōu)點是將領(lǐng)域知識固化到語言中,從而:
提高開發(fā)效率。
優(yōu)化團隊結(jié)構(gòu),降低交流成本,領(lǐng)域?qū)<液统绦騿T可以更好的合作。
降低耦合,易于維護。
其次,由于LOP不是通用語言,所涉及的范圍就狹窄很多,所以:
更容易得到穩(wěn)定的系統(tǒng)
更容易移植
相應(yīng)的,LOP也有它的劣勢:
LOP對領(lǐng)域知識抽象的要求比框架更高。
開發(fā)一門新的語言本身的成本。幸好現(xiàn)在設(shè)計一門新的語言不算太難,還有Lua這樣的“專用二次開發(fā)”語言的支持。
性能損失。不過相比開發(fā)成本的節(jié)約,在非性能核心部分使用LOP還是很值得的。
5.3. 在嵌入式系統(tǒng)中的應(yīng)用
舉例,嵌入式設(shè)備的Web服務(wù)器。很多設(shè)備都提供Web服務(wù)用于配置,比如路由器,ADSL貓等等。這種設(shè)備所提供的web服務(wù)的典型用例是用戶填寫一些參數(shù),提交給Web服務(wù)器,Web 服務(wù)器將這些參數(shù)寫入硬件,并將操作結(jié)果或者其他信息生成頁面返回給瀏覽器。由于典型的Apache,Mysql,PHP組合體積太大且不容易移植,通常嵌入式系統(tǒng)的Web服務(wù)都是用C/C++直接寫就的。從socket管理,http協(xié)議到具體操作硬件,生成頁面,都一體負責。然而對于功能復雜,Web界面要求較高的情況,用C來寫頁面效率就太低了。
shttpd是一個小巧的web服務(wù)器,小巧到只有一個.c文件,4000余行代碼。雖然體積很小,卻具備了最基本的功能,比如CGI。它既可以獨立運行,也可以嵌入到其他的應(yīng)用程序當中。shttpd在大多數(shù)平臺上都可以順利編譯、運行。lua是一個小巧的腳本語言,專用于嵌入和擴展。它和C/C++代碼有著良好的交互能力。
將Lua引擎嵌入到shttpd中,再使用C編寫一個(一些)驅(qū)動硬件的擴展,注冊成為Lua的函數(shù),形成的系統(tǒng)結(jié)構(gòu)如下圖:
這樣的應(yīng)用在嵌入式系統(tǒng)中是有一定代表性的,即,以C實現(xiàn)底層核心功能,而把系統(tǒng)的易變部分以腳本實現(xiàn)。大家可以思考在自己的開發(fā)過程中是否可以使用這種技術(shù)。這是LOP的一種具體應(yīng)用模式。(沒有創(chuàng)造一種全新的語言,而是使用Lua)
6. 測試
6.1. 可測試性是軟件質(zhì)量的一個度量指標
好的軟件是設(shè)計出來的,好的軟件也一定是便于測試的。一個難于測試的軟件的質(zhì)量是難以得到保障的。在今天軟件規(guī)模越來越大的趨勢下,以下問題是普遍存在的:
測試只能手工進行,回歸測試代價極大,實際只能執(zhí)行點測,質(zhì)量無法保證
各個模塊只有集成到一起后才能測試
代碼不經(jīng)過任何單元測試就集成
這些問題的根源都在于缺乏一個良好的軟件設(shè)計。一個好的軟件設(shè)計應(yīng)該使得單元測試,模塊測試和回歸測試都變得容易,從而保證測試的廣度和深度,最終產(chǎn)生高質(zhì)量的軟件。除了功能,非功能性需求也必須是可測試的。所以,可測試性是軟件設(shè)計中一個重要的指標,是系統(tǒng)架構(gòu)師需要認真考慮的問題。
6.2. 測試驅(qū)動的軟件架構(gòu)
這里談的是測試驅(qū)動的軟件架構(gòu),而不是測試驅(qū)動的開發(fā)。TDD(Test Driven Development) 是一種開發(fā)方式,是一種編碼實踐。而測試驅(qū)動的架構(gòu)強調(diào)的是,從提高可測試性的角度進行架構(gòu)設(shè)計。軟件的測試分為多個層次:
6.3. 系統(tǒng)測試
系統(tǒng)測試是指由測試人員執(zhí)行的,驗證軟件是否完整正確的實現(xiàn)了需求的測試。這種測試中,測試人員作為用戶的角色,通過程序界面進行測試。在大部分情況下這些工作是手工完成的。在規(guī)范的流程中,這個過程通常要占到整個軟件開發(fā)時間的1/3以上。而當有新版本發(fā)布的時候,盡管只涉及了軟件的一部分,測試部門依然需要完整的測試整個軟件。這是由代碼“副作用”特點決定的。有時候修改一個bug可以引發(fā)更多的bug,破壞原來工作正常的代碼。這在測試中叫回歸測試(Regression test)。對于規(guī)模較大的軟件,回歸測試需要很長的時間,在版本新增功能和錯誤修正不多的情況下,回歸測試可以占到整個軟件開發(fā)過程了一半以上,嚴重影響了軟件的交付,也使軟件測試部門成為軟件開發(fā)流程中的瓶頸。測試過程自動化,是部分解決這個問題的辦法。
作為架構(gòu)師,有必要考慮如何實現(xiàn)軟件的可自動化測試性。
6.3.1. 界面自動化測試
在沒有圖形化界面以前,字符方式的界面是比較容易進行自動化測試的。一個編寫良好的腳本就可以實現(xiàn)輸入和對輸出的檢查。但是對于圖形化的界面,人的參與似乎變得不可缺少。有一些界面自動化的測試工具,如WinRunner, 這些工具可以記錄下測試人員的操作成為腳本,然后通過回放這些腳本,就可以實現(xiàn)操作的自動化。針對嵌入式設(shè)備,有Test Quest可以使用,通過在設(shè)備中運行一個類似遠程桌面的Agent,PC端的測試工具可以用圖像識別的方法識別出不同的組件,并發(fā)送相應(yīng)用戶的輸入。此類工具的基本工作原理如圖:
但是這個過程在實際中存在三個問題:
可靠性差,經(jīng)常中斷運行。要寫一個可靠的腳本甚至比開發(fā)軟件還要困難。比如,按下一個按鈕,有時候一個對話框立刻就出現(xiàn),有時候可能要好幾秒,有時候甚至不出現(xiàn),操作錄制工具不能自動實現(xiàn)這些判斷,而需要手動修改。
對操作結(jié)果的判斷很困難,尤其是非標準的控件。
當界面修改后,原有代碼很容易失效
要應(yīng)用基于圖形界面的自動化測試工具,架構(gòu)師在架構(gòu)的時候應(yīng)該考慮:
界面風格如何保持一致。應(yīng)當由架構(gòu),而非程序員決定架構(gòu)的風格。包括布局,控件大小,相對位置,文字,對操作的響應(yīng)方式,超時時長,等等。
如何在最合適測試工具的界面和用戶喜歡的界面之中折中。比如,Test Quest是基于圖像識別的,那么黑白兩色的界面是最有利的,而用戶喜歡的漸進色就非常不利。也許讓界面具備自動的切換能力最好。
對于已經(jīng)完成的產(chǎn)品,如果架構(gòu)沒有為自動化測試做過考慮,所能應(yīng)用的范圍就非常有限,不過還是有一些思路可以供參考:
實現(xiàn)小規(guī)模的自動化腳本。針對一個具體的操作流程進行測試,而不是試圖用一個腳本測試整個軟件。一系列的小測試腳本組成了一個集合,覆蓋系統(tǒng)的一部分功能。這些測試腳本可以都以軟件啟動時的狀態(tài)作為基準,所以在狀態(tài)處理上會比較簡單
”猴子測試”有一定的價值。所謂猴子測試,就是隨機操作鼠標和鍵盤。這種測試完全不理解軟件的功能,可以發(fā)現(xiàn)一些正常測試無法發(fā)現(xiàn)的錯誤。據(jù)微軟內(nèi)部的資料,微軟的一些產(chǎn)品15%的錯誤是由“猴子測試”發(fā)現(xiàn)的。
總的來講,基于界面的自動化測試是不成熟的。對架構(gòu)師而言一定要避免功能只能通過界面才能訪問。要讓界面僅僅是界面,而軟件大部分的功能是獨立于界面并可以通過其他方式訪問的。上面框架的例子中的設(shè)計就體現(xiàn)了這一點。
思考:如何讓界面具有自我測試功能?
6.3.2. 基于消息的自動化測試
如果軟件對外提供基于消息的接口,自動化測試就會變得簡單的多。上面已經(jīng)提到了固件的TL1接口。對于界面部分,則應(yīng)該在設(shè)計的時候,將純粹的“界面”獨立出來,讓它盡可能的薄,而其他部分依然可以基于消息提供服務(wù)。
在消息的基礎(chǔ)上,用腳本語言包裝成函數(shù)的形式,可以很容易的調(diào)用,并覆蓋消息的各種參數(shù)組合,從而提高測試的覆蓋率。關(guān)于如何將消息包裝為腳本,可以參考SOAP的實現(xiàn)。如果使用的不是XML,也可以自己實現(xiàn)類似的自動代碼生成。
這些測試腳本應(yīng)該由開發(fā)人員撰寫,每當實現(xiàn)了一個新的接口(也就是一條新的消息),就應(yīng)該撰寫相應(yīng)的測試腳本,并作為項目的一部分保存在代碼庫中。當需要執(zhí)行回歸測試的時候,只要運行一遍測試腳本即可,大大提高了回歸測試的效率。
所以,為了實現(xiàn)軟件的自動化測試,提供基于消息的接口是一個很好的辦法,這讓我們可以在軟件之外獨立的編寫測試腳本。在設(shè)計的時候可以考慮這個因素,適當?shù)脑黾榆浖⒌闹С?。當然,TL1只是一個例子,根據(jù)項目的需要,可以選擇任何適合的協(xié)議,如SOAP。
6.3.3. 自動化測試框架
在編寫自動化測試腳本的時候,有很多的工作是重復的,比如建立socket連接,日志,錯誤處理,報表生成等。同時,對于測試人員來說,這些工作可能是比較困難的。因此,設(shè)計一個框架,實現(xiàn)并隱藏這些重復和復雜的技術(shù),讓測試腳本的編寫者將注意力集中在具體的測試邏輯上。
這樣一個框架應(yīng)該實現(xiàn)以下功能:
完成連接的初始化等基礎(chǔ)工作。
捕獲所有的錯誤,保證Test Case中的錯誤不會打斷后續(xù)的Test Case執(zhí)行。
自動檢測和執(zhí)行Test Case。新增的Test Case是獨立的腳本文件,無須修改框架的代碼或者配置。
消息編解碼,并以函數(shù)的方式提供給Test Case編寫者調(diào)用。
方便的工具,如報表,日志等。
自動統(tǒng)計Test Case的運行結(jié)果并生成報告。
自動化測試框架的思路和一般的軟件框架是一致的,就是避免重復勞動,降低開發(fā)難度。
下圖是一個自動化測試框架的結(jié)構(gòu)圖:
每個Test Case都必須定義一個規(guī)定的Run函數(shù),框架將依次調(diào)用,并提供相應(yīng)的庫函數(shù)供Test Case用來發(fā)送命令和獲得結(jié)果。這樣,測試用例的編寫者就只需要將注意力集中在測試本身。舉例:
def run(): open_laser() assert(get_laser_state() == ON) insert_error(BIT_ERROR) assert(get_error_bit() == BIT_ERROR)
測試用例的編寫者擁有的知識是“必須先打開激光器然后才能向線路上插入錯誤”。而架構(gòu)師能提供的是消息收發(fā),編解碼,錯誤處理,報表生成等,并將這些為測試用例編寫者隔離。
問題: open_laser, get_laser_state這些函數(shù)是誰寫的?
問題:如何進一步實現(xiàn)知識的解耦?能否有更方便的語言來編寫TestCase?
6.3.4. 回歸測試
有了自動化的測試腳本和框架,回歸測試就變得很簡單了。每當有新版本發(fā)布時,只需運行一遍現(xiàn)有的Test Case,分析測試報告,如果有測試失敗的Case則回歸測試失敗,需要重新修改,直到所有的Case完全通過。完整的回歸測試是軟件質(zhì)量的重要保證。
6.4. 集成測試
集成測試要驗證的是系統(tǒng)各個組成模塊的接口是否工作正常。這是比系統(tǒng)測試更低層的測試,通常由開發(fā)人員和測試人員共同完成。
例如在一個典型的嵌入式系統(tǒng)中,F(xiàn)PGA,固件和界面是常見的三個模塊。模塊本身還可以劃分為更小的模塊,從而降低復雜度。嵌入式軟件模塊測試的常見問題是硬件沒有固件則無法工作,固件沒有界面就無法驅(qū)動;反過來,界面沒有固件不能完整運行,固件沒有硬件甚至無法運行。于是沒有經(jīng)過測試的模塊直到集成的時候才能完整運行,發(fā)現(xiàn)問題后需要考慮所有模塊的問題,定位和解決的代價都很大。假設(shè)有模塊A和B,各有十個bug。如果都沒有經(jīng)過模塊測試直接集成,可以認為排錯的工作量相當于10*10等于100。
所以,在設(shè)計一個模塊的時候,首先要考慮,這個模塊如何單獨測試?比如,如果界面和固件之間是通過SOCKET通信的,那么就可以開發(fā)一個模擬固件,在同樣的端口上提供服務(wù)。這個模擬固件不執(zhí)行實際的操作,但是會響應(yīng)界面的請求并返回模擬的結(jié)果。并且返回的結(jié)果可以覆蓋到各種典型的情況,包括錯誤的情況。使用這樣的技術(shù),界面部分幾乎可以得到100%的驗證,在集成階段遇到錯誤的大大減少。
對固件而言,因為處于系統(tǒng)的中間,所以問題復雜一些。一方面,要讓固件可以通過GUI以外的途徑被調(diào)用;另一方面則要模擬硬件的功能。對于第一點,在設(shè)計的時候,要讓接口和實現(xiàn)分離。接口可以隨意的更換,比如和GUI的接口也許是JSON,同時還可以提供telnet的TL1接口,但是實現(xiàn)是完全一樣的。這樣,在和GUI集成之前,就可以通過TL1進行完全的測試固件。對于第二點,則應(yīng)該在設(shè)計的時候提取出硬件抽象層,讓固件的主要實現(xiàn)和寄存器,內(nèi)存地址等因素隔離開來。在沒有硬件或者硬件設(shè)計未定的時候?qū)崿F(xiàn)一個硬件模擬層,來保證固件可以完整運行并測試。
6.5. 單元測試
單元測試是軟件測試的最基本單位,是由開發(fā)人員執(zhí)行以保證其所開發(fā)代碼正確的過程。開發(fā)人員應(yīng)該提交經(jīng)過測試的代碼。未經(jīng)單元測試的代碼在進入軟件后,不僅發(fā)現(xiàn)問題后很難定位,而且通過系統(tǒng)測試是很難做到對代碼分支的完全覆蓋的。TDD就是基于這個層次的開發(fā)模式。
單元測試的粒度一般是函數(shù)或者類,例如下面這個常用函數(shù):
int atoi(const char *nptr);
這是一個功能非常單一的函數(shù),所以單元測試對它非常有效。可以通過單元測試驗證下列情況:
一般正常調(diào)用,如”9”,”1000”,”-1”等
空的nptr指針
非數(shù)字字符串,”abc”,”@#!123”,”123abc”
帶小數(shù)點的字符串, “1.1”,”0.111”,”.123”
超長字符串
超大數(shù)字,”999999999999999999999999999”
超過一個的-號和位置錯誤的-號,”—1”,”-1-“,”-1-2”
如果atoi通過了以上測試,我們就可以放心的將它集成到軟件中去了。由它再引發(fā)問題的概率就很小了(不是完全沒有,因為我們不能遍歷所有可能,只是挑選有代表性的異常情況進行測試)。
以上的例子可以說是單元測試的典范,但實際中卻常常不是這么回事。我們常常發(fā)現(xiàn)寫好的函數(shù)很難做單元測試,不僅工作量很大,效果也不見得好。其根本的原因是,函數(shù)沒有遵循好一些原則:
單一功能
低耦合
反觀atoi的例子,功能單一明確,和其他函數(shù)幾乎沒有任何耦合(我上面并沒有寫atoi的代碼實現(xiàn),大家可以自己實現(xiàn),希望是0耦合)。
下面我舉一個實際中的例子。
這是一個簡單的TL1命令發(fā)送和解析軟件,功能需求描述如下:
ü 通過telnet與TL1服務(wù)器通訊
ü 發(fā)送TL1命令給TL1服務(wù)器
ü 解析TL1服務(wù)器的響應(yīng)
TL1是通訊行業(yè)廣泛使用的一種協(xié)議,為了給不熟悉TL1的朋友簡化問題,我定義了一個簡化的格式:
CMDPAYLOAD; CMD - 命令的名字,可以是任意字母開頭,由字母和下劃線組成的字符串 CTAG - 一個數(shù)字,用于標志命令的序號 PAYLOAD - 可以是任意格式的內(nèi)容 ; - 結(jié)束符 相應(yīng)的,TL1服務(wù)器的回應(yīng)也有格式: DATE CTAG COMPLD PAYLOAD ; DATE – 日期和時間 CTAG – 一個數(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為例,這個函數(shù)應(yīng)該完成的功能包括:
ü 建立telnet連接,如果連接尚未建立
ü 構(gòu)造TL1命令字符串
ü 發(fā)送
ü 接收反饋
ü 解析反饋,并給IP_CONF結(jié)構(gòu)復制
ü 返回
我們當然不希望每個這樣的函數(shù)都重復實現(xiàn)這些功能,所以我們定義了幾個模塊:
Telnet 連接管理
TL1命令構(gòu)造
TL1 結(jié)果解析
這里我們來分析TL1結(jié)果解析,假設(shè)設(shè)計為一個函數(shù),函數(shù)的原型如下:
Dict* TL1Parse(const char* tl1response)
這個函數(shù)的功能是接受一個字符串,如果它是一個合法且已知的TL1回應(yīng),則將其中的結(jié)果提取出來,放入一個字典對象中。
這本來會是一個很便于進行單元測試的例子:輸入各種字符串,檢查返回結(jié)果是否正確即可。但是在這個軟件中,有一個很特殊的問題:
TL1Parse在解析一個字符串時,它必須要知道當前要處理的是哪條命令的回應(yīng)。但是請注意,在TL1的回應(yīng)中,是不包括命令的名字的。唯一的辦法是使用CTAG,這個命令和回應(yīng)一一對應(yīng)的數(shù)字。Tl1Parse首先提取出CTAG來,然后查找使用這個CTAG的是什么命令。這里產(chǎn)生了一個對外調(diào)用,也就是耦合。
有一個對象維護了一個CTAG和命令名字對應(yīng)關(guān)系的表,通過CTAG,可以查詢到對應(yīng)的命令名,從而知道如何解析這個TL1 response.
如此一來,TL1Parse就無法進行單元測試了,至少不能輕易的進行。通常的樁函數(shù)的辦法都不好用了。
怎么辦?
重新設(shè)計,消除耦合。
將TL1Parse拆分為兩個函數(shù):
Tl1_header TL1_get_header(const char* tl1response) Dict* TL1_parse_payload(const char* tl1name ,const char* tl1payload)
這兩個函數(shù)都可以單獨進行完整的單元測試。而這兩個函數(shù)的代碼基本就是TL1Parse切分了一下,但是其可測試性得到了很大的提高,得到一個可靠的解析器的可能性自然也大大提升了。
這個例子演示了如何通過設(shè)計來提高代碼的可測試性—這里是單元測試。一個隨意設(shè)計,隨意實現(xiàn)的軟件要進行單元測試將會是一場噩夢,只有在設(shè)計的時候就考慮到單元測試的需要,才能真正的進行單元測試。
6.5.1. 圈復雜度測量
模塊的復雜度直接影響了單元測試的覆蓋率。最著名的度量代碼復雜度的方法是圈復雜度測量。
計算公式:V(F)=e-n+2。其中e是流程圖中的邊的數(shù)量,n是節(jié)點數(shù)量。簡單的算法是統(tǒng)計如 if、while、do和switch 中的 case 語句數(shù)加1。適合于單元測試的代碼的復雜度一般認為不應(yīng)該超過10。
6.5.2. 扇入扇出測量
扇入是指一個模塊被其他模塊所引用。扇出是指一個模塊引用其他模塊。我們都知道好的設(shè)計應(yīng)該是高內(nèi)聚低耦合的,也就是高扇入低扇出。一個扇出超過7的模塊一般認為是設(shè)計欠佳的。扇出過大的模塊進行單元測試不論從樁設(shè)置還是覆蓋率上都是困難的。將系統(tǒng)的傳出耦合和傳入耦合的數(shù)量結(jié)合起來,形成另一個度量:不穩(wěn)定性。
不穩(wěn)定性 = 扇出 / (扇入 + 扇出)
這個值的范圍從0到1。值越接近1,它就越不穩(wěn)定。在設(shè)計和實現(xiàn)架構(gòu)時,應(yīng)當盡量依賴穩(wěn)定的包,因為這些包不太可能更改。相反的,依賴一個不穩(wěn)定的包,發(fā)生更改時間接受到傷害的可能性就更大。
6.5.3. 框架對單元測試的意義
框架的應(yīng)用在很大程度上可以幫助進行單元測試。因為二次開發(fā)者被限定實現(xiàn)特定的接口,而這些接口勢必都是功能明確,簡單,低耦合的。之前的框架示例代碼也演示了這一點。這再次說明了,由高水平的工程師設(shè)計出的框架,可以強制初級工程師產(chǎn)生高質(zhì)量的代碼。
7. 維護架構(gòu)的一致性
在實際的開發(fā)中,代碼偏離精心設(shè)計的架構(gòu)是很常見的事情,比如下圖示例了一個嵌入式設(shè)備中設(shè)計的MVC模式:
View依賴于Controller和Model, Controller依賴于Model,Model作為底層服務(wù)提供者,不依賴View或者Controller. 這是一個適用的架構(gòu),可以在相當程度上分離業(yè)務(wù),數(shù)據(jù)和界面。但是,某個程序員在實現(xiàn)時,使用了一個從Model到View的調(diào)用,破壞了架構(gòu)。
這種現(xiàn)象通常發(fā)生在產(chǎn)品的維護階段,有時也發(fā)生在架構(gòu)的實現(xiàn)階段。為了增加一個功能或者修正一個錯誤,程序員由于不理解原有架構(gòu)的思路,或者只是單純的偷懶,走了“捷徑”。如果這樣的實現(xiàn)不能及時發(fā)現(xiàn)并糾正,設(shè)計良好的架構(gòu)就會被漸漸破壞,也就是我們常說的“架構(gòu)”腐爛了。通常一個有一定年齡的軟件產(chǎn)品的架構(gòu)都有這個問題。如何監(jiān)視并防止這種問題,有技術(shù)上的和管理上的手段。
技術(shù)上,借助工具,可以對系統(tǒng)組件的依賴進行分析,架構(gòu)的外在表現(xiàn)最重要的就是各個部分的耦合關(guān)系。有一些工具可以統(tǒng)計軟件組件的扇入和扇出。可以用這種工具編寫測試代碼,對組件的扇出進行檢測,一旦發(fā)現(xiàn)測試失敗,就說明架構(gòu)遭到了破壞。這種檢查可以集成在一些IDE中, 在編譯時同步進行,或者在check in的時候進行。更高級的工具可以對代碼進行反向工程生成UML,可以提供更進一步的信息。但通常對扇入扇出做檢查就可以了。
通過設(shè)置代碼檢視的開發(fā)流程,對程序員check in的代碼進行評審,也可以防止此類問題。代碼檢視是開發(fā)中非常重要的一環(huán),它屬于開發(fā)后期階段用來防止壞的代碼進入系統(tǒng)的重要手段。代碼檢視通常要關(guān)注以下問題:
是否正確完整的完成了需求
是否遵循了系統(tǒng)的架構(gòu)
代碼的可測試性
錯誤處理是否完備
代碼規(guī)范
代碼檢視通常以會議的形式進行,時間點設(shè)置在項目階段性完成,需要check in代碼時。對于迭代式開發(fā),則可以在一個迭代周期結(jié)束前組織。參與人員包括架構(gòu)師,項目經(jīng)理,項目成員,其他項目的資深工程師等。一般時間不要太長,以不超過2個小時為宜。會議前2天左右發(fā)出會議通知和相關(guān)文檔代碼,與會者必須先了解會議內(nèi)容,進行準備。會議中,由代碼的作者首先講解代碼需要實現(xiàn)的功能,自己的實現(xiàn)思路。然后展示代碼。與會者根據(jù)自己的經(jīng)驗提出各種問題和改進意見。這種會議最忌諱的是讓作者感到被指責或者輕視,所以,會議組織者要首先定義會議的基調(diào):會議成功與否的標準不是作者的代碼質(zhì)量如何,而是與會者是否提供了有益的建議。會后由作者給與會者打分,而不是反之。
8. 一個實際嵌入式系統(tǒng)架構(gòu)的演化
上世紀九十年代,互聯(lián)網(wǎng)的極速發(fā)展讓通訊測試設(shè)備也得到了極大的發(fā)展。那個年代,能夠?qū)崿F(xiàn)某種測量的硬件是競爭的核心,軟件的目的僅僅是驅(qū)動硬件運行起來,再提供一個簡單的界面。所以,最初的產(chǎn)品的軟件結(jié)構(gòu)非常簡單,類似前面的城鐵門禁系統(tǒng)。
優(yōu)點:程序簡單明了的實現(xiàn)了用戶的需求,一個程序員就可以全部搞定。
缺點:完全沒有劃分模塊,底層上層耦合嚴重。
8.1. 數(shù)據(jù)處理
用戶要求能將測量結(jié)果保存下來,并可以重新打開。數(shù)據(jù)存儲模塊和界面被獨立出來。
依然保持上面的主邏輯,但是界面部分不僅可以顯示實時的數(shù)據(jù),也可以從ResultManager中讀取數(shù)據(jù)來顯示。
優(yōu)點:數(shù)據(jù)和界面分離的雛形初步顯現(xiàn)
缺點:ResultManager只是作為一個工具存在,負責保存和裝載歷史數(shù)據(jù)。界面和數(shù)據(jù)的來源依然耦合的很緊。不同的界面需要的不同數(shù)據(jù)都是通過硬編碼判斷的。
8.2. 窗口管理
隨著功能不斷復雜,界面窗口越來越多,原來靠一個類來繪制各種界面的方式已經(jīng)不能承受。于是窗口的概念被引入。每個界面都被視為一個窗口,窗口中的元素為控件。窗口的打開,關(guān)閉,隱藏則由窗口管理器負責。
優(yōu)點:界面功能以窗口的單位分離,不再是一個超大的集合。
缺點:雖然有了窗口管理器,但是界面依然是直接和底層耦合的,依然是大循環(huán)結(jié)構(gòu)。
8.3. MVC模式
隨著規(guī)模進一步擴大,最初的大循環(huán)結(jié)構(gòu)終于無法滿足日益復雜的需求了。標準的MVC模式被引入,經(jīng)歷了一次大的重構(gòu)。
數(shù)據(jù)中心作為Model被獨立出來,保存著當前最新的數(shù)據(jù)。View被放在了獨立的任務(wù)中執(zhí)行,定期從DataCenter輪詢數(shù)據(jù)。用戶的操作通過View發(fā)送給Controller,進一步調(diào)用硬件驅(qū)動執(zhí)行。硬件執(zhí)行的結(jié)果從驅(qū)動到Controller更新到DataCenter中。界面,數(shù)據(jù),命令三者基本解耦。ResultManager成為DataCenter的一個組件,View不再直接與其通訊。
MVC模式的引入,第一次讓這個產(chǎn)品了有真正意義上職責明晰,功能獨立的架構(gòu)。
8.4. 大量類似模塊,低效的復用
到上一步,作為一個單獨的嵌入式設(shè)備,其架構(gòu)基本可以滿足需求。但是隨著市場的擴展,越來越多的設(shè)備被設(shè)計出來。這些設(shè)備雖然執(zhí)行的具體測量任務(wù)不同,但是他們都有著同樣的操作方式,類似的界面,更主要的是,它們面臨的問題領(lǐng)域是相同的。長期以來,復制和粘貼是唯一的復用方式,甚至類名變量名都來不及改。一個錯誤在一個設(shè)備上被修正,同樣一段代碼的錯誤在其他設(shè)備上卻來不及修改。而隨著團隊規(guī)模的擴大,甚至MVC的基本架構(gòu)在一些新設(shè)備上都沒能遵守。
最終框架被引入了這個系列的產(chǎn)品??蚣艽_定了如下內(nèi)容:
MVC模式的基本架構(gòu)
窗口管理器和組件布局算法
多國語言方案(字符串管理器)
日志系統(tǒng)
內(nèi)存分配器和內(nèi)存泄露檢測
8.5. 遠程控制
客戶希望將設(shè)備固定安放在網(wǎng)絡(luò)的某個位置,作為“探針”使用,在辦公室通過遠程控制來訪問這個設(shè)備。這對于原本是作為純手持設(shè)備設(shè)計的系統(tǒng)又是一個挑戰(zhàn)。幸運的是,MVC架構(gòu)具有相當?shù)膹椥?,早期的投入獲得了回報。
TL1 Server 對外提供基于Telnet的遠程控制接口。在系統(tǒng)內(nèi)部,它的位置相當于View,只和原有的Controller和DataCenter通訊。
8.6. 自動化的TL1解釋器
由于TL1命令相當多,而TL1又往往不是客戶的第一需求,很多設(shè)備的TL1命令開始不完整。究其原因,還是手寫TL1命令的解釋器太累。后來通過引入Bison和Flex,這個問題有所改善,但還是不足。自動化代碼生成在這個階段被引入。通過以如下的格式定義TL1,工具可以自動生成TL1的編碼和解碼器代碼。
CMD_NAME { cmd = “SET-TIME-CONFIG::
8.7. 測試的難題
經(jīng)過數(shù)十年的積累,產(chǎn)品已經(jīng)成為一個系列,幾十種設(shè)備。大部分設(shè)備進入了維護期,經(jīng)常有客戶提一些小的改進,或者要求修正一下缺陷。繁重的手工回歸測試成為了噩夢。
基于TL1的自動化測試極大的解放了測試人員。通過在PC上運行的測試腳本,回歸測試變得簡單而可靠。唯一不足的是界面部分無法驗證。
基于Test Quest的自動化工具需要在設(shè)備運行的pSOS系統(tǒng)上開發(fā)一個類似遠程桌面的軟件,而這在pSOS上并非易事。不過好消息是,由于框架固定了界面的風格和布局算法,基于Test Quest的自動化工具會有很高的識別效率。
8.8. 小結(jié)
從這個實際的嵌入式產(chǎn)品重構(gòu)的歷程可以看出,第三步引入MVC模式和第四步的框架化是非常關(guān)鍵的。成熟的MVC模式保證了后續(xù)一系列的可擴充性,而框架則保證了這個架構(gòu)的在所有產(chǎn)品中的準確重用。
9. 總結(jié)
本文是針對嵌入式軟件開發(fā)的特點,討論架構(gòu)設(shè)計的思路和方法。試圖給大家提供一種思想,啟發(fā)大家的思維。框架,自動化代碼生成和測試驅(qū)動的架構(gòu)是核心內(nèi)容,其中框架又是貫穿始終的要素。有人問我,什么是架構(gòu)師,怎么樣才能成為架構(gòu)師?我回答說:編碼,編碼,再編碼;改錯,改錯,再改錯。當你覺得厭煩的時候,停下來想想,怎么才能更快更好的完成這些工作?架構(gòu)師就是在實踐中產(chǎn)生的,架構(gòu)師來自于那些勤于思考,懶于重復的人。
-
嵌入式系統(tǒng)
+關(guān)注
關(guān)注
41文章
3606瀏覽量
129596 -
軟件架構(gòu)
+關(guān)注
關(guān)注
0文章
64瀏覽量
10296
原文標題:深度:嵌入式系統(tǒng)的軟件架構(gòu)設(shè)計!
文章出處:【微信號:mcu168,微信公眾號:硬件攻城獅】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論