在軟體行業,模組化設計早已深入人心,因為通過模組化這種「分而治之」的方法能有效地降低設計的複雜度。如何獲得更好的模組化設計不是這裡要討論的重點,本書只關注模組的初始化與終止化這兩個關鍵點。
在大多的嵌入式系統中,模組的執行是從呼叫它的初始化函式開始的。與模組大多有初始化函式相比,忽視為模組設計終止化函式這種現象卻很普遍。與執行在桌面作業系統上的軟體不同的是,通常整個嵌入式裝置就只有乙個應用軟體在執行,因此對軟體啟停不少會採用開關裝置電源或按下重啟按鈕這種「粗暴的」方式來完成,久而久之大家將為模組設計終止化函式當做了多餘。
首先,從完整性的角度來看,乙個模組如果提供初始化函式,那麼也應當設計終止化函式。其次,為每乙個模組設計終止化函式,意味著提供了一種「優雅地」關閉系統的手段,進一步的內涵是,通過這種方式將為我們創造檢測系統資源洩漏的時機。
讓我們站在堆管理模組的角度來檢查優雅終止模組所帶來的好處。這裡假設堆管理模組具備記錄每一次記憶體分配所發生的位置資訊這一功能。以它為例,是因為記憶體洩漏是嵌入式軟體開發中比較讓人頭痛的問題。
如果乙個系統中所有模組的終止行為都不經過各自的終止化函式的話,堆管理模組就無法通過它所記錄的分配位置資訊來了解是否存在記憶體洩漏問題。如果只為堆模組提供終止化函式也同樣無法發現記憶體洩漏問題,因為其他模組可能在初始化時分配記憶體,且這些內存在整個軟體生命週期中都需要使用。在系統終止時,如果這些記憶體不被各模組自行釋放的話,堆管理模組無法在它的終止化函式中判斷哪些記憶體發生了洩漏。
如果為每乙個模組都設計終止化函式,就能做到更容易檢測記憶體洩漏。如果那些在初始化函式中分配記憶體的模組在終止化時進行記憶體釋放操作,且假設所有使用了動態記憶體的模組的終止化函式是在堆管理模組的終止化函式之前被呼叫的,那麼當堆管理模組的終止化函式被呼叫時,就可以根據所記錄的資訊找到沒有釋放的記憶體(洩漏點)。
依此類推,其他的資源也可以採用這一方法發現洩漏。比如,在定時器管理模組的終止化函式中可以檢查是否所有的定時器都已**了。
在模組的終止化函式中檢查所管理資源是否存在洩漏需要解決乙個問題,即各個模組的依賴關係,以保證各資源管理模組的終止化函式是在使用它(所管理資源)的模組之後被呼叫的。第
14章所引入的模組分層與分級的概念將有助於解決模組間的依賴關係。
本書作業系統篇中的多個章節採用了這一設計原則以防範資源洩漏,讀者在閱讀這些章節時將加深對這一設計原則的理解。
統計資訊在好多方面將發揮作用。首先,它可被用於軟體調優。軟體在開發過程中不可能完全了解現場執行環境,這導致系統可能無法執行在最佳狀態(效能、可使用性等)。要讓系統處於最佳執行狀態,必須獲得軟體在現場的執行環境,這需要通過一定的資料,設計必要的統計資訊將有助於獲得所需的現場資料。
其次,統計資訊還有助於查錯。打個比方,如果乙個軟體系統是從裝置外部接收訊息並對之進行處理和響應的,如果設計有進入系統訊息數量的統計資訊,那麼當某些情形下出現負荷異常高時,通過它就可以判斷負荷異常是由外部引起的還是由內部造成的。大多難以定位錯誤根源的軟體缺陷,正是因為「蛛絲馬跡」太少了,而統計資訊有助於捕獲它們。
統計資訊通常採用為每乙個統計項定義乙個整型變數的形式,圖
13.7
是乙個定時器模組所設計的相關統計資訊。從第29和
48行可以看出,
statistic_t
型別就是整型的
typedef
。第70、71
和78~80
行則定義了相應的統計變數。當相應的情形出現時,則對對應的統計變數進行加一操作。統計資訊的顯示就是將統計變數的值通過一定形式輸出。由此看來,增加統計資訊的成本不論從記憶體空間還是處理器時間上其開銷都是很經濟的。
embedded/code/platform/common/inc/primitive.h
00029:
typedef unsigned int u32_t;
00047:
// for statistic
00048:
typedef u32_t statistic_t;
00049:
embedded/code/platform/timer/v3/src/timer.c
00068:
typedef struct bucket_t;
00075:
00076:
typedef struct timer_statistic_t;
00081:
00082:
static timer_statistic_t g_statistic;
圖13.7
運用這一設計原則,需要我們在設計過程中時刻思考哪些資訊對軟體查錯和調優有幫助。統計項的設計並不要求一步到位,可以在軟體生命週期的任何階段根據需要而增加。
程式**是設計的物質外殼,再好的思想必須最終通過**去表達,而這就離不開對函式、變數和引數進行恰當的命名,以便準確地傳達設計意圖。讓我們通過乙個例子來說明命名對設計的重要性。
假設需要設計乙個雙向鍊錶的操作函式,其功能是刪除煉表頭結點並將之當做函式的返回值返回,那如何給這一函式取名呢?
dll_get_head()
[1]這個命名可能會在讀者的腦海中浮現,但這個命名並不好,因為「
get」所表達的是「取」而沒有「刪除」的意思。當他人讀到對
dll_get_head()
函式的呼叫時,將理解為「只是引用頭結點但並不將它從鍊錶中刪除」,這顯然沒有精確地傳達設計本意。乙個沒有傳達設計本意的命名是注定要讓人困惑的。
那dll_extract_head()
呢?同樣不好,因為「
extract
」也不能表達從鍊錶中刪除結點的意思。用過
winzip
英文版的讀者或許注意到了,其中解壓檔案就用了「
extract
」這個詞。
比較合適的命名是
dll_pop_head()
。「pop
」這一動詞源於退棧操作,當從棧中「彈出」乙個元素時,意味著這一元素將從棧中被刪除並返回。將「
pop」引入雙向鍊錶的函式名中能準確地傳達設計意圖。
除了函式名的命名很重要外,函式引數和變數的命名同樣重要,因為它們能起到「點睛」的作用。對於圖
13.8
中的兩個函式,從引數名就能完全明白如何用,任何解釋用法的注釋都顯得多餘。
embedded/code/platform/common/inc/dll.h
void dll_insert_before
(dll_t *_p_dll, dll_node_t *_p_ref,
dll_node_t *_p_inserted);
void dll_insert_after
(dll_t *_p_dll, dll_node_t *_p_ref,
dll_node_t *_p_inserted);
圖13.8
軟體設計的最終產物不能是一堆難讀的**;相反,**應當努力做到讓人讀起來「行雲流水」。好的設計在看完它的介面函式和資料結構後就知道如何使用它,因為它們的命名向人傳達了模組的行為。從這一點說來,花時間斟酌命名是值得的,因為它節省了他人用於理解的時間。
在不少資料中強調注釋對於編碼的重要性,甚至提出程式應有三分之一的篇幅是注釋。對於「三分之一」這一提法,作者並不贊同。原因是:
n我們在讀程式時的第一反應是讀**而不是注釋。如果**能清楚地表達意思,那就沒有寫注釋的必要,即使寫了那也一定是多餘。如果注釋佔了整個專案源程式的三分之一,作者懷疑其中很多都是廢話,只是為了做到「佔三分之一」而已。
n注釋與**很容易在維護的過程中失步,因此會出現注釋所發出的聲音與源**實現完全不同這種尷尬。一旦注釋被發現不精確它就會被人遺忘,也就起不到注釋應有的效果。
與「三分之一」提法不同的是,作者認為注釋應當盡可能少,並將寫注釋所省下來的時間用於推敲命名。請注意,千萬不要誤認為「少注釋是好程式的充分條件」,而應當理解為「少注釋是好程式的必要條件」。
誠然,這並不是在否定注釋。大部分情形下,通過命名就能清楚地表達程式實現的區域性思想,而注釋應當放眼於全域性去寫以起到提綱挈領的作用,或者某些行為打破了常規
(比如,
switch
語句塊內的
case
與break
不成對)
也可以考慮通過注釋加以解釋。如果命名實在無法做到「傳神」或打破了常識的話,也可以考慮採用少量的注釋進行彌補。
人天生就是審美家,軟體工程師在進行軟體設計時這種天性會自然地發揮作用。將設計感覺當做是乙個設計原則,多少讓人覺得有點不靠譜,但這一原則也正體現了軟體設計中的藝術成分。
就作者的經驗來看,如果做設計時覺得彆扭,那工作效率一定不高;反之,則工作效率奇高。軟體設計真正花時間的是思考,而不是編碼。思考的目的是從紛繁的現象中試圖找到問題的本質,或者從眾多的因素中找出關鍵。設計時之所以出現「審美告警」,一定是有什麼沒有考慮清楚。這種情形下停下來做進一步的思考,將有助於理清思路,以最終獲得更好的設計。
相信每個軟體工程師或多或少都能感覺到「審美告警」,而訊號的強弱與軟體工程師的設計水平可能是正相關的。軟體工程師如果重視這種訊號,則這種訊號的靈敏度也會慢慢提高。因為重視它意味著將進行更多的思考,而思考多了就更容易形成自己的設計原則和思想;相反,如果長期忽視它的存在,則最終可能會造成這種訊號的消失。忽視「審美告警」的存在,或許意味著我們並不關心所設計的主題,其質量也別指望好到哪兒去,更有甚者會醞釀出將來的乙個「毒瘤」。
本文選自《專業嵌入式軟體開發
——全面走向高質高效程式設計(含
***光碟1張
)》一書
圖書詳細資訊
:[1]
函式名中的
dll是
double-linked list
,即雙向鍊錶的簡寫。
放之四海皆適用的設計原則(二)
在軟體行業,模組化設計早已深入人心,因為通過模組化這種 分而治之 的方法能有效地降低設計的複雜度。如何獲得更好的模組化設計不是這裡要討論的重點,本書只關注模組的初始化與終止化這兩個關鍵點。在大多的嵌入式系統中,模組的執行是從呼叫它的初始化函式開始的。與模組大多有初始化函式相比,忽視為模組設計終止化函...
放之四海皆適用的設計原則(一)
放之四海皆適用的設計原則 一 一種設計適合某一專案但未必適用於另乙個專案,要掌握每乙個專案所適用的各種設計是不現實的。經驗告訴我們,通過使用規則有助於讓我們的大腦掌控更多的東西。同樣地,借助設計原則將使得我們能更好地掌控軟體設計。一旦掌握一定的設計原則,無論做怎樣的專案,通過運用它們將有助於做出 更...
放之四海皆適用的設計原則(一)
放之四海皆適用的設計原則 一 一種設計適合某一專案但未必適用於另乙個專案,要掌握每乙個專案所適用的各種設計是不現實的。經驗告訴我們,通過使用規則有助於讓我們的大腦掌控更多的東西。同樣地,借助設計原則將使得我們能更好地掌控軟體設計。一旦掌握一定的設計原則,無論做怎樣的專案,通過運用它們將有助於做出 更...