孫美榮 楊春花
摘 要: 在現(xiàn)代軟件開發(fā)和維護中重構(gòu)是提高軟件可維護性和軟件質(zhì)量的常用手段。而大量重構(gòu)模式摻雜在日常的bug修復、功能增加等代碼變更中,使得變更理解變得非常復雜。因此,提出一種對常見的抽取方法和抽取類重構(gòu)模式的識別算法。研究基于工具ChangeDistiller和JDiff分別獲取變更類型和抽取代碼塊,通過判斷移動后的代碼塊與原文件變更代碼的關(guān)系,識別采取的重構(gòu)模式。該算法在4個開源項目中進行實驗,其平均準確率在80%左右。
關(guān)鍵詞: 重構(gòu)模式;抽取方法;抽取類
Abstract:Refactoring is a common way of improving software maintainability and software quality in modern software development and maintenance. In daily revision refactoring patterns are usually mixed with code changes accomplishing other tasks such as bug fixing and feature addition which makes the change understanding very complicated. The paper proposes an identifying algorithm for refactoring patterns including the extract method and extract class. It is based on ChangeDistiller and JDiff that are used to get code changes types and extract code blocks respectively. Then the algorithm will identify the refactoring pattern according to the characteristics of the relationship between the changed codes and the original codes. The algorithm has been tested on 4 open source projects with an average 80% accuracy.
Key words: refactoring patterns;extract method;extract class
引言
重構(gòu)[1]是現(xiàn)代軟件開發(fā)和維護中用于提高軟件可維護性和軟件質(zhì)量的常用手段?,F(xiàn)代的軟件開發(fā)一般基于版本管理系統(tǒng)設(shè)計實現(xiàn),軟件工程師為了維護系統(tǒng)或提高系統(tǒng)的性能每天會提交大量的代碼。而大量重構(gòu)模式摻雜在日常的bug修復、功能增加等代碼變更中,使得代碼評審者和軟件工程師在理解代碼時不得不對代碼進行人工探查,以區(qū)分哪些變更的代碼是重構(gòu),哪些不是。因此,為了使得變更的代碼易于理解,將重構(gòu)模式從代碼變更中隔離出來是非常必要的。
重構(gòu)[2]是一種基于規(guī)則、經(jīng)過訓練、有條不紊的程序整理方法,在整理過程中可以將不小心引入的錯誤降低。重構(gòu)是處理代碼壞味的一種常用手段。當前,在對重構(gòu)方法的研究方面包括基于K-最近鄰的C克隆代碼重構(gòu)方法[3]、使用抽象語法樹和靜態(tài)分析的克隆代碼自重構(gòu)方法[3]、基于抽象語法樹和多態(tài)機制的復雜條件語句自動重構(gòu)研究[4]、長方法壞味重構(gòu)選擇策略[5]等等。
重構(gòu)模式的識別是在變更后的代碼中尋找符合特定重構(gòu)模式的代碼修改,是重構(gòu)的反過程。劉陽[6]等人提出了一種重構(gòu)檢測算法,是基于版本元素匹配原理,對函數(shù)抽取重構(gòu)進行了識別,但是并沒有涉及其它類型的重構(gòu)模式。
通過對4個開源項目的變更代碼進行探查,研究發(fā)現(xiàn)抽取方法、抽取類等是最為常見的重構(gòu)模式。因此,本文對這2種重構(gòu)模式進行了研究,提出了識別的算法。
1 抽取方法與抽取類的重構(gòu)模式的識別
1.1 抽取方法與抽取類模式
圖1是抽取方法(Extract Method)模式的示例。其中,V1是變更前的版本,V2是變更后的版本。對比兩版本,V2中多了一個含參數(shù)的新增方法printDetails,且V1 中4~5行的內(nèi)容抽取到V2的新增方法5~7行中,則在V2相對于V1刪除代碼的位置有對方法printDetails進行調(diào)用。
抽取方法的表現(xiàn)形式有3種,分別是:沒有局部變量的代碼塊移動、有局部變量的代碼塊移動、對局部變量再賦值。圖1中的抽取方法重構(gòu)是有局部變量代碼塊移動模式。
抽取類(Extract Class)[7]重構(gòu)模式,一般用于處理過長的類。一個類如果包含過多的功能及屬性,會導致這個類過于臃腫。為了提高類的高內(nèi)聚,低耦合,就會將一些不必要的或不常用的方法提煉到另一個類中,來為這個類服務(wù)。如圖2 是一個抽取類模式的示例,用類圖形式展示。類Person中過多的屬性officeAreaCode和officeNumber以及功能代碼getTelephoneNumber()被抽取到了一個新類TelephoneNumber中,且在移動代碼的地方增加對新類的引用。
1.2 識別方法
1.2.1 抽取方法模式識別
根據(jù)上述抽取方法和抽取類模式的例子,不難發(fā)現(xiàn),屬于抽取方法和抽取類兩種模式的代碼變更具備如下3個特性:
(1)文件中有新增方法或提交的revision_id中含有新增類文件。
(2)抽取代碼塊移動到某個方法中。
(3)在刪除代碼的位置有對該方法的引用。
為了識別這3個特性,研究借用ChangeDistiller(https://bitbucket.org/sealuzh/tools-changedistiller/src/)工具獲得一個文件變更前后所有的代碼變更類型。這是Fluri[8]等人編寫的一個Tree differ 算法,對變更前后抽象語法樹進行對比,獲取分類的變更。同時,也可以區(qū)別多種方法類型的變化或類等級上的變化。
但是ChangeDistiller只能獲取原有方法中的新增語句,不能獲取新增方法中的語句體。為此,研究又對ChangeDistiller進行了些許擴展,使其可以返回每個代碼變更的代碼行或代碼行范圍。例如,對于一個新增方法printDetails,ChangeDistiller返回的變更類型為ADDITIONAL_FUNCTIONALITY: printDetails()。擴展后,將返回該方法的行號范圍5~10。
基于每個新增方法變更的行號范圍,研究中借用文本比較工具JDiff(https://maven.apache.org/archives/maven-1,x/plugins/jdiff)來獲取該方法的方法體。JDiff是一種面向行的文本比較(text diff)工具,用于顯示同一文件2個版本之間的更改。
另外,對于新增方法中的代碼塊和刪除的代碼塊之間的移動關(guān)系判斷,進一步借助了Levevshtein(https://en.wikipedia.org/wiki/Levenshtein_distance)算法。Levevshtein是一種計算2個字符串間的差異程度的字符串度量(string metric)算法,即一個單詞變成另一個單詞要求的最少單個字符編輯數(shù)量(如:刪除、插入和替換)。
圖3顯示了該算法的框架,其中l(wèi)eft,right分別表示變更前后的兩文件,left是變更前版本,right表示變更后版本。研究可得算法設(shè)計流程如下:
(1)代碼變更抽取。通過ChangeDistiller獲取所有類型的代碼變更,包括新增的方法、刪除的語句、原方法中新增的語句等。一個代碼變更由變更類型(ChangeType)、變更實體(ChangeEntiy)和變更雙親實體(ParentEntity)構(gòu)成。根據(jù)研究中對ChangeDistiller的擴展,對每個新增方法madd,將對應(yīng)一個行號范圍(beginline endline)。
(2)方法體語句的獲取。根據(jù)新增方法的行號范圍通過JDiff獲取代碼塊。此步驟的結(jié)果是一個元組的集合EMADD={
(3)代碼變更分組。通過上述步驟(1),所有刪除的語句行,以及原方法中新增的語句行都已經(jīng)獲得。然后就可將所有刪除和新增的語句根據(jù)有關(guān)的方法父類實體進行分組。此步驟的結(jié)果是一個元組的集合MCHANGE={ < mname linsert ldelete >| mname是方法名稱,linsert =linsert1 ... linsertn是該方法內(nèi)部增加的語句,ldelete = ldelete1 ... ldeletem是刪除的語句}。
(4)代碼塊抽取判定。步驟(2)中獲得的所有新增方法的代碼行塊,與步驟(3)中獲取的所有刪除的代碼塊,進行相似度比較,判斷是否存在代碼塊抽取。
(5)模式判定。若塊b1=
V2:METHOD_INVOCATION:printDetails(outstanding
1.2.2 抽取類模式識別
抽取類模式識別與抽取方法模式識別的步驟基本相似;算法框架略。在此,可給出算法步驟分述如下。
(1)代碼變更抽取。通過ChangeDistiller獲取所有類型的代碼變更。擴展方法獲取revision_id中新增類文件fnew,及所有方法MADD。
(2)代碼變更分組。 同抽取方法步驟(3)。
(3)方法體語句的獲取。根據(jù)步驟(2)中的MCHANGE,在fnew中獲取與ldelete相似的代碼塊ladd。根據(jù)ladd歸屬的方法分組EMADD={
(4)同抽取方法步驟(4)。
(5)模式判定。若塊b1=
V2:FIELD: person.officeTelephone: TelephoneNumber;officeTelephone.getTelephoneNumber()
1.3 算法
編寫抽取方法與抽取類重構(gòu)模式識別的偽代碼算法。本次研究中,關(guān)鍵設(shè)計的偽代碼可見如下:
對給定某個程序的2個相鄰版本
算法偽代碼中第5行通過ChangeDistiller可知f是否為變更文件;第13行中的mname是根據(jù)Schange.cparententity獲得;第28行中的新增文件fnew是根據(jù)本次提交的revision_id與上次revision_id-1進行比較獲得;第29行根據(jù)類的反射機制獲取fnew中的所有方法Madd;第47行返回所有成功識別的重構(gòu)模式集合P。
2 實驗驗證
2.1 數(shù)據(jù)源
為了驗證實驗的可執(zhí)行性,過程中通過minigit(https://github.com/3ofcoins/minigit/)工具獲取了4個開源項目進行驗證。分別為:jEdit、maven、goole_guice、eclipse,利用MySql根據(jù)各項目提交的日志篩選出將近10年的重構(gòu)revision_id,然后對篩選出的重構(gòu)revision_id進行人工檢測,表1即為篩選出的抽取方法、抽取類兩種重構(gòu)模式的基本詳細信息。具體內(nèi)容詳述如下。
(1)jEdit( http://sourceforge.net/project/jedit/)是一個跨平臺的文本編輯器。數(shù)據(jù)獲取時間段為:1998/09/27~2012/08/08,本次實驗人工檢測JEdit發(fā)生重構(gòu)的revision_id數(shù)目為45個版本,含有的總文件數(shù)目548個。
(2)maven(http://maven.apache.org/dowload.cgi)是對象模型(POM),可以通過一小段描述信息來管理項目的構(gòu)建、報告和文檔的軟件項目管理工具。數(shù)據(jù)獲取時間段為:2003/09/02~2014/01/29,本次實驗人工檢測maven發(fā)生重構(gòu)的revision_id數(shù)目為123個版本,含有的總文件數(shù)目為1 494個。
(3)goole_guice(https://github.com/apress/goo-gle guice),讀作"juice")是超輕量級的,下一代的,為Java 5及后續(xù)版本設(shè)計的依賴注入容器。數(shù)據(jù)獲取時間段為:2006/08/23~2013/12/12,本次實驗人工檢測goole_guice發(fā)生重構(gòu)的revision_id數(shù)目為25個版本,含有的總文件數(shù)目為567個。
(4)eclipse(http://www.eclipse.org/downloads/eclipse-packages/)是一個開放源代碼的、基于Java的可擴展開發(fā)平臺。數(shù)據(jù)獲取時間段為:2001/06/23~2013/10/16,本次實驗人工檢測eclipse發(fā)生重構(gòu)的revision_id數(shù)目為20個版本,含有的總文件數(shù)目為175個。
2.2 結(jié)果及實驗分析
圖4、5是變更前后的兩文件,圖6是該算法對這一變更檢測的詳細信息輸出。
圖4、圖5給出了ChangeDistiller獲取的新增方法injectMirror,以及由JDiff結(jié)合Levenshtein獲取的抽取代碼塊,ML(502~514)方法體行號范圍。
該實驗對4個Java開源項目中的數(shù)據(jù)進行驗證,每個項目中有30~100個重構(gòu)revison_id版本,對表1中的數(shù)據(jù)進行驗證,因為每個revison_id版本中包含多個文件,所以重構(gòu)檢測不止對一個文件進行操作。通過實驗后檢測得到的實驗結(jié)果,可見表2。
通過表2可以得出,該實驗測定運行的抽取方法和抽取類的檢測結(jié)果的平均準確率為80%,準確率在75%~82.5%之間略有波動。準確率沒有達到100%的原因與相似度值選取有關(guān)。本文是借用Levenshtein算法比較兩字符串的相似性,對于閾值的選取是關(guān)鍵原因。閾值選取過高,降低了準確率;閾值選取過小,則會大大降低查準率。因此,對代碼相似度算法的選取也是后續(xù)研發(fā)的重要工作之一。
3 結(jié)束語
本文中,研究提出一種基于ChangeDistiller和文本差異工具識別變更代碼中重構(gòu)模式的算法,并通過4個開源試驗成功識別了抽取方法和抽取類兩種重構(gòu)模式。未來工作可有針對性地圍繞如下工作展開研究:
(1)獲取更多的相關(guān)數(shù)據(jù)進行驗證。
(2)對Extract Superclass、Move Method等模式進行研究。
參考文獻
[1] MURPHYHILL E PARNIN C BLACK A P. How we refactor and how we know it[J]. IEEE 31st international conference on Software Engineering. Vancouver BC Canada:IEEE 2009:287-297.
[2] FOWLER M BECK K ROBERTS D et al. Refactoring improving the design of existing code[M]. Sebastopol CA:Addison-Wesley Professional 1999.
[3] 于冬琦 彭鑫 趙文耘. 使用抽象語法樹和靜態(tài)分析的克隆代碼自動重構(gòu)方法[J]. 小型微型計算機系統(tǒng) 2009 30(9):1752-1760.
[4] 劉偉 胡志剛 劉宏韜. 基于抽象語法樹和多態(tài)機制的復雜條件語句自動重構(gòu)研究[J]. 電子科技大學學報 2014 43(5):736-741.
[5] 馬飛飛 吳海濤. 長方法壞味重構(gòu)選擇策略[J]. 計算機應(yīng)用 2014,34(s1):284-286,293.
[6] 劉陽 劉秋榮 劉輝. 函數(shù)抽取重構(gòu)的自動檢測方法[J]. 計算機科學 2015 42(12):105-107.
[7] FOKAEFS M TSANTALIS N STROULIA E et al. Identification and application of Extract Class refactorings in object-oriented systems[J]. Journal of Systems & Software 2012 85(10):2241-2260.
[8] FLURI B GALL H C. Classifying change types for qualifying change couplings[C]//International Conference on Program Comprehension (2006). Athens Greece:IEEE 2006:35-45.