高質量程式碼的三要素(一)

醉面韋陀發表於2010-01-08

我們評價高質量程式碼有三要素:可讀性、可維護性、可變更性。我們的程式碼要一個都不能少地達到了這三要素的要求才能算高質量的程式碼。

1.可讀性強

一提到可讀性似乎有一些老生常談的味道,但令人沮喪的是,雖然大家一而再,再而三地強調可讀性,但我們的程式碼在可讀性方面依然做得非常糟糕。由於工作的需要,我常常需要去閱讀他人的程式碼,維護他人設計的模組。每當我看到大段大段、密密麻麻的程式碼,而且還沒有任何的註釋時常常感慨不已,深深體會到了這項工作的重要。由於分工的需要,我們寫的程式碼難免需要別人去閱讀和維護的。而對於許多程式設計師來說,他們很少去閱讀和維護別人的程式碼。正因為如此,他們很少關注程式碼的可讀性,也對如何提高程式碼的可讀性缺乏切身體會。有時即使為程式碼編寫了註釋,也常常是註釋語言晦澀難懂形同天書,令閱讀者反覆斟酌依然不明其意。針對以上問題,我給大家以下建議:

1)不要編寫大段的程式碼

如果你有閱讀他人程式碼的經驗,當你看到別人寫的大段大段的程式碼,而且還不怎麼帶註釋,你是怎樣的感覺,是不是“嗡”地一聲頭大。各種各樣的功能糾纏在一個方法中,各種變數來回撥用,相信任何人多不會認為它是高質量的程式碼,但卻頻繁地出現在我們編寫的程式了。如果現在你再回顧自己寫過的程式碼,你會發現,稍微編寫一個複雜的功能,幾百行的程式碼就出去了。一些比較好的辦法就是分段。將大段的程式碼經過整理,分為功能相對獨立的一段又一段,並且在每段的前端編寫一段註釋。這樣的編寫,比前面那些雜亂無章的大段程式碼確實進步了不少,但它們在功能獨立性、可複用性、可維護性方面依然不盡人意。從另一個比較專業的評價標準來說,它沒有實現低耦合、高內聚。我給大家的建議是,將這些相對獨立的段落另外封裝成一個又一個的函式。

許多大師在自己的經典書籍中,都鼓勵我們在編寫程式碼的過程中應當養成不斷重構的習慣。我們在編寫程式碼的過程中常常要編寫一些複雜的功能,起初是寫在一個類的一個函式中。隨著功能的逐漸展開,我們開始對複雜功能進行歸納整理,整理出了一個又一個的獨立功能。這些獨立功能有它與其它功能相互交流的輸入輸出資料。當我們分析到此處時,我們會非常自然地要將這些功能從原函式中分離出來,形成一個又一個獨立的函式,供原函式呼叫。在編寫這些函式時,我們應當仔細思考一下,為它們取一個釋義名稱,併為它們編寫註釋(後面還將詳細討論這個問題)。另一個需要思考的問題是,這些函式應當放到什麼地方。這些函式可能放在原類中,也可能放到其它相應職責的類中,其遵循的原則應當是“職責驅動設計”(後面也將詳細描述)。

下面是我編寫的一個從XML檔案中讀取資料,將其生成工廠的一個類。這個類最主要的一段程式就是初始化工廠,該功能歸納起來就是三部分功能:用各種方式嘗試讀取檔案、以DOM的方式解析XML資料流、生成工廠。而這些功能被我歸納整理後封裝在一個不同的函式中,並且為其取了釋義名稱和編寫了註釋:

 

 

 

 

Java程式碼 複製程式碼
  1. /**  
  2.  * 初始化工廠。根據路徑讀取XML檔案,將XML檔案中的資料裝載到工廠中  
  3.  * @param path XML的路徑  
  4.  */  
  5. public void initFactory(String path){   
  6.     if(findOnlyOneFileByClassPath(path)){return;}   
  7.     if(findResourcesByUrl(path)){return;}   
  8.     if(findResourcesByFile(path)){return;}   
  9.     this.paths = new String[]{path};   
  10. }   
  11.   
  12. /**  
  13.  * 初始化工廠。根據路徑列表依次讀取XML檔案,將XML檔案中的資料裝載到工廠中  
  14.  * @param paths 路徑列表  
  15.  */  
  16. public void initFactory(String[] paths){   
  17.     for(int i=0; i<paths.length; i++){   
  18.         initFactory(paths[i]);   
  19.     }   
  20.     this.paths = paths;   
  21. }   
  22.   
  23. /**  
  24.  * 重新初始化工廠,初始化所需的引數,為上一次初始化工廠所用的引數。  
  25.  */  
  26. public void reloadFactory(){   
  27.     initFactory(this.paths);   
  28. }   
  29.   
  30. /**  
  31.  * 採用ClassLoader的方式試圖查詢一個檔案,並呼叫<code>readXmlStream()</code>進行解析  
  32.  * @param path XML檔案的路徑  
  33.  * @return 是否成功  
  34.  */  
  35. protected boolean findOnlyOneFileByClassPath(String path){   
  36.     boolean success = false;   
  37.     try {   
  38.         Resource resource = new ClassPathResource(path, this.getClass());   
  39.         resource.setFilter(this.getFilter());   
  40.         InputStream is = resource.getInputStream();   
  41.         if(is==null){return false;}   
  42.         readXmlStream(is);   
  43.         success = true;   
  44.     } catch (SAXException e) {   
  45.         log.debug("Error when findOnlyOneFileByClassPath:"+path,e);   
  46.     } catch (IOException e) {   
  47.         log.debug("Error when findOnlyOneFileByClassPath:"+path,e);   
  48.     } catch (ParserConfigurationException e) {   
  49.         log.debug("Error when findOnlyOneFileByClassPath:"+path,e);   
  50.     }   
  51.     return success;   
  52. }   
  53.   
  54. /**  
  55.  * 採用URL的方式試圖查詢一個目錄中的所有XML檔案,並呼叫<code>readXmlStream()</code>進行解析  
  56.  * @param path XML檔案的路徑  
  57.  * @return 是否成功  
  58.  */  
  59. protected boolean findResourcesByUrl(String path){   
  60.     boolean success = false;   
  61.     try {   
  62.         ResourcePath resourcePath = new PathMatchResource(path, this.getClass());   
  63.         resourcePath.setFilter(this.getFilter());   
  64.         Resource[] loaders = resourcePath.getResources();   
  65.         for(int i=0; i<loaders.length; i++){   
  66.             InputStream is = loaders[i].getInputStream();   
  67.             if(is!=null){   
  68.                 readXmlStream(is);   
  69.                 success = true;   
  70.             }   
  71.         }   
  72.     } catch (SAXException e) {   
  73.         log.debug("Error when findResourcesByUrl:"+path,e);   
  74.     } catch (IOException e) {   
  75.         log.debug("Error when findResourcesByUrl:"+path,e);   
  76.     } catch (ParserConfigurationException e) {   
  77.         log.debug("Error when findResourcesByUrl:"+path,e);   
  78.     }   
  79.     return success;   
  80. }   
  81.   
  82. /**  
  83.  * 用File的方式試圖查詢檔案,並呼叫<code>readXmlStream()</code>解析  
  84.  * @param path XML檔案的路徑  
  85.  * @return 是否成功  
  86.  */  
  87. protected boolean findResourcesByFile(String path){   
  88.     boolean success = false;   
  89.     FileResource loader = new FileResource(new File(path));   
  90.     loader.setFilter(this.getFilter());   
  91.     try {   
  92.         Resource[] loaders = loader.getResources();   
  93.         if(loaders==null){return false;}   
  94.         for(int i=0; i<loaders.length; i++){   
  95.             InputStream is = loaders[i].getInputStream();   
  96.             if(is!=null){   
  97.                 readXmlStream(is);   
  98.                 success = true;   
  99.             }   
  100.         }   
  101.     } catch (IOException e) {   
  102.         log.debug("Error when findResourcesByFile:"+path,e);   
  103.     } catch (SAXException e) {   
  104.         log.debug("Error when findResourcesByFile:"+path,e);   
  105.     } catch (ParserConfigurationException e) {   
  106.         log.debug("Error when findResourcesByFile:"+path,e);   
  107.     }   
  108.     return success;   
  109. }   
  110.   
  111. /**  
  112.  * 讀取並解析一個XML的檔案輸入流,以Element的形式獲取XML的根,  
  113.  * 然後呼叫<code>buildFactory(Element)</code>構建工廠  
  114.  * @param inputStream 檔案輸入流  
  115.  * @throws SAXException  
  116.  * @throws IOException  
  117.  * @throws ParserConfigurationException  
  118.  */  
  119. protected void readXmlStream(InputStream inputStream) throws SAXException, IOException, ParserConfigurationException{   
  120.     if(inputStream==null){   
  121.         throw new ParserConfigurationException("Cann't parse source because of InputStream is null!");   
  122.     }   
  123.     DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();   
  124.        factory.setValidating(this.isValidating());   
  125.        factory.setNamespaceAware(this.isNamespaceAware());   
  126.        DocumentBuilder build = factory.newDocumentBuilder();   
  127.        Document doc = build.parse(new InputSource(inputStream));   
  128.        Element root = doc.getDocumentElement();   
  129.        buildFactory(root);   
  130. }   
  131.   
  132. /**  
  133.  * 用從一個XML的檔案中讀取的資料構建工廠  
  134.  * @param root 從一個XML的檔案中讀取的資料的根  
  135.  */  
  136. protected abstract void buildFactory(Element root);   
	/**
	 * 初始化工廠。根據路徑讀取XML檔案,將XML檔案中的資料裝載到工廠中
	 * @param path XML的路徑
	 */
	public void initFactory(String path){
		if(findOnlyOneFileByClassPath(path)){return;}
		if(findResourcesByUrl(path)){return;}
		if(findResourcesByFile(path)){return;}
		this.paths = new String[]{path};
	}
	
	/**
	 * 初始化工廠。根據路徑列表依次讀取XML檔案,將XML檔案中的資料裝載到工廠中
	 * @param paths 路徑列表
	 */
	public void initFactory(String[] paths){
		for(int i=0; i<paths.length; i++){
			initFactory(paths[i]);
		}
		this.paths = paths;
	}
	
	/**
	 * 重新初始化工廠,初始化所需的引數,為上一次初始化工廠所用的引數。
	 */
	public void reloadFactory(){
		initFactory(this.paths);
	}
	
	/**
	 * 採用ClassLoader的方式試圖查詢一個檔案,並呼叫<code>readXmlStream()</code>進行解析
	 * @param path XML檔案的路徑
	 * @return 是否成功
	 */
	protected boolean findOnlyOneFileByClassPath(String path){
		boolean success = false;
		try {
			Resource resource = new ClassPathResource(path, this.getClass());
			resource.setFilter(this.getFilter());
			InputStream is = resource.getInputStream();
			if(is==null){return false;}
			readXmlStream(is);
			success = true;
		} catch (SAXException e) {
			log.debug("Error when findOnlyOneFileByClassPath:"+path,e);
		} catch (IOException e) {
			log.debug("Error when findOnlyOneFileByClassPath:"+path,e);
		} catch (ParserConfigurationException e) {
			log.debug("Error when findOnlyOneFileByClassPath:"+path,e);
		}
		return success;
	}
	
	/**
	 * 採用URL的方式試圖查詢一個目錄中的所有XML檔案,並呼叫<code>readXmlStream()</code>進行解析
	 * @param path XML檔案的路徑
	 * @return 是否成功
	 */
	protected boolean findResourcesByUrl(String path){
		boolean success = false;
		try {
			ResourcePath resourcePath = new PathMatchResource(path, this.getClass());
			resourcePath.setFilter(this.getFilter());
			Resource[] loaders = resourcePath.getResources();
			for(int i=0; i<loaders.length; i++){
				InputStream is = loaders[i].getInputStream();
				if(is!=null){
					readXmlStream(is);
					success = true;
				}
			}
		} catch (SAXException e) {
			log.debug("Error when findResourcesByUrl:"+path,e);
		} catch (IOException e) {
			log.debug("Error when findResourcesByUrl:"+path,e);
		} catch (ParserConfigurationException e) {
			log.debug("Error when findResourcesByUrl:"+path,e);
		}
		return success;
	}
	
	/**
	 * 用File的方式試圖查詢檔案,並呼叫<code>readXmlStream()</code>解析
	 * @param path XML檔案的路徑
	 * @return 是否成功
	 */
	protected boolean findResourcesByFile(String path){
		boolean success = false;
		FileResource loader = new FileResource(new File(path));
		loader.setFilter(this.getFilter());
		try {
			Resource[] loaders = loader.getResources();
			if(loaders==null){return false;}
			for(int i=0; i<loaders.length; i++){
				InputStream is = loaders[i].getInputStream();
				if(is!=null){
					readXmlStream(is);
					success = true;
				}
			}
		} catch (IOException e) {
			log.debug("Error when findResourcesByFile:"+path,e);
		} catch (SAXException e) {
			log.debug("Error when findResourcesByFile:"+path,e);
		} catch (ParserConfigurationException e) {
			log.debug("Error when findResourcesByFile:"+path,e);
		}
		return success;
	}

	/**
	 * 讀取並解析一個XML的檔案輸入流,以Element的形式獲取XML的根,
	 * 然後呼叫<code>buildFactory(Element)</code>構建工廠
	 * @param inputStream 檔案輸入流
	 * @throws SAXException
	 * @throws IOException
	 * @throws ParserConfigurationException
	 */
	protected void readXmlStream(InputStream inputStream) throws SAXException, IOException, ParserConfigurationException{
		if(inputStream==null){
			throw new ParserConfigurationException("Cann't parse source because of InputStream is null!");
		}
		DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        factory.setValidating(this.isValidating());
        factory.setNamespaceAware(this.isNamespaceAware());
        DocumentBuilder build = factory.newDocumentBuilder();
        Document doc = build.parse(new InputSource(inputStream));
        Element root = doc.getDocumentElement();
        buildFactory(root);
	}
	
	/**
	 * 用從一個XML的檔案中讀取的資料構建工廠
	 * @param root 從一個XML的檔案中讀取的資料的根
	 */
	protected abstract void buildFactory(Element root);
	

 

 

 

 

完整程式碼在附件中。在編寫程式碼的過程中,通常有兩種不同的方式。一種是從下往上編寫,也就是按照順序,每分出去一個函式,都要將這個函式編寫完,才回到主程式,繼續往下編寫。而一些更有經驗的程式設計師會採用另外一種從上往下的編寫方式。當他們在編寫程式的時候,每個被分出去的程式,可以暫時只寫一個空程式而不去具體實現功能。當主程式完成以後,再一個個實現它的所有子程式。採用這樣的編寫方式,可以使複雜程式有更好的規劃,避免只見樹木不見森林的弊病。

有多少程式碼就算大段程式碼,每個人有自己的理解。我編寫程式碼,每當達到15~20行的時候,我就開始考慮是否需要重構程式碼。同理,一個類也不應當有太多的函式,當函式達到一定程度的時候就應該考慮分為多個類了;一個包也不應當有太多的類。。。。。。

2)釋義名稱與註釋

我們在命名變數、函式、屬性、類以及包的時候,應當仔細想想,使名稱更加符合相應的功能。我們常常在說,設計一個系統時應當有一個或多個系統分析師對整個系統的包、類以及相關的函式和屬性進行規劃,但在通常的專案中這都非常難於做到。對它們的命名更多的還是程式設計師來完成。但是,在一個專案開始的時候,應當對專案的命名出臺一個規範。譬如,在我的專案中規定,新增記錄用newadd開頭,更新記錄用editmod開頭,刪除用del開頭,查詢用findquery開頭。使用最亂的就是get,因此我規定,get開頭的函式僅僅用於獲取類屬性。

註釋是每個專案組都在不斷強調的,可是依然有許多的程式碼沒有任何的註釋。為什麼呢?因為每個專案在開發過程中往往時間都是非常緊的。在緊張的程式碼開發過程中,註釋往往就漸漸地被忽略了。利用開發工具的程式碼編寫模板也許可以解決這個問題。

用我們常用的MyEclipse為例,在選單“window>>Preferences>>Java>>Code Style>>Code Templates>>Comments”中,可以簡單的修改一下。

 



 

 

 

Files”代表的是我們每新建一個檔案(可能是類也可能是介面)時編寫的註釋,我通常設定為:

Java程式碼 複製程式碼
  1. /*  
  2.  * created on ${date}  
  3. */  
/*
 * created on ${date}
*/

  

 

Types”代表的是我們新建的介面或類前的註釋,我通常設定為:

Java程式碼 複製程式碼
  1. /**  
  2.  *   
  3.  * @author ${user}  
  4.  */  
/**
 * 
 * @author ${user}
 */

  

 

第一行為一個空行,是用於你寫該類的註釋。如果你採用“職責驅動設計”,這裡首先應當描述的是該類的職責。如果需要,你可以寫該類一些重要的方法及其用法、該類的屬性及其中文含義等。

${user}代表的是你在windows中登陸的使用者名稱。如果這個使用者名稱不是你的名稱,你可以直接寫死為你自己的名稱。

其它我通常都保持為預設值。通過以上設定,你在建立類或介面的時候,系統將自動為你編寫好註釋,然後你可以在這個基礎上進行修改,大大提高註釋編寫的效率。

同時,如果你在程式碼中新增了一個函式時,通過Alt+Shift+J快捷鍵,可以按照模板快速新增註釋。

在編寫程式碼時如果你編寫的是一個介面或抽象類,我還建議你在@author後面增加@see註釋,將該介面或抽象類的所有實現類列出來,因為閱讀者在閱讀的時候,尋找介面或抽象類的實現類比較困難。

 

 

  

Java程式碼 複製程式碼
  1. /**  
  2.  * 抽象的單表陣列查詢實現類,僅用於單表查詢  
  3.  * @author 範鋼  
  4.  * @see com.htxx.support.query.DefaultArrayQuery  
  5.  * @see com.htxx.support.query.DwrQuery  
  6.  */  
  7. public abstract class ArrayQuery implements ISingleQuery {   
  8. ...  
/**
 * 抽象的單表陣列查詢實現類,僅用於單表查詢
 * @author 範鋼
 * @see com.htxx.support.query.DefaultArrayQuery
 * @see com.htxx.support.query.DwrQuery
 */
public abstract class ArrayQuery implements ISingleQuery {
...

 

2.可維護性

軟體的可維護性有幾層意思,首先的意思就是能夠適應軟體在部署和使用中的各種情況。從這個角度上來說,它對我們的軟體提出的要求就是不能將程式碼寫死。

1)程式碼不能寫死

我曾經見我的同事將系統要讀取的一個日誌檔案指定在C盤的一個固定目錄下,如果系統部署時沒有這個目錄以及這個檔案就會出錯。如果他將這個決定路徑下的目錄改為相對路徑,或者通過一個屬性檔案可以修改,程式碼豈不就寫活了。一般來說,我在設計中需要使用日誌檔案、屬性檔案、配置檔案,通常都是以下幾個方式:將檔案放到與類相同的目錄,使用ClassLoader.getResource()來讀取;將檔案放到classpath目錄下,用File的相對路徑來讀取;使用web.xml或另一個屬性檔案來制定讀取路徑。

我也曾見另一家公司的軟體要求,在部署的時候必須在C:/bea目錄下,如果換成其它目錄則不能正常執行。這樣的設定常常為軟體部署時帶來許多的麻煩。如果伺服器在該目錄下已經沒有多餘空間,或者已經有其它軟體,將是很撓頭的事情。

2)預測可能發生的變化

除此之外,在設計的時候,如果將一些關鍵引數放到配置檔案中,可以為軟體部署和使用帶來更多的靈活性。要做到這一點,要求我們在軟體設計時,應當更多地有更多的意識,考慮到軟體應用中可能發生的變化。比如,有一次我在設計財務軟體的時候,考慮到一些單據在製作時的前置條件,在不同企業使用的時候,可能要求不一樣,有些企業可能要求嚴格些而有些要求鬆散些。考慮到這種可能的變化,我將前置條件設計為可配置的,就可能方便部署人員在實際部署中進行靈活變化。然而這樣的配置,必要的註釋說明是非常必要的。

軟體的可維護性的另一層意思就是軟體的設計便於日後的變更。這一層意思與軟體的可變更性是重合的。所有的軟體設計理論的發展,都是從軟體的可變更性這一要求逐漸展開的,它成為了軟體設計理論的核心。

 

相關文章