精益求精,抑或得過且過

作者: agiledon  發布時間: 2011-02-28 21:57  閱讀: 1518 次  推薦: 0   原文鏈接   [收藏]  

  程序員面臨的最痛苦之事,莫過于修改舊代碼;如果還有比這更痛苦的,就是修改糟糕透頂,亂得一團糟的爛代碼。最近因為手底下一幫程序員都在忙,市場部正好又反饋過來一個要命的bug,一時手癢,就領下了這個任務。我們這個產品是針對教育行業的,它是在好幾年前開發,然后不斷完善和維護。這些階段都是在我來到這家公司之前完成的。所以,我對于產品的代碼并不熟悉。

  原來的需求是假定客戶設置分數段時,不同的分數段有不同的有效分,對應著也就有不同的名次。這些數據都是經過分析器分析獲得,并持久化到數據庫中。當我們需要生成學生報告時,再從數據庫中獲取,并將數據填充到iReport設置好的模板中,一個是二維表,一個是柱狀和曲線圖。

  現在,我們發現某些學校需要給不同的分數段設置完全相同的有效分,以及相同的名次。報告打印出來,二維表沒有錯,曲線圖卻出現了“缺斤少兩”的現象。例如設置五個分數段,卻可能只顯示了四條曲線。

  閱讀代碼,我明白了原因。在原來的實現中,由于默認不同分數段有不同名次,因此,在獲取這些分數段的值時,是將它們放入到一個Map<Subject, Map<Integer, Double>>中。這個Map是根據科目進行分類的,子Map的key值為Integer類型,其實就是分數段對應的名次,value則是設置的有效分。由于現在的名次存在重復,導致Map中的元素存在偏差。這就是五個分數段只顯示四條曲線的緣由。

  事實上,在我看到這樣的Map時,就明白這種“超級強大”的容器,事實上往往會成為壞代碼的泥沼。當我們將這樣的對象作為參數,在方法之間傳來傳去的時候,會帶來諸多問題:
  1)性能影響。這種可能比較龐大的容器對象,有可能會形成性能瓶頸;
  2)強類型。雖然這里使用了泛型,但泛型類型卻使用了基本類型;
  3)封裝性不夠好。這樣的容器對象暴露了太多的數據細節,且不利于為其定義職責行為。
  4)可讀性差。看到這樣的Map,你并不會在第一時間明白它到底存放了什么。
  5)可擴展性差:當這個Map作為方法的參數時,相當于這個參數沒有被對象化。如果擁有這個參數的方法被公開,且廣泛調用,一旦需要改變參數,牽連到的代碼就會非常多。

  事實正是如此。當我在分析我們的遺留代碼時,發現很多地方都在重復獲取這個Map對象,并且這個Map對象也在多個方法之間傳遞。因為這種容器對象自身的缺陷,為我的bug修復帶來了很多阻礙。要解決這個Bug,就不能再將名次作為Map的key值。查看相關的數據表,事實上我們還給出了一個分數段的名稱。當名次和有效分存在重復的情況下,結合分數段名稱就能確定唯一值。因此,一個簡單地做法就是將Map的key修改為名次加名稱的組合,即將Integer修改為String。

  現在,我需要決策。如果希望一勞永逸,就應該摒棄這種Map的做法。我們應該利用封裝來實現這一目標。例如定義ValidScore和ValidScoreRange。后者相當于之前的Map<Integer, Double>。ValidScore則包含屬性:Rank, LineName, Score。事實上,ValidScoreRange正是ValidScore的集合。我們還可以在ValidScore和ValidScoreRange中定義許多與該領域相關的行為。通過這樣的封裝,問題會變得簡單許多。

  然而,思考良久,我卻放棄了這個符合OO原則的做法。原因在于,遺留代碼中使用Map<Subject, Map<Integer, Double>>的類和方法有很多,特別讓人煩惱的是在這些方法中,有很多還定義在某些公共類中,被系統的大多數Client所調用。例如這樣的代碼:

 
public static JFreeChart createEliteTotalChart(Grade grade, String partial,
ExamSet es, Student stu, Map subToStuTotalMap,
Map
<Subject, Map<Integer, Double>> subToValidScoreMap,
List subList,
long stuRank,
String[] validLineSeries,
double barWidth,Color barColor) {
if (subToStuTotalMap == null || subList == null) {
return null;
}
Color[] color
= {Color.RED,Color.BLUE,Color.GREEN,
Color.CYAN,Color.BLACK,Color.MAGENTA};
CategoryDataset[] dataset
= EIDatasetFactory.createEliteTotalDataset(
subToStuTotalMap, subToValidScoreMap, subList,validLineSeries);


  //......為清晰起見,其余代碼略
}

  注意,在createEliteTotalChart()方法中,調用了EIDatasetFactory的createEliteTotalDataset(),對Map<Subject, Map<Integer, Double>>對象進行了處理。EIDatasetFactory正是一個公開的公共類,createEliteTotalDataset()方法也是一個被廣泛調用的方法。

  好吧,我們可以利用重構來完成代碼的改造。但是我現在不敢輕易修改這些方法。因為我并不知道這些方法除了生成有效分報告外,還有哪些功能會調用。我們當然可以通過尋找引用來識別到底有哪些功能會調用,事實上,我找到了好幾個引用。可是我依舊需要保持足夠的謹慎。究其原因,是之前的遺留代碼并沒有提供充分的單元測試,無法通過單元測試用例來保證這一修改的正確性。

  好吧。我得承認重構本來就離不開單元測試,除非是那些直接可以利用工具就能完成的重構。問題不是我不能重構,也并非我的能力達不到;問題也不是我無法保證修改的正確性;真正的問題是我沒有時間。作為公司的技術領導,我可以向CEO爭取這個時間嗎?不能!因為CEO必須考慮成本,他要考慮花這么多時間去做看似不能增加功能的重構是否值得。或許你會說,做這樣一個小小的重構需要花太多時間嗎?問題是在這個遺留系統中,這樣的代碼簡直數不勝數。所以,在這里時間才是真正的罪惡。不過,最大的罪惡還是人,是編寫代碼的人。如果在一開始開發系統的時候,我們就能注意代碼質量,隨時重構,并遵循面向對象設計原則,也許境況會變得煥然一新。這就是所謂的“破窗理論”了。從中可見,代碼質量對系統架構的影響。

  面對這樣一座滿是破窗的大樓,我們是下定決心推倒重建,還是修修補補維持現狀?這實在是一個哈姆雷特似的問題。站在技術人員的角度,我傾向于推倒重建;然而站在公司領導的角度,卻不得不思考成本與時間。

  因為這個遺留系統還可以茍延殘喘,因為我的時間不允許停留在這個遺留系統太久,我還有新產品的架構工作要做。最終我選擇了“得過且過”。我針對這一特定需求,找到了調用的方法。為了不影響其他功能,我利用Copy&Paste的方式復制了該方法,只是為它換了一個名字。當然,我還可以利用Extract Method重構來盡量避免代碼的重復。然后,將Map<Subject, Map<Integer, Double>>對象修改為Map<Subject, Map<String, Double>>。因為該方法又連續調用多個其他方法,傳遞了Map<Subject, Map<Integer, Double>>對象,我又如此炮制地復制了這些被調用方法,最終解決了這個Bug。可是看到這么多Copy&Paste的重復代碼,我不寒而栗,決定倉皇逃走。在時間這個強大敵人的面前,我悲哀地選擇了投降。

0
0
 
標簽:重構
 
 

文章列表

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()