讓軟件改進過程實現自動化

作者:Joe Walker
顧愷翻譯 
來源:賽迪技術網
發文時間:2004.01.06

概要

幾年前每個人都被Quality 軟件的問題所困擾。Quality 中的Q 之所以大寫就是爲了強調單詞的重要性。引入了消耗幾個小時的程序和檢查過程、以及大量的文書工作,就是爲了設法減少軟件的故障率。就是前不久,這種趨勢已經演變爲更爲輕便的處理過程,該過程使用了輕便的軟件和極端編程。這仍然是一個積極的轉變因爲保證軟件品質仍然是重要的。有些分析家估計:有60%的軟件故障可自動檢測出來。那麼我們能做的就是在昨天的質量裝置恐龍和今天的困惑之間建一座橋樑,以便讓問題解決的又快又代價低?作者Joe Walker 測試了不同類型的故障和七種工具以幫助你揭示他們的真面目。

有許多免費工具可以輕而易舉的報告你的代碼有什麼故障,然後幫助你改進代碼的質量。而且因爲故障常常是主觀方面的,大部分工具可以配置以適合你的本地代碼類型。

但是一開始你就有減少故障的義務。這聽起來好像是一個簡單的問題——肯定對於每個人來說軟件質量必須擺在第一位。但是,花費最少時間來修改代碼往往看起來更加合理一些。任何保存壽命短的軟件應該編輯的儘可能少,而且編寫沒有人閱讀的單行文檔也是毫無意義的。我在想有多少塊文檔花費在編寫和檢查方面的時間比某人閱讀他們節省的時間多呢?因此,我們的目的就是把值得我們提高作品質量的工作和浪費資源的工作區別開來。

你想在提高代碼質量方面走多遠?

存在以下幾種故障種類。我給出的工具在它們攻擊的故障類型方面有區別。我根據他們的嚴重性確定了四種不同的故障類,從困擾用戶的普通故障到阻止代碼重新使用的有組織的故障都有:

·Current bugs: 當前故障阻止軟件的正確運行。它們困擾用戶並且可(或者應該)通過你的軟件測試顯示出來。當前故障與潛在故障截然不同。

·Latent bugs: 這些故障當前不產生問題。但是它們潛伏着,在以後纔出現。阿里安5號火箭的失敗就是由於一個浮點到整數轉換錯誤引起的,當前面的火箭速度較慢時處於睡眠期;但是速度較快的阿里安 5卻觸發了這個問題。千年病毒也是一個潛伏故障的例子,只有當環境改變時它才浮出水面。使用傳統的測試方法測試潛伏病毒要困難得多,並且找到他們就需要具有遠見的人詢問:“什麼改變才能讓故障從黑暗中走出來?

·Accident-waiting-to-happen bugs: 代碼可以既簡單又安全,或者他也可以是一團糟,等着絆倒那些對他們的編輯結果想得不多的可憐的程序員。一般而言,代碼越老,就越有可能出現明顯生鏽的刀口;並且某些工業證據也表明:長時間沒有重構的代碼一般七年之後也就不再需要重新編寫了。

·Poorly organized bugs: 組織不好的代碼不容易重新使用並且常常包含對代碼的過多依賴,其實有些是不必要的。因此它更有可能受其他代碼位中的故障影響。編碼標準致力於解決這種類型的問題,但是編碼標準不能完全解決問題。他們不能停止問題如循環的依賴性,它使得你毫無機會來維持可重新使用的代碼。

這些問題根據他們的嚴重性組織的比較粗糙。當前故障一般比潛伏故障更難以解決,而潛伏故障依次的比等待發生的故障或者組織故障更加令人擔心。

下列代碼包含了上述四種故障案例。認出它們後獎勵一下你自己:


1: public static void main(String[] args)
2: {
3:     System.out.println(toUpperCase('q'));
4: }
5:
6: private static char toUpperCase(char main)
7: {
8:     return (char) (main - 31);
9: }

這裏有一個當前故障因爲行8應該讀取:


這種簡單問題不解決,程序也不會運轉。與其辛苦計算出q的大寫字母爲Q,還不如設法勸說我們相信答案就是R。

這裏有一個潛伏故障因爲遲早有人會設法得到ß 或者某數的大寫版本,而不是任何明智的東西。

一個小小的等待發生事故的故障是由於行6中的某些傻變量名引起的。main 並沒有真正的描述什麼正在進行,而且還用方法名來迷惑我們。更好的代碼如下:

如果該代碼描述了爲什麼她要從小寫字母盤上的字符減去32,那麼它會安全的多。

最後如果你將toUpperCase()方法變爲公用的,併爲方便其他類的使用將它變爲實際類——甚至你可以使用下面的標準Sun微系統提供的方法,那麼代碼就組織得更好一些:



你想在哪裏止住?

你什麼時候停止調整和提高你的代碼呢?這個問題沒有一個真正的答案;這取決於你和你的項目。商業項目常常只關注於ROI (投資回報),因此它主要解決當前故障,並且通過實現編碼標準解決這個問題很容易。

我們常常看到商業項目無意中採取了某些步驟來阻止潛伏故障的解決,它們實現了一個嚴格的變化控制程序,該程序將代碼修改與產生的故障聯繫在一起來幫助測試。要開發隊伍足夠忠誠於軟件品質,以致於關心某人的軟件而且不設置障礙,這實在是太難了。

在更具有啓迪性的開發環境裏,大家都承認重構是創造高品質軟件的至關重要的組成部分。重構就是逐步穩固你代碼的組織以避免等待發生事故的過程。

但是即使是最關注品質的開發者也有他們的限制。你真的關心你註釋中的標點符號嗎?或者你真的關心方法是否是以最合理的順序聲明的嗎?

Reasoning Inc.最近研究了不同商業和開源項目的品質,並據此發表大字標題。該公司針對不同的商業選擇分析了Linux TCP/IP堆棧的源代碼、Apache Web服務器、和Tomcat 。有趣的是, Reasoning公司還運行了下面描述的免費工具版本,然後手工分析結果以刪除錯誤證據。他的結果出現了一些異常,但是它表明:開源關注於品質由此產生的回報比品質軟件本身產生的回報明顯要大得多。

我鑑別的七種工具可幫助顯示故障,他們與Reasoning 公司的方法類似,並且在某些情況下也能幫助解決這些故障。我還將這些工具用於我最近的項目之一:DocTree。 DocTree 產生一個動態的裝載Webpage,它包含到一萬個類的Java文檔的深度鏈接,這些類來自於許多重要的Java軟件項目。DocTree不是一個很大的項目,但是它足夠成爲這七種工具的有用的測試案例。該資源可從java.net (見 Resources)處下載,所以你可以利用Ant看到這些工具是如何在同一個基礎上運行。 有幾個工具有繪圖用戶界面(GUIs)。但是,把它們當作你的構造過程(如,從一個Ant構造腳本)的一部分來運行可能更有意義。運行這些工具花的時間越少,繼續提高你的軟件就越有可能。

使用工具如CruiseControl、 Anthill是相當容易的,或者甚至是使用簡單的cron 每晚寄出當作電子郵件給予你結果的構造,也是相當容易的。在建立了這樣一個程序後,它就比GUI容易使用。

讓我們看看這七種工具:

·FindBugs

·PMD/CPD

·Checkstyle

·Jalopy/Jacobe

·JDepend

·JUnit

·Eclipse

FindBugs

FindBugs主要解決當前和潛伏故障。它尋找普通問題和常常顯示某處出錯的小處代碼。

FindBugs顯示的一些故障案例包括:

·返回不穩定靜態數據的方法

·Get方法不同步的地方顯示Get方法;set方法同步的地方顯示set方法

·Iterator類定義過程中出現的問題

·無意義的控制流程語句

·存在NullPointerException的地方;引用值與Null的多餘比較的地方

FindBugs 處於非常活躍的開發中,並且你可以擴展它,通過插入機制將自定義檢查包括進來。

FindBugs在DocTree找到了一個相當嚴重的潛伏故障:一個類定義了equals(),但沒有定義hashcode()。這在當前配置中不會引起任何問題。但是,如果以後在DocTree迭代中,使用了Hashtable 或者HashMap中的類,就會出現假的結果。FindBugs也找到了一些次要的潛伏故障如異常上的信息流一直沒有關閉直到出現碎片整理和其他次要問題。隨着顯示的問題的增長,你軟件上的故障數就會減少。

FindBugs使用字節碼分析來生成他的故障報告。

下面是創建FindBugs 報告的Ant 目標的DocTree項目片段:



PMD/CPD

PMD是非常可自定義的,並能找出這四種故障類的問題。但是,他最擅長於尋找組織故障和事故等待發生的故障。與FindBugs不同,PMD使用源代碼分析,與編譯器工作方式類似,除了它生成故障報告而不是字節碼之外。PMD 是我爲這篇文章測試的最可自定義的工具之一,主要是因爲她有一個很酷的特徵:允許你使用XPath引擎訪問源代碼解析器的輸出。這個解析器產生一棵樹,樹中的節點屬於你程序的一部分。所以一個類節點將包含用於每個變量和方法的字節點。方法節點也會包含用於該方法內的語句的字節點。如果他是一篇使用XPath的XML文檔,PMD允許你訪問這棵樹。

所以一旦你理解了XPath和關於解析器如何從你的源代碼中生成節點樹的一些基本概念,你就能輕而易舉的添加新的法則。例如:來自PMD文檔的下列表達式演示瞭如何檢查當語句是否使用了波形括號:

它將尋找樹(//)中任何地方沒有包含Block(用於某些波形括號的解析器術語)的當語句。

PMD檢測的問題案例有:

·空塊

·爲方法計算的過多的參數。

·過長的類或者過長的輸入清單

·不能使用的域,方法和變量

·壞掉的雙檢查鎖定

這些檢查中有些是很主觀的。個人最好封裝的組件就是它過長的類。所以,感謝你能配置PMD來檢查你認爲重要的問題。當你第一次使用PMD來檢查更大的項目時跳躍式檢查尤爲重要。如果你一開始運行的檢查爲空,PMD將生成七年的解決價值,所以從更加重要的檢查開始很有好處。

CPD (複製/粘貼 檢測器)是檢測項目內的複製和粘貼代碼的相關設備。大量的複製代碼顯示出:開發者不使用繼承或者創建庫就可以找出類似的代碼和複製的任何東西、故障等等。

來自DocTree 項目的PMD Ant 目標看起來如下:



Checkstyle

Checkstyle 與PMD相當相似,儘管它在尋找組織故障方面更爲出色。與 PMD一樣,它使用編譯器前端來生成他的故障報告。PMD提供更廣泛的範圍內的更多檢查。但是,Checkstyle是更可配置的。雖然某些檢查會交迭,但Checkstyle的檢查中有許多不在PMD內,反之亦然。對大部分來說只要擁有他們中的一個便以足夠。

Checkstyle 可尋找:

·不能使用的或者多餘的輸入

·空格更好的地方不使用跳格符,反之亦然

·不遵循命名標準的變量、方法或者類

·過分複雜的分配或者返回語句

來自DocTree的Checkstyle Ant目標如下:



Jalopy/Jacobe

Jalopy和Jacobe是幫助解決事故等待發生故障和組織故障的代碼格式程序。Jalopy是開源,但是他沒有出現在活躍的開發中。Jacobe 可免費使用,但不是開源。

記住使用任何代碼格式程序可嚴重的毀壞你使用源代碼控制系統揭示某些代碼後的歷史紀錄的功能。當引入故障時使用源代碼控制系統分析發生的變化是很普通的。但是,源代碼格式程序的普遍使用不必作任何實際修改就能夠改變你項目中的源代碼行的重要率。如果你需要重新格式化你項目的源代碼,你應該儘可能早的完成,儘管接受它你不能夠追蹤重新格式化之前的修改,或者當該代碼由於某些其他原因需要編輯的時候,逐步重新格式化。

JDepend

JDepend在你的源代碼周圍生成了無數個度量,包括來自於主序列的傳入和傳出的耦合和分配。不幸的是,據我的經驗,這些度量常常是毫無意義的,除非你已經理解了你正在測試的代碼;並且如果你理解了,他也只告訴你你已經知道的東西,但是,JDepend對於識別循環依賴性特別有用。循環依賴性是組織故障,它可引起維護問題和產生不能重新使用的代碼。

例如:如果你有三個軟件包A, B和C,並且 A依賴於B, B依賴於C, 而且C 依賴於 A,那麼如果不都得到你就不可能重新使用他們。如果這些軟件包中任何一個使用了GUI代碼,那麼當你不注意的下載了GUI代碼到你的企業JavaBeans (EJB)或者 servlet中,即使你感興趣的代碼不是GUI代碼,那麼你就會想爲什麼你的服務器代碼快要死了。

來自於DocTree 的JDepend Ant 目標看起來如下:



JUnit

JUnit是大家熟知的工具,它可幫助你爲你的軟件編寫自動的單元測試;我這裏不想詳細討論它爲什麼那麼有名。想了解更多信息請看Resources 。我想說在本文提到的所有工具中,JUnit 最有可能幫助你找出你代碼中的故障,儘管要付出代價。JUnit需要你花費相當多的時間爲你的代碼編寫測試案例,但是很有可能這個時間花得很值。

Eclipse

IDEs 總是一個使人激動的話題。當你考慮免費工具時,Eclipse有幾種可集成到編譯器上的檢測工具如 FindBugs ,並且正在開發中的第3 版本包含了更多這種檢測。Eclipse 海配送一個代碼格式程序,他雖然不如Jalopy 或者Jacobe高級,但是更加可行因爲它可以裝在編輯器內。

Eclipse 編譯器在當前和即將上市的版本中執行的高級檢測包括:

·意外的boolean賦值 (e.g., if (a = b))

·不能到達的捕捉塊

·隱藏字段或者變量的局部變量聲明

·不能使用的變量、輸入、字段和給出的聲明

項目集成

將你的項目與jar和配置文件集成而且不使你的項目過大或者迷惑 jar文件,這是一個挑戰。如果你的代碼倉庫包含許多項目,你恰好想將這些工具用在這些項目上,那麼你將面臨某些倉庫嚴重膨脹的危險。

我建議你建立一個包含你想使用的工具的獨立項目,然後從你的Ant 構造文件中引用他們。這樣就不出現在想要使用工具的隊組成員中分配工具的問題。對於那些不需要使用工具的成員來說他們是可選的,並且他們不會佔據倉庫太多的空間。

故障解決

自動品質檢測可減少你軟件中故障數,從而增強它的品質和可持續性。在你安裝了工具之後,你可以用非常小的代價——時間和金錢都是——來維護質量控制。在實現本文列出的任何策略之前,明白的定義你對於不同等級的品質有多看重以及消除某種故障有多值得,這是很重要的。這種分析對於不同的項目將產生不同的結果。好消息是調試工具將你從沉重的基礎工作中解救出來,並且允許你自主地安排他們的使用和你管理軟件品質的方法。


<target name="web.jdepend" depends="jar">
  <mkdir dir="${target.temp}/jdepend"/>
  <mkdir dir="${target.web}/jdepend"/>
  <jdepend format="xml" fork="yes"
      outputfile="${target.temp}/jdepend/jdepend-report.xml">
    <sourcespath>
      <pathelement path="${source.java}/main"/>
    </sourcespath>
    <classpath>
      <dirset dir="${target.classes}"/>
      <fileset dir="${source.jar}" includes="**/*.jar"/>
      <fileset dir="${support.tools}/jdepend24" includes="**/*.jar"/>
    </classpath>
  </jdepend>
  <style
      basedir="${target.temp}/jdepend"
      destdir="${target.web}/jdepend"
      includes="jdepend-report.xml"
      style="${support.tools}/jdepend26/jdepend.xsl"/>
</target>

<target name="web.checkstyle">
  <mkdir dir="${target.temp}/checkstyle"/>
  <mkdir dir="${target.web}/checkstyle"/>
  <taskdef resource="checkstyletask.properties">
    <classpath>
      <fileset dir="${support.tools}/checkstyle31"
          includes="**/*.jar"/>
    </classpath>
  </taskdef>
  <copy file="${support.tools}/checkstyle31/custom.xml"
      overwrite="true"
      tofile="${target.temp}/checkstyle/custom.xml">
    <filterset>
      <filter token="source.java"
          value="${basedir}/${source.java}"/>
      <filter token="target.checkstyle"
          value="${basedir}/${target.temp}/checkstyle"/>
    </filterset>
  </copy>
  <checkstyle config="${target.temp}/checkstyle/custom.xml"
      failOnViolation="false">
    <fileset dir="${source.java}/main"
        includes="**/*.java"/>
    <formatter type="plain"/>
    <formatter type="xml"
        toFile="${target.temp}/checkstyle/checkstyle_errors.xml"/>
  </checkstyle>
  <style
      basedir="${target.temp}/checkstyle"
      destdir="${target.web}/checkstyle"
      includes="checkstyle_errors.xml"
      style="${support.tools}/checkstyle31/checkstyle-noframes.xsl"/>
</target>

<target name="web.pmd">
  <taskdef name="pmd" classname="net.sourceforge.pmd.ant.PMDTask">
    <classpath>
      <fileset dir="${support.tools}/pmd121" includes="**/*.jar"/>
    </classpath>
  </taskdef>
  <mkdir dir="${target.web}/pmd"/>
  <pmd rulesetfiles="${basedir}/${support.tools}/pmd121/ruleset.xml"
      shortFilenames="true">
    <formatter type="html" toFile="${target.web}/pmd/index.html"/>
    <fileset dir="${source.java}/main" includes="**/*.java"/>
  </pmd>
</target>

//WhileStatement[not(Statement/Block)]

<target name="web.findbugs" depends="jar">
<mkdir dir="${target.web}/findbugs"/>
<taskdef name="findbugs"
    classname="edu.umd.cs.findbugs.anttask.FindBugsTask">
  <classpath>
    <fileset dir="${support.tools}/findbugs066"
        includes="**/*.jar"/>
  </classpath>
</taskdef>
<findbugs home="${support.tools}/findbugs066"
    output="text"
    outputFile="${target.web}/findbugs/report.txt"
    reportLevel="low" sort="text">
  <class location="${target.jar}/${ant.project.name}.jar"/>
  <sourcePath path="${source.java}/main"/>
</findbugs>
</target>

3: System.out.println(Character.toUpperCase('q'));

6: private static char toUpperCase(char lower)

8: return (char) (main - 32);
發佈了25 篇原創文章 · 獲贊 2 · 訪問量 7萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章