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

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

3.可變更性

前面我提到了,軟體的變更性是所有軟體理論的核心,那麼什麼是軟體的可變更性呢?按照現在的軟體理論,客戶對軟體的需求時時刻刻在發生著變化。當軟體設計好以後,為應對客戶需求的變更而進行的程式碼修改,其所需要付出的代價,就是軟體設計的可變更性。由於軟體合理地設計,修改所付出的代價越小,則軟體的可變更性越好,即程式碼設計的質量越高。一種非常理想的狀態是,無論客戶需求怎樣變化,軟體只需進行適當地修改就能夠適應。但這之所以稱之為理想狀態,因為客戶需求變化是有大有小的。如果客戶需求變化非常大,即使再好的設計也無法應付,甚至重新開發。然而,客戶需求的適當變化,一個合理地設計可以使得變更代價最小化,延續我們設計的軟體的生命力。

1)通過提高程式碼複用提高可維護性

我曾經遇到過這樣一件事,我要維護的一個系統因為應用範圍的擴大,它對機關級次的計算方式需要改變一種策略。如果這個專案統一採用一段公用方法來計算機關級次,這樣一個修改實在太簡單了,就是修改這個公用方法即可。但是,事實卻不一樣,對機關級次計算的程式碼遍佈整個專案,甚至有些還寫入到了那些複雜的SQL語句中。在這樣一種情況下,這樣一個需求的修改無異於需要遍歷這個專案程式碼。這樣一個例項顯示了一個專案程式碼複用的重要,然而不幸的是,程式碼無法很好複用的情況遍佈我們所有的專案。程式碼複用的道理十分簡單,但要具體運作起來非常複雜,它除了需要很好的程式碼規劃,還需要持續地程式碼重構。

對整個系統的整體分析與合理規劃可以根本地保證程式碼複用。系統分析師通過用例模型、領域模型、分析模型的一步一步分析,最後通過正向工程,生成系統需要設計的各種類及其各自的屬性和方法。採用這種方法,功能被合理地劃分到這個類中,可以很好地保證程式碼複用。

採用以上方法雖然好,但技術難度較高,需要有高深的系統分析師,並不是所有專案都能普遍採用的,特別是時間比較緊張的專案。通過開發人員在設計過程中的重構,也許更加實用。當某個開發人員在開發一段程式碼時,發現該功能與前面已經開發功能相同,或者部分相同。這時,這個開發人員可以對前面已經開發的功能進行重構,將可以通用的程式碼提取出來,進行相應地改造,使其具有一定的通用性,便於各個地方可以使用。

一些比較成功的專案組會指定一個專門管理通用程式碼的人,負責收集和整理專案組中各個成員編寫的,可以通用的程式碼。這個負責人同時也應當具有一定的程式碼編寫功力,因為將專用程式碼提升為通用程式碼,或者以前使用該通用程式碼的某個功能,由於業務變更,而對這個通用程式碼的變更要求,都對這個負責人提出了很高的能力要求。

雖然後一種方式非常實用,但是它有些亡羊補牢的味道,不能從整體上對專案程式碼進行有效規劃。正因為兩種方法各有利弊,因此在專案中應當配合使用。

2)利用設計模式提高可變更性

對於初學者,軟體設計理論常常感覺晦澀難懂。一個快速提高軟體質量的捷徑就是利用設計模式。這裡說的設計模式,不僅僅指經典的32個模式,是一切前人總結的,我們可以利用的、更加廣泛的設計模式。

 

a. if...else...

這個我也不知道叫什麼名字,最早是哪位大師總結的,它出現在Larman的《UML與模式應用》,也出現在出現在Mardin的《敏捷軟體開發》。它是這樣描述的:當你發現你必須要設計這樣的程式碼:“if...elseif...elseif...else...”時,你應當想到你的程式碼應當重構一下了。我們先看看這樣的程式碼有怎樣的特點。

 

 

Java程式碼 複製程式碼
  1.            if(var.equals("A")){   
  2.     doA();   
  3. }else if(var.equals("B")){   
  4.     doB();   
  5. }else if(var.equals("C")){   
  6.     doC();   
  7. }else{   
  8.     doD();   
  9. }  
                if(var.equals("A")){
    		doA();
    	}else if(var.equals("B")){
    		doB();
    	}else if(var.equals("C")){
    		doC();
    	}else{
    		doD();
    	}

  

 

 

 

這樣的程式碼很常見,也非常平常,我們大家都寫過。但正是這樣平常才隱藏著我們永遠沒有注意的問題。問題就在於,如果某一天這個選項不再僅僅是ABC,而是增加了新的選項,會怎樣呢?你也許會說,那沒有關係,我把程式碼改改就行。然而事實上並非如此,在大型軟體研發與維護中有一個原則,每次的變更儘量不要去修改原有的程式碼。如果我們重構一下,能保證不修改原有程式碼,僅僅增加新的程式碼就能應付選項的增加,這就增加了這段程式碼的可維護性和可變更性,提高了程式碼質量。那麼,我們應當如何去做呢?

 

經過深入分析你會發現,這裡存在一個對應關係,即A對應doA()B對應doB()...如果將doA()doB()doC()...與原有程式碼解耦,問題就解決了。如何解耦呢?設計一個介面X以及它的實現ABC...每個類都包含一個方法doX(),並且將doA()的程式碼放到A.doX()中,將doB()的程式碼放到B.doX()中...經過以上的重構,程式碼還是這些程式碼,效果卻完全不一樣了。我們只需要這樣寫:

 

 

 

 

 

這樣就可以實現以上的功能了。我們看到這裡有一個工廠,放著所有的ABC...並且與它們的key對應起來,並且寫在配置檔案中。如果出現新的選項時,通過修改配置檔案就可以無限制的增加下去。

這個模式雖然有效提高了程式碼質量,但是不能濫用,並非只要出現if...else...就需要使用。由於它使用了工廠,一定程度上增加了程式碼複雜度,因此僅僅在選項較多,並且增加選項的可能性很大的情況下才可以使用。另外,要使用這個模式,繼承我在附件中提供的抽象類XmlBuildFactoryFacade就可以快速建立一個工廠。如果你的專案放在spring或其它可配置框架中,也可以快速建立工廠。設計一個Map靜態屬性並使其V為這些ABC...這個工廠就建立起來了。

 

b.策略模式

也許你看過策略模式(strategy model)的相關資料但沒有留下太多的印象。一個簡單的例子可以讓你快速理解它。如果一個員工系統中,員工被分為臨時工和正式工並且在不同的地方相應的行為不一樣。在設計它們的時候,你肯定設計一個抽象的員工類,並且設計兩個繼承類:臨時工和正式工。這樣,通過下塑型別,可以在不同的地方表現出臨時工和正式工的各自行為。在另一個系統中,員工被分為了銷售人員、技術人員、管理人員並且也在不同的地方相應的行為不一樣。同樣,我們在設計時也是設計一個抽象的員工類,並且設計數個繼承類:銷售人員、技術人員、管理人員。現在,我們要把這兩個系統合併起來,也就是說,在新的系統中,員工既被分為臨時工和正式工,又被分為了銷售人員、技術人員、管理人員,這時候如何設計。如果我們還是使用以往的設計,我們將不得不設計很多繼承類:銷售臨時工、銷售正式工、技術臨時工、技術正式工...如此的設計,在隨著劃分的型別,以及每種型別的選項的增多,呈笛卡爾增長。通過以上一個系統的設計,我們不得不發現,我們以往學習的關於繼承的設計遇到了挑戰。

解決繼承出現的問題,有一個最好的辦法,就是採用策略模式。在這個應用中,員工之所以要分為臨時工和正式工,無非是因為它們的一些行為不一樣,比如,發工資時的計算方式不同。如果我們在設計時不將員工類分為臨時工類和正式工類,而僅僅只有員工類,只是在類中增加“工資發放策略”。當我們建立員工物件時,根據員工的型別,將“工資發放策略”設定為“臨時工策略”或“正式工策略”,在計算工資時,只需要呼叫策略類中的“計算工資”方法,其行為的表現,也設計臨時工類和正式工類是一樣的。同樣的設計可以放到銷售人員策略、技術人員策略、管理人員策略中。一個通常的設計是,我們將某一個影響更大的、或者選項更少的屬性設計成繼承類,而將其它屬性設計成策略類,就可以很好的解決以上問題。

 



 

 

 

使用策略模式,你同樣把程式碼寫活了,因為你可以無限制地增加策略。但是,使用策略模式你同樣需要設計一個工廠——策略工廠。以上例項中,你需要設計一個發放工資策略工廠,並且在工廠中將“臨時工”與“臨時工策略”對應起來,將“正式工”與“正式工策略”對應起來。

 

c.介面卡模式

我的筆記本是港貨,它的插頭與我們常用的插座不一樣,所有我出差的時候我必須帶一個介面卡,才能使用不同地方的插座。這是一個對介面卡模式最經典的描述。當我們設計的系統要與其它系統互動,或者我們設計的模組要與其它模組互動時,這種互動可能是呼叫一個介面,或者交換一段資料,接受方常常因傳送方對協議的變更而頻繁變更。這種變更,可能是接受方來源的變更,比如原來是A系統,現在變成B系統了;也可能是接受方自身的程式碼變更,如原來的介面現在增加了一個引數。由於傳送方的變更常常導致接受方程式碼的不穩定,即頻繁跟著修改,為接受方的維護帶來困難。

遇到這樣的問題,一個有經驗的程式設計師馬上想到的就是採用介面卡模式。在設計時,我方的介面按照某個協議編寫,並且保持固定不變。然後,在與真正對方介面時,在前段設計一個介面卡類,一旦對方協議發生變更,我可以換個介面卡,將新協議轉換成原協議,問題就解決了。介面卡模式應當包含一個介面和它的實現類。介面應當包含一個本系統要呼叫的方法,而它的實現類分別是與A系統介面的介面卡、與B系統介面的介面卡...

 



 

 

我曾經在一個專案中需要與另一個系統介面,起初那個系統通過一個資料集的方式為我提供資料,我寫了一個接收資料集的介面卡;後來改為用一個XML資料流的形式,我又寫了一個接收XML的介面卡。雖然為我提供資料的方式不同,但是經過介面卡轉換後,輸出的資料是一樣的。通過在spring中的配置,我可以靈活地切換到底是使用哪個介面卡。

 

d.外觀模式

32個經典模式中的外觀模式,對開發者的程式碼規劃能力提出了更高的要求,它要求開發者對自己開發的所有程式碼有一個相互聯絡和從中抽象的能力,從各個不同的模組和各個不同的功能中,抽象出其過程比較一致的通用流程,最終形成外觀。譬如說,讀取XML並形成工廠,是許多模組常常要使用的功能。它們雖然有各自的不同,但是總體流程都是一樣的:讀取XML檔案、解析XML資料流、形成工廠。正因為有這樣的特徵,它們可以使用共同的外觀,那麼,什麼是外觀模式呢?

外觀模式(Façade Model)通常有一個抽象類。在這個抽象類中,通常有一個主函式,按照一定地順序去呼叫其它函式。而其它函式往往是某這個連續過程中的各個步驟,如以上例項中的讀取XML檔案、解析XML資料流、形成工廠等步驟。由於這是一個抽象類,這些步驟函式可以是抽象函式。抽象類僅僅定義了整個過程的執行順序,以及一些可以通用的步驟(如讀取XML檔案和解析XML資料流),而另一些比較個性的步驟,則由它的繼承類自己去完成(如上例中的“形成工廠”,由於各個工廠各不一樣,因此由各自的繼承類自己去決定它的工廠是怎樣形成的)。

 



 

 

各個繼承類可以根據自己的需要,通過過載重新定義各個步驟函式。但是,外觀模式要求不能過載主函式,因此正規的外觀模式其主函式應當是final(雖然我們常常不這麼寫)。另外,外觀模式還允許你定義的這個步驟中,有些步驟是可選步驟。對與可選步驟,我們通常稱為“鉤子(hood)”。它在編寫時,在抽象類中並不是一個抽象函式,但卻是一個什麼都不寫的空函式。繼承類在編寫時,如果需要這個步驟則過載這個函式,否則就什麼也不寫,進而在執行的時候也如同什麼都沒有執行。

通過以上對外觀模式的描述可以發現,外觀模式可以大大地提高我們的程式碼複用程度。

以上一些常用設計模式,都能使我們快速提高程式碼質量。還是那句話,設計模式不是什麼高深的東西,恰恰相反,它是初學者快速提高的捷徑。然而,如果說提高程式碼複用是提高程式碼質量的初階,使用設計模式也只能是提高程式碼質量的中階。那麼,什麼是高階呢?我認為是那些分析設計理論,更具體地說,就是職責驅動設計和領域驅動設計。

Java程式碼 複製程式碼
  1. X x = factory.getBean(var);   
  2. x.doX();  
X x = factory.getBean(var);
x.doX();

 

相關文章