具體的重構(gòu)手段可參考《代碼大全2》或《重構(gòu):改善既有代碼的設(shè)計》,本文不再班門弄斧,而側(cè)重重構(gòu)時一些粗淺的“方法論”,旨在提高重構(gòu)效率。
作者未采用重量級的重構(gòu)工具,僅用到Source Insight的”Smart Rename”功能。也未使用CUnit等單元測試工具,而是通過在線調(diào)測和自動化測試保證代碼的正確性。
一 背景
MDU系列產(chǎn)品從他處接手,OMCI模塊相關(guān)人員含作者在內(nèi)不過三五人。除新增功能的開發(fā)外,大量時間花費在處理遺留故障上。但該模塊代碼龐雜且可讀性差,導(dǎo)致大家僅了解其“大概輪廓”,難以放心地使用和維護(hù)。
此外,忙碌容易使人迷失方向。主要的時間精力花費在故障處理上時,自然無暇考慮整改代碼,從而陷入四處救火、疲于奔命的尷尬境地。
二 目標(biāo)
重構(gòu)的主要目的在于改善既有代碼的設(shè)計,而不是修改缺陷、新增功能等。
重構(gòu)可以是修改變量名、重新安排目錄這樣簡單的物理重構(gòu),也可以是抽取子函數(shù)、精簡冗余設(shè)計這樣稍許復(fù)雜的邏輯重構(gòu)。但均不改變現(xiàn)有代碼的功能。
重構(gòu)可以將意大利面條式的雜亂代碼整理為千層餅式的整潔代碼。整潔的代碼更加健壯,因其便于建立完善的測試防護(hù)網(wǎng)。同時,新手老人均可放心地修改。
期望重構(gòu)之后,代碼邏輯一目了然,擴(kuò)展和修改非常方便,出現(xiàn)故障時能迅速定位和修復(fù)。前人摔跤過的地方后人不再栽倒,前人思考出的成果后人可直接借用。總之,高度人性化,極大解放人力和腦力。
最初的想法是,通過重構(gòu)部分流程和代碼(代碼先行),建立測試防護(hù)體系,生成階段報告,展現(xiàn)代碼質(zhì)量(實例加數(shù)據(jù))和故障收斂曲線。借助這樣的報告,可望獲得領(lǐng)導(dǎo)層的支持和宣貫,也有利于績效考核。
三 實踐
具體實踐時,作者并未進(jìn)行純粹的“重構(gòu)”,還兼做缺陷修改,并增加自動化測試等輔助功能。原則上,對既有代碼注重重構(gòu),對新增代碼注重復(fù)用。
3.1 代碼研讀
OMCI模塊代碼龐雜,分支眾多,上手困難(據(jù)稱半年勉強(qiáng)入門,一年才能熟練)。若不能有效掌握現(xiàn)有代碼,后續(xù)難免被迫付出時間健康而又得不到項目認(rèn)同(事實上,模塊內(nèi)發(fā)現(xiàn)的遺留故障源源不斷)。反之,若能全面掌握現(xiàn)有代碼,后續(xù)才可能通過反向工程、系統(tǒng)/代碼恢復(fù)和重構(gòu)等手段,將模塊改造得更易開發(fā)和維護(hù),最終解放編碼者自己。
為提高代碼研讀效率,可采用分工閱讀和代碼注釋的方法。
“分工閱讀”是指將模塊分為若干塊子功能(如協(xié)議解析、告警、統(tǒng)計、二層、語音等),組內(nèi)每人負(fù)責(zé)一塊或幾塊,不定期地交流和輪值。
“代碼注釋”是指在學(xué)習(xí)代碼過程中,隨手注釋代碼(大至流程、函數(shù),小至代碼行),功能、意圖、技巧、缺陷、疑問等均可(凡經(jīng)過思考的地方都是可加注釋之處)。其中“疑問”既可咨詢兄弟產(chǎn)品同一模塊的同事再轉(zhuǎn)換為功能或意圖,也可由其他注釋者解答。
這樣做的好處是:避免重復(fù)鉆研;經(jīng)驗積累;可供量化。
代碼可取產(chǎn)品最新版本,建立服務(wù)器公共代碼目錄(SVN管理更好)。注釋時不要覆蓋其他人的注釋即可。
建議注釋統(tǒng)一格式,便于識別和檢索,形如”//>”。以下示出一個代碼注釋實例:
?case?OMCI_ME_ATTRIBUTE_2:?//?Operational?state ?????if?(attr.attr.ucOperationState?!=?0?&&?attr.attr.ucAdminState?!=?1)?//xywang0618>?BUG:?should?be?ucOperationState! ?????{ ?????????return?OMCI_FUNC_RETURN_OUT_OF_RANGE; ?????} ?????break;
3.2 可讀性
首先,規(guī)范變量、函數(shù)等命名。具體方法不再贅述。
其次,注釋到位,尤其是全局變量和通用函數(shù)。舉例如下:
/****************************************************************************** *?函數(shù)名稱:??ByteArray2StrSeq *?功能說明:??掩碼字節(jié)數(shù)組字符串化 ????????????該數(shù)組元素為掩碼字節(jié),將其所有值為1的比特位置轉(zhuǎn)換為指定格式的字符串 *?輸入參數(shù):??pucByteArray:?掩碼字節(jié)數(shù)組 ????????????ucByteNum???:?掩碼字節(jié)數(shù)組待轉(zhuǎn)換的有效字節(jié)數(shù)目 ????????????ucBaseVal???:?掩碼字符串起始字節(jié)對應(yīng)的數(shù)值 *?輸出參數(shù):? pStrSeq ????:掩碼字符串,以','、'-'間隔 ????????????形如0xD7(0b'11010111)??--->?"0-1,3,5-7" *?返?回?值:? pStr ???????:pStrSeq的指針備份,可用于strlen等鏈?zhǔn)奖磉_(dá)式 *?用法示例:??INT8U?aucByteArray[8]?=?{0xD7,?0x8F,?0xF5,?0x73}; ????????????CHAR?szSeq[64]?=?{0}; ????????????ByteArray2StrSeq(aucByteArray,?4,?0,?szSeq); ???????????????---->?"0-1,3,5-8,12-19,21,23,25-27,30-31" ????????????memset(szSeq,?0,?sizeof(szSeq)); ????????????ByteArray2StrSeq(aucByteArray,?4,?1,?szSeq); ???????????????---->?"1-2,4,6-9,13-20,22,24,26-28,31-32" *?注意事項:??因本函數(shù)內(nèi)含strcat,故調(diào)用前應(yīng)按需初始化pStrSeq ******************************************************************************/ CHAR?*ByteArray2StrSeq(INT8U?*pucByteArray,?INT8U?ucByteNum,?INT8U?ucBaseVal,?CHAR?*pStrSeq);
最后,整改晦澀難懂的代碼。主要有兩種手段:
1) 改寫方法
以PON光路檢測為例,底層接口提供的光功率單位為0.1uW,OMCI協(xié)議Test消息上報的光功率單位為0.002dBuW,而Ani-G功率屬性單位則為0.002dBmW。
原有代碼轉(zhuǎn)換如下(為突出重點有所改編):
INT16S?wRxPower?=?GetRxPowerInDot1uW();?//接收光功率 if(wRxPower?1){ ????wRxPower?=?1; } /*0.1uw?to?0.002dbm*/ dblVal?=?10?*?log10(wRxPower)?-?40; dblVal?=?dblVal?*?500; wRxPower?=?(INT16U)dblVal; wRxPower??=?(int)wRxPower*100; /*opt?pwr??0.00002db??????X??*?0.00002*/ wRxPower?=?wRxPower?+?(30?*?500)?*?100; if(wRxPower?0){ ????val?=?(INT16U)((0?-?wRxPower)?/?100); ????val?=?(((~val)?&?0x7fff)?+?1)?|?0x8000; ????wRxPower?=?val; } else{ ????wRxPower?=?wRxPower?/?100; }
可見,原實現(xiàn)中轉(zhuǎn)換關(guān)系非常晦澀難懂。其實借助1dBuW=10*lg(1uW)和1dBuW-1dBmW=30dB兩個公式,經(jīng)過簡單的數(shù)學(xué)推導(dǎo)即可得到更簡潔易懂的表達(dá)(為突出重點有所改編):
INT16S?wRxPower?=?GetRxPowerInDot1uW();?//接收光功率 //Test單位0.002dBuW,底層單位0.1uW,轉(zhuǎn)換關(guān)系T=(10*lg(B*0.1))/0.002=5000*(lgB-1) wRxPower?=?(INT16S)(5000?*?(log10((DOUBLE)wRxPower)-1)); //Ani-G功率屬性單位0.002dBmW,Test結(jié)果單位0.002dBuW //轉(zhuǎn)換關(guān)系A(chǔ)(dBmW)*0.002?+?30?=?T(dBuW)*0.002,即A=T-15000 INT16S?wAniRxPwr?=?wRxPower?-?15000;
注意,原實現(xiàn)中誤認(rèn)為Ani-G功率屬性與Test結(jié)果的單位相同,新實現(xiàn)已修正該錯誤。
2) 封裝函數(shù)
以實體屬性的掩碼校驗為例,原有代碼如下:
/*掩碼初校驗*/ if?((OMCIMETYPE_SET?==?vpIn->omci_header.ucmsgtype) ?||?(OMCIMETYPE_GET?==?vpIn->omci_header.ucmsgtype)) { ????wMask?=?W(response.omcimsg.auccontent[0],response.omcimsg.auccontent[1]); ????usSupportMask?=?(1?<(OMCI_ATTRIBUTE_NUMBER?-?map.num))-1; ????if(?0?!=?(wMask?&?usSupportMask)) ????{ ????????OmciPrint_warn("[%s]?check?mask?warning:?(meclass[%u],?meid[%u],?msgtype[%u],?mask[0x%x],?unsupport?mask[0x%x])! ", ???????????????????????FUNCTION_NAME,?vpIn->omci_header.wmeclass,?vpIn->omci_header.wmeid,?vpIn->omci_header.ucmsgtype,?wMask,?usSupportMask); ????} }
對usSupportMask賦值及判斷的語句(第6~7行),用于校驗掩碼是否越界。為更具可讀性,將其封裝為如下函數(shù):
/****************************************************************************** *?函數(shù)名稱:??OmciIsMaskOutOfLimit *?功能說明:??判斷實體屬性掩碼是否越界(比特1數(shù)目超過屬性數(shù)目) *?輸入?yún)?shù):??INT16U?wMeMask??:實體掩碼 *???????????INT8U?ucAttrNum?:屬性數(shù)目 *?輸出參數(shù):??NA *?返?回?值:??BOOL ******************************************************************************/ BOOL?OmciIsMaskOutOfLimit(INT16U?wMeMask,?INT8U?ucAttrNum) { ????//wMeMask?????:mmmm?mmmm?mmm0?m000 ????//wInvertMask?:0000?0000?000i?iiii ????INT8U?wInvertMask?=?(1?<(OMCI_ATTR_MAX_NUM-ucAttrNum))?-?1; ????return?(0?!=?(wMeMask?&?wInvertMask)); }
封裝后的函數(shù)名恰當(dāng)?shù)仄鸬健白悦枋觥钡淖饔谩?/p>
3.3 在線調(diào)測工程
該產(chǎn)品作為嵌入式終端,需要在Linux系統(tǒng)中編譯打包版本,然后將其下載到目標(biāo)單板上運行。這種交叉編譯方式對于單個模塊的調(diào)試而言,效率無疑比較低下。
為提高調(diào)測效率,在Linux服務(wù)器搭建在線調(diào)測工程。即提取OMCI模塊代碼,稍作改造后直接在服務(wù)器上編譯和運行。這樣就可避免每次修改代碼都要重啟單板升級大版本,調(diào)測效率極高。
為使模塊可獨立運行,需要編寫模擬接口以屏蔽底層調(diào)用,并裁減暫不必要的特性(如線程和通信)等。
3.4 模擬數(shù)據(jù)庫
OMCI模塊使用某內(nèi)存數(shù)據(jù)庫來管理需要持久化的實體信息,但該數(shù)據(jù)庫代碼內(nèi)調(diào)用了大量平臺相關(guān)的接口,不利于實現(xiàn)模塊的在線調(diào)測。因此,作者研讀源代碼后編寫了一個模擬數(shù)據(jù)庫。該庫仿照模塊使用的幾個原庫接口及行為,模擬接口內(nèi)部校驗均增加錯誤信息打印,以便于排障。
此外,在數(shù)據(jù)庫接口原語的基礎(chǔ)上二次封裝統(tǒng)一接口,一舉消除模塊內(nèi)數(shù)據(jù)庫操作代碼的凌亂和重復(fù)。
3.5 自動化測試
沒有測試保護(hù)網(wǎng)的重構(gòu),無異于沒有血源的外科手術(shù)。
首先,公共接口和函數(shù)均提供有相應(yīng)的測試函數(shù),兼做示例和用例。如:
//Start?of?ByteArray2StrSeqTest// VOID?ByteArray2StrSeqTest(VOID) { ????//ByteArray2StrSeq函數(shù)算法不甚優(yōu)美和嚴(yán)謹(jǐn),應(yīng)多加測試驗證,如有可能盡量優(yōu)化。 ????INT8U?ucTestIndex?=?1; ????INT8U?pucByteArray[]?=?{0xD7,?0x8F,?0xF5,?0x73,?0xB7,?0xF0,?0x00,?0xE8,?0x2C,?0x3B}; ????CHAR?pStrSeq[50]?=?{0}; ????//Time?Consumed(x86_gcc3.2.3_glibc2.2.5):?72us ????memset(pStrSeq,?0,?sizeof(pStrSeq)); ????ByteArray2StrSeq(pucByteArray,?4,?1,?pStrSeq); ????printf("[%s]?Result:?%s,?pStrSeq?=?%s! ",?__FUNCTION__,?ucTestIndex++, ???????????strcmp(pStrSeq,?"1-2,4,6-9,13-20,22,24,26-28,31-32")???"ERROR"?:?"OK",?pStrSeq); ????//Time?Consumed(x86_gcc3.2.3_glibc2.2.5):?7us ????memset(pStrSeq,?0,?sizeof(pStrSeq)); ????ByteArray2StrSeq(pucByteArray,?4,?0,?pStrSeq); ????printf("[%s] ?Result:?%s,?pStrSeq?=?%s!!! ",?__FUNCTION__,?ucTestIndex++, ???????????strcmp(pStrSeq,?"0-1,3,5-8,12-19,21,23,25-27,30-31")???"ERROR"?:?"OK",?pStrSeq); ????//Time?Consumed(x86_gcc3.2.3_glibc2.2.5):?4us ????memset(pStrSeq,?0,?sizeof(pStrSeq)); ????ByteArray2StrSeq(&pucByteArray[4],?2,?1,?pStrSeq); ????printf("[%s] ?Result:?%s,?pStrSeq?=?%s! ",?__FUNCTION__,?ucTestIndex++, ???????????strcmp(pStrSeq,?"1,3-4,6-12")???"ERROR"?:?"OK",?pStrSeq); ????//Time?Consumed(x86_gcc3.2.3_glibc2.2.5):?4us ????memset(pStrSeq,?0,?sizeof(pStrSeq)); ????ByteArray2StrSeq(&pucByteArray[6],?2,?1,?pStrSeq); ????printf("[%s] ?Result:?%s,?pStrSeq?=?%s! ",?__FUNCTION__,?ucTestIndex++, ???????????strcmp(pStrSeq,?"9-11,13")???"ERROR"?:?"OK",?pStrSeq); ????//Time?Consumed(x86_gcc3.2.3_glibc2.2.5):?5us ????memset(pStrSeq,?0,?sizeof(pStrSeq)); ????ByteArray2StrSeq(&pucByteArray[8],?2,?1,?pStrSeq); ????printf("[%s] ?Result:?%s,?pStrSeq?=?%s! ",?__FUNCTION__,?ucTestIndex++, ???????????strcmp(pStrSeq,?"3,5-6,11-13,15-16")???"ERROR"?:?"OK",?pStrSeq); } //End?of?ByteArray2StrSeqTest//
此外,模塊內(nèi)還增加自動化測試功能(TestSuite),可用來驗證批量或單個實體的配置和查詢操作。批量測試結(jié)果統(tǒng)計如下(省略各實體的具體測試結(jié)果)
在上述測試結(jié)果中,F(xiàn)ailed TestCase(s)最為關(guān)鍵,表示失敗的用例數(shù)目。此外,UnCompared TestCase(s)表示未做比較的條目數(shù),如獲取時間等易變屬性的實體,無法預(yù)置恰當(dāng)?shù)钠谕Y(jié)果,因此未做比較。測試過程中的打印信息可保存為日志文件,然后在打印日志中搜索Failure關(guān)鍵字,即可獲知哪些配置失敗。
當(dāng)大量修改當(dāng)前代碼時,借助上述自動化測試功能,可迅速獲知修改結(jié)果的影響。在開發(fā)新功能時,可先設(shè)計好測試用例和期望結(jié)果,然后按照“測試驅(qū)動開發(fā)”的模式來編碼,提高編碼效率和正確率。
3.6 直搗核心
傳統(tǒng)的重構(gòu)步驟是先容易后困難,先外圍后核心。而作者反其道而行之,首先重構(gòu)核心公共的代碼。這樣做的好處是:
1) 便于梳理頭文件包含關(guān)系
在線調(diào)測工程中最初只保留最為公共的代碼文件(如日志功能),重構(gòu)并調(diào)測通過后再逐步添加其他單一功能的目標(biāo)代碼。該過程中會按需拆分和/或組合文件,減少頭文件的嵌套和交叉引用。
2) 避免重復(fù)工作甚至返工
公共代碼重構(gòu)后并封裝后,對較外圍的應(yīng)用代碼重構(gòu)時會更容易消除冗余。若先重構(gòu)好外圍代碼,很可能發(fā)現(xiàn)某些邏輯可以統(tǒng)一到公共代碼內(nèi),從而導(dǎo)致大面積返工;而若先著手重構(gòu)公共代碼,則通過研讀外圍代碼對其的使用方式,很容易及早甄別這些冗余性。
3) 迭代驗證
在重構(gòu)后的公共代碼基礎(chǔ)上逐步疊加外圍代碼時,也在反復(fù)測試公共代碼的正確性和易用性。
4) 增強(qiáng)信心
先核心后外圍、逐步疊加驗證的過程可控,可增強(qiáng)大規(guī)模重構(gòu)時的信心,緩解壓力。反之,若先重構(gòu)好外圍代碼,等觸及核心時牽一發(fā)而動全身,壓力極大。
四 效果
在某產(chǎn)品代碼基礎(chǔ)上,進(jìn)行OMCI模塊DB/LOG/實體存取/消息處理/性能統(tǒng)計等重構(gòu)。經(jīng)過三個多月的重構(gòu)后,模塊代碼復(fù)雜度大幅下降(某核心源文件平均復(fù)雜度降為原先1/4),代碼顯著精簡(據(jù)不完全統(tǒng)計已精簡萬余行),同時更具可讀性。新增代碼的過程中,編寫大量工具類宏和函數(shù),并增加OMCI自動化測試、內(nèi)存檢測等實用功能。
通過LineCount和Source Monitor度量某功能代碼重構(gòu)效果,如下表所示:
此外,重構(gòu)過程中積累的通用框架、代碼及經(jīng)驗,可進(jìn)一步應(yīng)用到新的項目中。
編輯:黃飛
?
評論