張 帆,王 嫣
(鄭州工程技術(shù)學(xué)院 a.信息工程學(xué)院;b.國際教育學(xué)院,河南 鄭州 450044)
按照計算機(jī)行業(yè)日新月異的發(fā)展速度,時至今日,程序重構(gòu)已經(jīng)算是一門相當(dāng)陳舊的技術(shù)了,但是陳舊并不代表著不重要,隨著演化式程序設(shè)計的發(fā)展,程序重構(gòu)得到越來越廣泛的使用。幾乎所有的軟件開發(fā)團(tuán)隊都會時不時地遇到程序重構(gòu)問題,然而就是這樣一種被認(rèn)為是現(xiàn)代軟件開發(fā)中必不可少的基本技能,在現(xiàn)實中對重構(gòu)的錯誤理解和使用卻比比皆是。首先,不知道正確使用重構(gòu)的場合,總是等到代碼已經(jīng)腐化不堪的時候才想起重構(gòu);其次,在面對需要重構(gòu)的代碼時,沒有選擇標(biāo)準(zhǔn)、無從下手,在接下來的代碼修改過程中不懂得安全、逐步的重構(gòu)方法,將代碼置于危險的境地,難以回收;最后,在構(gòu)建、測試失敗后無法恢復(fù),只能推倒重來。
本文源于如何將應(yīng)用程序從C轉(zhuǎn)換為C++,并強調(diào)對STL(標(biāo)準(zhǔn)模板庫)通用組件的應(yīng)用[1]。通過這種轉(zhuǎn)換可以降低程序的復(fù)雜度,同時提高源代碼的可維護(hù)性。類似下面的數(shù)據(jù)結(jié)構(gòu)是工作的起點:
struct aclEntry {
int aclTarget;
int aclSubject;
int aclResources;
// ...
struct aclEntry *reserved;
struct aclEntry *next;
};
嘗試通過查找、轉(zhuǎn)換和語法調(diào)整,將類似的數(shù)據(jù)結(jié)構(gòu)替換為相應(yīng)的STL等價物,上面的數(shù)據(jù)結(jié)構(gòu)可以在許多模塊中找到。很明顯,代碼復(fù)用是基于復(fù)制-粘貼方式進(jìn)行的,因為除了稍作修改以適用于存儲不同屬性外,代碼看起來幾乎相同。
作為標(biāo)準(zhǔn)模板庫的STL包含數(shù)據(jù)結(jié)構(gòu)的一般定義、算法和相關(guān)概念,這種結(jié)構(gòu)也映射在嘗試移植軟件的過程中。數(shù)據(jù)結(jié)構(gòu)和相關(guān)算法的轉(zhuǎn)換劃分為兩個子任務(wù):第一,更改數(shù)據(jù)結(jié)構(gòu)的定義;第二,處理依賴關(guān)系。
通過將代碼中的復(fù)雜表達(dá)式替換為相應(yīng)的函數(shù)對象,數(shù)據(jù)結(jié)構(gòu)和相關(guān)算法隨之被STL組件所代替。代碼由兩個不同的部分組成,一部分是應(yīng)用程序的特定屬性(如aclTarget,aclResources),另一部分則用于組織(如next)。這兩部分是分離的,需要進(jìn)行針對性的修改。應(yīng)用程序特定信息(數(shù)據(jù)類型)的修改通過以下方式進(jìn)行:
1)從結(jié)構(gòu)中去除組織的相關(guān)信息;
2)添加默認(rèn)的構(gòu)造函數(shù)和析構(gòu)函數(shù)并復(fù)制;
3)為結(jié)構(gòu)定義==和<運算符。
圖1 數(shù)據(jù)結(jié)構(gòu)的替換
圖1顯示了如何將新定義的僅包含基本信息的數(shù)據(jù)類型,通過適當(dāng)?shù)耐ㄓ媒M件實例化。與數(shù)據(jù)組織相關(guān)的信息由STL提供的容器(通用數(shù)據(jù)結(jié)構(gòu))處理。為了提供完整的數(shù)據(jù)類型并實現(xiàn)[2]描述中的標(biāo)準(zhǔn)形式,需要在原有基礎(chǔ)上增加一些C++的特定操作。
struct aclEntry {
int aclTarget;
int aclSubject;
int aclResources;
// ...
aclEntry ();
aclEntry (int target,int subject,int resources);
aclEntry (const aclEntry &);
~aclEntry ();
};
inline operator==(const aclEntry &,const aclEntry &);
inline operator<(const aclEntry &,const aclEntry &);
typedef list
這些替換相對簡單,因為初始化的值必須由現(xiàn)有的初始化例程派生。默認(rèn)構(gòu)造函數(shù)和復(fù)制構(gòu)造函數(shù)是列表模板的必要組成部分。默認(rèn)構(gòu)造函數(shù)只負(fù)責(zé)初始化對象屬性,而復(fù)制構(gòu)造函數(shù)必須將給定對象的值分配給結(jié)構(gòu)中的屬性。
使用通用數(shù)據(jù)結(jié)構(gòu)的實例替換原有數(shù)據(jù)結(jié)構(gòu)后,每個訪問此數(shù)據(jù)結(jié)構(gòu)的功能和模塊都必須做相應(yīng)修改。因為C語言各模塊之間的接口轉(zhuǎn)換相當(dāng)復(fù)雜,所以整個C到C++轉(zhuǎn)換過程的大部分工作都在于此。對不同模塊之間的依賴關(guān)系進(jìn)行了如下分類:訪問依賴、函數(shù)依賴和結(jié)構(gòu)依賴。
C語言使用指針來直接訪問數(shù)據(jù)結(jié)構(gòu)的屬性。STL中容器的訪問方法被稱為迭代器,它為訪問列表項提供了的良好接口。如:
表達(dá)式
aclEntry *ptr;
ptr = ptr->next;
或
aclEntry *ptr;
ptr->subject = 3;
必須替換為
acl_list::iterator itr;
itr++;
或
acl_list::iterator itr;
(*itr).subject = 3;
這些組件的引入導(dǎo)致需要對保存在列表和列表本身中的變量(如添加/刪除新條目)的所有讀和寫操作進(jìn)行修改。另外,數(shù)據(jù)封裝使得為條目的所有屬性添加訪問方法成為必然。但是,嚴(yán)格的數(shù)據(jù)封裝并不能通過轉(zhuǎn)換實現(xiàn),訪問依賴可以通過一個相對簡單的搜索和替換程序來解決[3]。
另一項要完成的任務(wù)是修改新條目的創(chuàng)建方法,不是使用malloc()或free()來創(chuàng)建或刪除一個項目,而是替換成insert和delete方法,并且為初始化引入了一個構(gòu)造函數(shù)。
一些程序員經(jīng)常會將結(jié)構(gòu)中屬性的順序?qū)?yīng)結(jié)構(gòu)中每個變量在內(nèi)存中的順序。這種技術(shù)被稱為“造型”,用于將不同類型的數(shù)據(jù)項放入同一個列表中,來實現(xiàn)C++中的多態(tài)。因為“造型”不能映射為通用組件,并且STL的初始化只允許將一種類型的對象放入同一個容器中,所以數(shù)據(jù)結(jié)構(gòu)轉(zhuǎn)換為STL容器會出現(xiàn)十分嚴(yán)重的問題。將這些數(shù)據(jù)項插入到STL容器中時,可以使用繼承和一個中間量來解決丟失信息的問題。
由于C語言中指向的列表項的指針可以用來指向單個項目或指向整個列表,所以如何區(qū)分特定項目和整個列表就成了一個更復(fù)雜的問題。在工作中會選擇下面啟發(fā)式的方法:
1)如果變量被取消引用以獲得對條目屬性的訪問權(quán)限,則該變量被轉(zhuǎn)換為迭代器。
2)如果只有下一個指針受變量范圍內(nèi)語句的影響,則該變量的類型為列表。
區(qū)分列表和單個項目對于下一種依賴關(guān)系的解決也至關(guān)重要。
當(dāng)一個數(shù)據(jù)結(jié)構(gòu)參與函數(shù)的聲明和定義,或者作為函數(shù)的參數(shù)或返回值時,就會發(fā)生這種依賴。如:
這種函數(shù)聲明:
void f(struct aclEntry *)
必須替換為:
f(acl_list::iterator)
或
f(acl_list &)
與此同時,函數(shù)的原型聲明也要隨之改變。問題是要確定在整個函數(shù)中,是需要整個列表還是只需要某個特定的條目。分析函數(shù)的功能是解決這個問題的唯一方法。如:
函數(shù)聲明:
struct aclEntry * f()
不能簡單地替換為:
acl_list::iterator f()
或
acl_list & f()
某些函數(shù)返回一個指向新創(chuàng)建列表開始位置的指針。因此,必須使用另一種技術(shù)來解決這個問題,為返回新創(chuàng)建的列表提供附加參數(shù),并在函數(shù)內(nèi)部進(jìn)行分配。如:
bool f(acl_list &);
同時,將返回值更改為bool,以用于向調(diào)用函數(shù)提供是否發(fā)生錯誤的信息。
除了列表之外,還發(fā)現(xiàn)了一個定義非常類似于列表的數(shù)據(jù)結(jié)構(gòu)——樹。 但是,不可能應(yīng)用適合列表的相同轉(zhuǎn)換。
struct tree {
struct tree *child_list;
struct tree *next_peer;
struct tree *parent;
char label[MAXLABEL];
u_long subid;
int type;
};
在這種情況下,不可能將一個數(shù)據(jù)結(jié)構(gòu)直接映射到通用數(shù)據(jù)結(jié)構(gòu)的實例上,必須通過對數(shù)據(jù)結(jié)構(gòu)的分析才能發(fā)現(xiàn)可能的映射。
因為必須考慮所有可能性以決定如何更改舊數(shù)據(jù)結(jié)構(gòu)的特定實例,所以認(rèn)識和處理這些依賴關(guān)系會耗費大量的時間和精力,但解決這個問題可能會對進(jìn)一步的研究極具意義。
目前為止,本團(tuán)隊成功完成了用通用組件的實例替換簡單數(shù)據(jù)結(jié)構(gòu)和相關(guān)算法的任務(wù)。而復(fù)雜數(shù)據(jù)結(jié)構(gòu)的替換需要更多的支持。例如,上文提到的“樹”,這種數(shù)據(jù)結(jié)構(gòu)可以被通用組件集的一個實例替換,但需要分析所有函數(shù)中使用“樹”這種結(jié)構(gòu)的變量。
通過對結(jié)果程序的評估,代碼總量并沒有太大差異。但是,由于在組織上開銷的減少,C源代碼模塊的大小會減少約10%,這在提高C源代碼中列表的可維護(hù)性方面是不可或缺的。
本文方法也同樣適用于一些不使用通用組件的C++程序。由于語言具備封裝性和繼承性,C++程序的轉(zhuǎn)換過程會更容易。
C++的模板為本文的方法提供了基礎(chǔ),可以為將來的開發(fā)提供更多可能性。這已經(jīng)在一篇關(guān)于程序通用的論文中得到證明[4]。模板也可以用來配置子組件[5]。本團(tuán)隊目前正致力于將這些方法整合到重構(gòu)研究中。
對于開發(fā)、構(gòu)建和測試環(huán)境,C/C++領(lǐng)域尤其嚴(yán)重,很多軟件開發(fā)人員連一個好用的IDE都找不到,更不要說眾所周知的構(gòu)建速度慢、自動化測試匱乏等一系列問題。本團(tuán)隊提出了適用于C/C++程序的重構(gòu)方法,它利用通用組件(數(shù)據(jù)結(jié)構(gòu)和算法)來替代編程中通過復(fù)制-粘貼方式獲得的代碼段。由于對程序進(jìn)行了封裝和局部性原理,數(shù)據(jù)結(jié)構(gòu)的組織和內(nèi)存管理與用戶特定數(shù)據(jù)類型分開。看似簡單的數(shù)據(jù)結(jié)構(gòu)替換過程,卻極大地提高了源代碼的可維護(hù)性。
雖然對復(fù)雜數(shù)據(jù)結(jié)構(gòu)的替換仍然是一個難點,但可以實證檢驗?zāi)硞€數(shù)據(jù)結(jié)構(gòu)到相關(guān)通用組件的實例的映射。這需要深入的分析以及更多的變通,因為數(shù)據(jù)的組織無法通過一種一對一的映射進(jìn)行轉(zhuǎn)換。
本團(tuán)隊下一步的工作是要在設(shè)計的過程中做盡可能的改善,而不依賴重構(gòu)。在設(shè)計的過程中要考慮到盡可能地減少重構(gòu),減少維護(hù)和升級的開銷,這中間也是一個博弈的過程,需要在各種利益之間取得平衡。