竊以為軟體的最大追求是在合適的地方做正確的事

ppshen發表於2006-01-10
前段時間讀了《軟體的最大追求是什麼》,擊節叫好,深以為然,雖然該文章很多地方顯得有點極端。

如今的軟體系統越來越複雜,如果軟體的結構不好會影響軟體的可維護性,重構程式碼是一件極其痛苦的事情。

關於軟體的複雜性問題,我做了一些思考:

1) Cyclomatic Complexity (圈複雜性 = number of decision points +1    其中number of decision points是指一個if else之類的條件判斷語句 (引自《軟體的最大追求是什麼》)

if else 語句可以被行為模式/策略模式代替,不妨看下列的例子:

假設我們要根據條件判斷來完成不同的行為,用if else 是這樣寫的:

main() {

if(case A){

//do with strategy A

}else(case B){

//do with strategy B

}else(case C){

//do with strategy C

}

}



用策略模式則如下:

class runner{

do();

}



class A extends runner{

do(){

//do with strategy A

}

}



class B extends runner{

do(){

//do with strategy B

}

}



class C extends runner {

do(){

//do with strategy C

}

}



main(){

runner.do();

}



用了策略模式後main()中的語句的確簡單多了,再也看不到該死的if else了^_^酷~~~



可實際上是這樣簡單嗎???仔細研究一下上面的程式碼就能看出問題出來,首先這兩段程式碼中的A、B、C的實際意義是不一樣的:第一段程式碼中ABC代表的是某一個邏輯條件的值而第二段中的ABC則是具體的類;第二段得到如此簡化的前提是它得到的邏輯條件就是一個類;如果得到的仍然只是一個邏輯條件,那麼為了達到程式碼簡化的效果,必須由另一個類(或方法)完成這種邏輯條件到具體類的轉換,會出現類似下列的程式碼

class RunnerFactory{

runner getInstante(case){

if (case A) return new A();

else if (case B) return new B();

else if (case C) return new C(); } }



從測試的角度來說,兩者的測試分支都是3,複雜度相同,而第二種方法在很多的時候貌似還要測試藉口的有效性。用策略模式還有一個缺點就是會使系統中的類的數量大大的增加,如上的例子,採用if else類的數量為1,而採用策略模式的類個數為5個或6個(主要取決於邏輯對映是否用單獨的類)。

如果邏輯判斷的條件有三個,每個邏輯條件有三種可能的話,用策略模式系統至少會增加10個新類;如果條件更多的話類的個數也會更多 這麼看來GOF的策略模式還要它幹嘛??

當然不是,策略模式在很多情況下是一種非常好的解決方案。

這還要從if else 語句造成程式複雜以至難以維護的真正原因說起。就我個人的感覺真正造成if else語句難以維護的原因是每一個邏輯分支中的處理語句過長。比如我現在工作中維護的程式碼,常常一個條件下面的業務處理語句有兩三千行,每次我光確定某個邏輯分支的結束位置就要找半天,頭暈-_-!。如果是多層條件的話情況就更糟了。一個分支就一千多行,幾個分支上萬行自然很難維護。 if else 語句本質上是程式的流程控制語句,而分支中N長的程式碼通常是業務處理語句。

行為模式/策略模式就是把流程判斷和業務處理進行了一次解耦,將業務邏輯封裝成一個個單獨的類。換句話說,行為模式/策略模式並不是不需要if else 語句(事實上該判斷的還是要判斷),只不過的換了地方或者是別的程式碼幫你做了。另一方面,進行邏輯判斷的語句被集中起來而不是分散在程式的各個角落,有利於邏輯本身的維護。策略模式/行為模式還有一個明顯的好處就是如果新增加了一種狀態,我們只需要新增加一個策略類(同上的ABC)就可以了,避免了在程式中改動那些大段大段讓人厭煩的if else 語句。

所以對於你的程式來說到底是使用設計模式還是簡單的使用if else 關鍵在於你的程式是否複雜,有沒有必要將控制邏輯和業務邏輯進行解耦。當然如果你可以用別的方式實現解耦也是非常好的。



2) Response for Class(RFC) 當一個類和很多其他類存在依賴時,它就變得複雜甚至難以修改和維護,這樣,RFC值越大,表示你的系統味道越壞。(引自《軟體的最大追求是什麼》)



複雜性是由類與類之間的依賴關係(dependency)造成的。

具體如下所示:

interface Runner;

class A implement runner{ do(){}; }



一個大型的系統中很多地方用到了runner介面,於是在很多地方出現瞭如下的相同程式碼:

{

Runner r = new A();

r.do();

}



如果因為某種原因runner介面的實現類改為B,則在所有用到runner介面的地方程式碼都要統統改為:

{

//Runner r = new A();

Runner r = new B(); r.do();

}



這些遍佈系統各個角落的改動是繁瑣且容易出錯的。

於是出現了各種框架和模式,其中最著名的當然是IOC(Inversion of Control)反轉控制或者稱之為依賴型注射(Dependency Injection) 那些討厭的程式碼變成了如下:

{

ApplicationContext ctx = new FileSystemXmlApplicationContext("spring.xml");

Runner r = (Runner)ctx.getBean("Runner");

r.do();

}



這樣我們就不需要對各個角落的程式碼進行維護了,容器會自動的為我們選擇合適的類。

我到覺得維護的工作還是我們的,為了讓容器注射入合適的類,我們必須要維護一個叫spring.xml的配置檔案,或者如果你不喜歡配置檔案這個東東的話(比如偶)就得自己寫一個註冊類,將依賴關係一一註冊進去。你還要為使用該介面的類定義一個以該介面為引數的建構函式或者該介面的setter方法,好讓容器可以將實現類順利的注射進來。

該做的還是要做,只不過換了一個地方做而已。但就是換了個地方,實現了對依賴關係的集中維護(又是集中),大大的改善了系統的結構,明確了不同職責單位之間的分工。呵呵,有時自己也極端的覺得設計的工作說到底就是解決程式碼結構的問題^_^



IOC一直是和輕量級框架聯絡在一起的。所謂的重量級框架EJB也有實現相同功能的解決方案:Service Locator Service Locator就是一個定位器,它維護了一個資料結構(比如一個表),透過這個定位器你可以準確的定位到你的介面想要的實現類,Service Locator同樣使你免去了改變介面實現類後的維護惡夢:

{

Runner r = (Runner)ServiceLocator.lookup("Runner");

r.do();

}



無論是IOC還是Service Locator都幫助你維護了類之間的依賴關係。那麼是否我們在程式設計中一定要用呢,這又是個權衡的問題,IOC帶來了很多的好處,不過我個人認為它的程式碼是讓人費解的(你根本不知道它做了什麼),並且為了用它,一方面你要藉助於容器,另一方面你要編寫配置檔案,要為依賴型注射提供合適的途徑。

如果你的系統類之間的依賴型錯綜複雜,需求的變化常常導致實現類的變化,同時你又希望採用測試驅動的快速開發模式,IOC毫無疑問是一個完美的解決方案;如果你的系統不存在上述的問題,為什麼不簡簡單單的在程式中寫死呢,何苦去維護一堆配置檔案(我所在的開發部門貌似都比較痛恨配置檔案這個東東)。Service Locator也有很多缺點,被罵的最多的就是沒法快速測試。

反轉控制,即轉換控制權。依賴關係控制權的轉換是對程式碼結構的一次重構,重構的目標還是解耦,讓不同的職責程式碼集中放到不同的地方,於是程式設計師可以更加專注的解決特定的問題,比如業務邏輯。

程式設計的發展就是對程式碼結構的不斷調整,不斷解耦,讓特定的程式碼解決特定的問題而不是什麼都混在一起。從程式導向到物件導向難道不是這樣嗎,封裝的本質也是解耦。



在實際問題的解決當中,最重要的信條就是合適,要記住任何結構的改進都會付出代價,你的改進是否值得你為此付出的代價。比如當你做一個嵌入式程式的時候你首要考慮的自然是效率問題;而如果你做的是一個ERP產品,在系統設計的時候,光系統的可維護性問題就讓你不得不絞盡腦汁考慮一下程式碼的結構。



一句話,只做最對的。

程式設計的最大追求就是在合適的地方做正確的事。

相關文章