Java重構技巧

  • 何謂重構

重構的主要目的在於將現有的程式碼重新整理,以提升程式的可讀性讓Bug更容易被查覺,同時重構之後可讓新功能的追加更加的容易。因為程式的結構與體質變好了,在良好的體質結構上去加重東西也比較不容易導致程式的混亂或崩潰。重構對於開發大型應用程式是相當重要的一環。一般來程式會隨著客戶的規格變更、時空環境的變化而隨時會有新的功能要追加或修改。而不斷的功能追加會導致程式碼越來越大的複雜與肥胖。開發者便需要花費更多的時間與精神去理解這複雜的程式後才能在追加功能上去,然更越加越複雜後最終會達到連開發者自身都難以維護的狀況。因此一個大型的專案必須時常的進行重構整理,不斷的將新追加的功能加以消化與一塊塊的模組,必要時需要修改核心的架構以利其它的功能模組得以附加上來,如此應用程式才會健全的成長。下圖為追加功能與重構的概念關係圖。

追加功能前執行重構,有利於程式的組織架構更完整

  • 如何追加程式功能與改善程式體質

到底何時需要對應用程式去進行重構?要如何利用重構去改善程式的體質?這是一般工程師最容易產生的疑問。由其在專案交期時間很緊迫的時間下,重構有可能導致專案延期,但不重構又有可能會導致事後程式維護上的困難度。因此在適當的時機與條件下進行重構是很重要的經驗判斷,以下我們將介紹幾個重構的限制與使用時機情況。

  • 重構的限制

重構必須在應用程式己經完成到一定程度後才能進行,對於正開發到一半的應用程式,進行重構反而會導致情事更加的混亂。重構必須是要一步步的改善,因此必須當程式完成到一定程度,經過驗證測試對功能上都沒有問題時,此時在對程式的本質進行分析,在不影響功能運作的情況下,尋找更好的架構與撰寫方式。每次的重構,只能針對一小部份來修改,修改後如發現不適合或是有衝突問題,則必須要能還原到重構前的狀態,接著在重新思考新的改進方案。要切記一次只能進行一項重構,這將是最重要的觀念。

  • 重構解決的問題

一般大型應用程式開發的過程中,不管你是老鳥或是菜鳥的工程師,在閱讀程式碼的過程,應該時常腦海裡會浮現以下的問題:

  • 這段程式碼好像剛在那裡看過一模一樣的。
  • 這段函式的程式碼好像太長了。
  • 這個函式與類別的名稱,與它所實際運做的功能好像不相符。
  • 程式看起來不太像是物件導向。
  • 這個類別提供的公開函式過多,我不知要如何選擇使用。
  • 程式到處充滿了警告與提示註解,讓我不敢下手撰寫。
  • 程式之中出現一堆無意義的變數名稱,看似無用,但又怕動了會影響功能運作。
  • 加一個新功能後反而造成連鎖性的問題,必須要到每個類別去一一修正。
  • 這段程式碼看來應該要這樣寫才會更好,心裡很想變更它,但又怕造成其它的問題。
  • 學長老是告誡你,每段程式碼存在都必有其存在的原因,就算看的在不順眼也不敢去變更它,導致專案愈來愈肥大。

上述這些問題都是重構要協助你解決的問題,大型應程式的重構必須要有相當的經驗與細心的工程師才能勝任。比較好的方式是從自從養成良好的習慣,每個人對自己所撰寫的程式碼能多加的思考其優缺點,在開發的過程中就不斷的進行小幅度的最佳化。正所謂積少成多,慢慢的累積與調整的過程中,你將能更加的深入了解物件導向的特性與實際應用的地方,這也就是實戰的經驗,所以動手做就對了。但也別忘了重構的精神:一次只進行一項重構,重構失敗了就必須能還原至原狀(記得備份)。

  • 重構的時機

在進行重構之前你必須要能判斷分析出,那些程式碼需要被改進。簡單的說當你覺得程式碼:難以被理解、難以增加功能、難以修正、難以被維護,程式碼開始造成開發者的不便時,就是你開始動手要去整理它的時後了。下表為Refactoring一書中所列出的22個適合重構的條件。

重複的程式碼過多 程式中有許多重複的程式碼到處分散。
過長的函式內容 單一的函式程式碼過長。
肥胖的類別內容 單一類別擁有過多的函式方法與欄位變數。
過多的函式傳入參數 函式傳入參數過多,導致不容易理解。
分散的程式結構 修改一個功能必須到同時去修改很多檔案。
屬性與操作的相依性過高 每個類別之間的相依性過高,修改其中一個容易造成其它類發生問題。
資料的過度分散 應該要集中處理的資料內容,卻分散在許多不同類別。
過於集中使用基本型態 不加以利用類別,而大量的使用int,char…這些基本資料型態。
過多的switch或if判斷式 過長的switch與if else 判斷式,導造程式效能低落。
並行繼承 建立一個子類別實作後,同時必須在其它package中也建立一個子類別。
無用的類別 使用量很低,或是根本沒有使用到的類別。
過度通用化(抽像化) 過度預期擴充,過度的抽像化造成實作上的困擾。
過多暫時的變數 在類別中,存在過多的暫使性成員變數。
過多函式連鎖呼叫 過多層次的函式呼叫,造成堆疊記憶體的浪費。
本身不太做事的類別 將事情都委託分散給其它類別進行,自身卻沒什麼事做。
不適當的繼承關係 子類別與父類別明明沒太多的關連,卻使用了繼承建立不必要的連結。
類別的介面不一致 類別本身的功能與所繼承實作的介面不搭配
不佳的類別函式庫 使用的類別函式庫(ClassLibrary)難以使用,不符合應用程式的需求。
只包含資料的類別 只包含了欄位與get、set方法的類別。
函式承續性不佳 繼承的函式在覆寫後,卻反而造成呼叫上產生問題。
過多提示性註解 使用了很多提示性註解來避免實作犯錯。
  • 如何養成良好的程式撰寫習慣

每位程式設計師都會有其習慣的程式撰寫風格,若今天應用程式只有你一個人從頭開發到尾,當然只要你自己能看的懂你的應用程式,不管你要使用怎麼樣的寫法都可以。但絕大部份的應用程式都是有許多人一起Teamwork完成的,機乎很少有應用程式,是從頭到尾都由同一組人在維護(員工也是會有離職的時後)。良好的編碼習慣可以提高程式的可讀性,讓其它的程式設計師可以更快的進入狀況,可以著手去改造原先程式中不良的部份。

Java的編譯器只定義了程式碼的語法標準,並未定義程式風格的標準,因此長期以來Java程式撰寫的風格標準是許多程式設計師之間的一種默契,Sun也為此特撰寫了一篇文章[Code Conventions for the Java Programming Language],底下我們將參考此文件,提供編碼上的建議。

  • 檔案名稱與變數名稱
項目 命名習慣 範例
函式參數 使用完整的英文來描述傳遞的變數/物件,第一個字母開頭為小寫,其隨以大寫開頭。 loginName
成員變數 使用完整的英文,第一個單字(word)的第一個字母小寫,其餘的單字的第一個字母大寫。 firstName
Boolean形態的Getter 成員函式 開頭需加上 'is',隨後接上以大寫開頭的成員變數名稱。 isLife()
Getter 成員函式 第一個字母以'get'開頭,隨後以大寫的字母定義要存取的成員變數名稱。 getUserName(),getPassword()
Setter 成員函式 第一個字母以'set'開頭,隨後以大寫的字母定義要存取的成員變數名稱。 setUserName(),setPassword()
成員函式 使用完整的英文,第一個單字(word)的第一個字母小寫,其餘的單字的第一個字母大寫,盡量以以動詞開頭 removeAccount()
檔案檔案 檔案名稱必須與類別名稱相同,副檔名為'.java'。 AccountManagement.java
常數名稱 使用全大寫英文字母,每個單字之間以底線分隔。 MSG_ACK_DATAMSG_REPLY_DATA
介面 跟類別的命名規則相同 Runnable
區域變數 第一個單字為小寫開頭,其餘的單字的第一個字母大寫。盡量避免與全域的成員變數名稱相同,若有相同的名稱可使用this 關鍵字來指定變數所屬的範圍。 userName
迴圈計數器變數 通常以 'i', 'j', 'k' 或 'counter' 來命名。建議多使用for each 用法,可以提升程式效率 i, j, k, cnt, counter
套件名稱 使用完整的英文,通常全都小寫。為了避免和其他套件名稱衝突,需加上公司的網域名稱。 java.util
  • 註解方式

類別的註解方式,宣告在整個類別的最開頭,主要用來撰寫類別的修改歷程與版權宣告。

/*

功能群組分割註解,用來將切分不同群組類形的成員變數或函式。

/*

第二階層的功能群組分割註解,例如在變數宣告區內,可以在依不同的變數形態多加一層註解分類說明。

/*

變數與函式呼叫註解,用來說明每個變數或呼叫的函式所代表功能。

@Override

函式註解,說明傳入參數資料,與回傳的參數形資料。

/**
  • 縮排與折行

建議以四個空白的字元來定義一個縮排單位,並將每一行的長度限定為80個字元,這是為許多的終端機或文件的顯示上若字元超過80個字元,很容易造成長度過長而自動折行這將造成程式的排版錯亂。程式碼撰寫風格可以在Eclipse的[喜好設定Java程式撰寫風格格式制作程式] 中進行設定。

  • 宣告順序
宣告順序 宣告內容
1 類別或介面的註解
2 類別的或介面的宣告
3 類別或介面的實作註解(示所要而定)
4 靜態變數宣告
5 成員變數宣告
6 建構函式宣告
7 覆寫函式宣告(override)
8 內部成員函式宣告
9 公開成員函式宣告
10 抽像函式宣告
11 內部類別宣告

【範例說明】Android Activity設計範本

package com.ittraining;
  • 善用Enum(列舉)

相信有許多人都曾經在switch case的程式碼中,看到許多利用數字代碼來當做條件判斷式的應用。例如下列程式為一個map3播放程式片斷,一般有許多時後在函式的返回值中光用false與true並不足以代碼我們想要表達的執行結果。下列的程式中playMusic 這個函式同時可能會有多種狀況發生,我們稱這些數字為狀態碼。如果你直接將狀態碼以數字形態,直接撰寫進程式中,時間一久你很容易自己都忘記當初這些數字代碼的是什麼函義。

public class Test {
善用列舉
適用的狀況 過多的數字代碼充斥在程式碼中,導致程式難以理解或造成誤解。
解決方法 使用列舉來取代數字代碼。
重構後結果 優點: 
實施方式 1.宣告Enum類別

enum關鍵字是從J2SE5.0開始支援使用,主要用途是用來取代數字型態的常數符號。在沒有enum以前我們對常數定義只能這樣撰寫:

public static final int PLAY = 0;

有了enum之後你可以這樣定義常數:

public enum Music { PLAY, STOP, PAUSE }

當然enum所帶來的改變不止如此,基本上enum所定義就是一個類別,只不過類別的宣告語法全被enum這個關鍵字給簡化了,enum 底下的每個成員皆為繼承至java.lang.Enum類別,而且是被宣告成final static的型態,因此你可以直接透過類別名稱來直接使用它們。以下為一段列舉經過反組譯後的程式碼,我們可以直接透過原始碼更加理解Enum到底為我們做了那些事。

【範例程式】原始enum宣告

public enum Season {Spring, Summer, Autumn, Winter}

【範例程式】實際反組譯後的程式碼

public final class Season extends Enum {

enum的應用相當的廣乏,善加利用enum可以讓你的程式碼更加的乾淨與優雅。enum除了用來定義常數之外,它同時也能使用在switch、尋訪、集合元件(Enumset與EnumMap)。以下範例將各別介紹它的用法。

【範例程式】EnumMap應用

package com.ittraining.refactoring.enumer;

【運行結果】

【範例程式】enum於switch中的應用

package com.ittraining.refactoring.enumer;

【運行結果】

【範例程式】enum多型的應用

package com.ittraining.refactoring.enumer;

【運行結果】

  • 函式的擷取

函式的擷取在Refactoring中原文為Extract Method。當一個函式若存在過長的程式碼,而且程式碼出現許多重複的部份。函式的擷取便是將重覆的程式碼另外切出去成為一個新的函式,程式中重複的程式碼便可以改呼叫此新的函式。

函式的擷取
適用的狀況 函式的程式碼過長。
解決方法 取出可以群組化的程式片斷,另外建立新的函式。
重構後結果 優點: 
實施方式 1.建立新的函式

下圖為實施函式的擷取對應用程式所帶來的改變

【範例程式】

函式擷取前 函式擷取後
public class Banner { public class Banner {

【運行結果】

  • 延伸練習

以下為一個ATM 提款機的範例,該範會提示用戶輸入4位數的密碼,密碼輸入正確後接著提示用戶輸入要提取的現金金額。 試著將以下的範例利用方法的擷取進行重構。

【範例程式】 未經重構的ATM提款程式

package com.ittraining.refactoring.extractmethod.test;

【運行結果】

  • 類別的擷取

類別的擷取在Refactoring一書中原文稱為Extract Class。類別本身為一群功能函式的集合,類別具有完成該功能的義務。當一個類別所負責的功能過多、過雜時,會導致過度集中化的應用程式,大部功的核心功能都集中在少部份的幾個類別完成。這將會非常不利應用程式事後的擴充與修改。一般較佳的情況是一個類別只負責處理一個任務,而該類別中所提供的函式必須是與此任務相關聯的。這將可以減少類別與類別之間過度相依性,有助於應用程式的模組化與功能重整。

類別的擷取 
適用的狀況 單一類別負擔了過多功能實作
解決方法 將類別中的功能函式加以群組分類,將相同類型的函式分割出來,成為一個新的類別。
重構後結果 優點:
實施方式 1.建立新的類別

下圖為實施類別的擷取對應用程式所帶來的改變

【範例程式】

類別擷取前 類別擷取後
public class Book { public class Book {

【運行結果】

  • 延伸練習
  • 對每本書追加可有多個作者功能.
  • 對每個作者追加個人blog與討論區網址

  • Android 產訓專班 – Java課程

作者: Jarey
Email [email protected]
課程: 第三堂 Java API函式庫
內容: 1.Java集合物件(ArrayList、HashMap、LinkList…)2.Java IO API3.多執行運作概念與實作方式4.Java Multithread Design Pattrern (ThreadPool、Blocking Queue...)5.Java例外處理6.Java網路API