程式者的胡言亂語

pageicon 星期二 十一月 06, 2007

物件導向程式設計中常見的錯誤(1)

Martin Fowler所著的《重構(refactoring)》,介紹了如何系統性的在不更動軟體系統既有功能的前提下,透過調整程式碼,來改善軟體本身的品質及效能,使得程式碼更具可讀性、系統架構更具擴充性、也更容易維護。這「重構」是一整套系統性的方法,而在這套方法中,我認為最有價值的地方之一,在於「重構」方法針對程式碼或系統架構可能會有的缺失,整理出一份完整的列表。這一份完整的列表就好比病人可能會有的病癥(在《重構》中稱為bad smells in code),程式員便可以拿著這份列表按圖索驥。「重構」方法不僅整理病癥,更提供了藥方。程式碼找到了病癥,便可以服用Martin Fowler所寫下的藥方,各式冷熱雜症一帖見效。

儘管重構方法中寫下了十分完備的各式程式碼的病癥,但有幾項可以算是患病率極高的,透過改善這幾項問題,便能有效的改善程式碼本身的品質。本回就讓我們來看看患病率高居不下的兩種問題吧:(1)過度冗長的methodlong method)以及2)相似或重複的程式碼(duplicated code)。

首先就讓我們先來看「過度冗長的method」這個問題吧。在物件導向程式設計中,物件的method是一種功能單位。public method代表著物件對外的介面,物件能提供的功能規格;private method則代表物件內部實作的功能區塊。物件的method的作用,便有如程序式程式語言中的程序(procedure)或函式(function)一般。在程式語言的發展中,之所以發展出程序及函式的概念,在於程式語言的設計者,查覺出程式中時常會有重複作用的程式碼區塊,因而衍生出將重複作用的程式碼區塊抽離成為單一的程序或函式,進以避免重複實作相同的程式碼,同時也降低程式碼佔去的空間。

如何決定那些程式碼區塊構成單一個函式,是考驗程式員功力的地方。但一般來說,會從語義的角度來衡量一個函式應涵蓋的程式碼內容。正如前文所提到的,做為物件的public method,代表這個method提供給用戶端程式碼的,是一個單一的服務。從服務的觀點來看,便很容易規劃一個public method究竟應該提供什麼功能。有許多程式員,會將實作public method所提供之功能的程式碼,通通寫到這個method裡,也就使得method本身的程式碼長度變得十分冗長。就功能上的實作來說,這樣的做法一點問題都沒有,但從程式碼的品質觀點來看,卻是大有問題。怎麼說呢?

首先是程式碼的可讀性問題,當一個method過於冗長時,往往代表其中尚有可以拆解出來的語意區塊,但卻未加以拆解,這使得該method的內容皆由最低階的程式述句來表述,再加上冗長的行數,對程式碼的閱讀者來說,無法迅速且輕易的從高階的角度,理解整個method的行為及內容。另一個重大的問題便是可重複運用性並沒有被發揮。一個冗長的method,幾乎都包含著多個可以被再拆解成為更小method的語意單位,而這些可被拆解出的小型method,在系統中往往可供其他的大型method所叫用。試想以下的情境:為了實作物件的public method A所欲提供的強大功能,你將所需的程式碼全都寫到這個method裡,造就了一個long method-而這正是很多人撰寫method的方式。當你開始著手撰寫另一個物件需要提供的public method B時,你發現在這個新的public method B實作裡,也會需要method A中所包含的一段完全一模一樣或者高度相似的程式碼,這個時候你會怎麼辦?

你會將這兩個method所共同需要的部份,抽離出來成為單一的private method C,並且在method AB中叫用method C以滿足AB的需求,還是選擇直接將method A中的程式碼複製到method B中直接使用或是做小幅度的修改呢?有滿高比例的程式員,會選擇複製及貼上(copy & paste)的方式,而這其實同時也衍生出另一個bad smell相似或重複的程式碼duplicated code)。除了duplicated code的負面影響外,copy & paste的做法,也阻斷了讓該段程式碼獨立做為可被重複運用之程式單元的可能性。因為這理應可成為method C的程式碼片段,被偷偷的藏在了method Amethod B中,接手開發或維護的後人,倘若不知道method Amethod B之間有這段關係,便有可能自行撰寫和method C作用相當的程式碼,甚至從method AB其中之一進行copy & paste的動作。但倘若method C已被明確的抽離,接手的後人便有很高的機會,會明白到這個被標定為獨立函式的語意單元,也會更有機會加以重複的運用。

冗長的method相似或重複的程式碼之間的關係,就好比狼跟狽這兩種動物之間的關係一樣,時常是相伴出現的。會有冗長的method出現,多半都是因為程式員未將冗長的method中可被抽離的程式碼單元獨立出來,而這些可被抽離的程式碼單元,往往又具重用性,時常在別的程式碼中會需要用到,造成了其他程式碼不是採用copy & paste的方式來含括這段程式碼,就是在未查覺既有程式碼的情況下,重新再實作一遍。

相似或重複的程式碼其危害又是如何呢?前文已經提及,相似或重複的程式碼,多半源自於許多程式員習以為常的copy & paste的動作。當程式員需要某一段程式碼來達成一段功能,而系統中又已經存在相同或類似的程式碼時,許多程式員所選擇的,便是直接將既存的程式碼複製一份,貼到撰寫中的程式碼中,再施以小幅度的修改,來達成自己的目的。如此一來,系統中便會處處充滿著具有相同血統或再雜以其他血脈的程式碼。相似或重複的程式碼的根本危害在何處呢?如果位居血統最根源位置的那段程式碼,被發現有了問題,需要加以修改。這個時候,由於這段程式碼的子子孫孫們可能已經在系統中開枝散葉,不僅可能難以找出衍生於此的所有程式碼,即便能夠找出,也得大費周章的將所有的程式碼一一改正。

重構方法中的「提煉函式(Extract Method)」是對付過度冗長的method相似或重複的程式碼的主要方法之一。所謂「提煉函式」,便是將程式碼中的某一片段,予以抽離出來成為獨立的method。當你發現你的某個method過於冗長時,便應該考慮將其中的可獨立的語意單元抽離出來,另行化為一個單獨的(較低階的)method。這麼一來,較高階的method,閱讀起來便像是由數個低階的method所組成,容易從其代表的語義來理解高階method的運作,不致於被method中較不具語義代表性的冗長程式述句影響到理解。

相似的,當你發現你正在撰寫的程式碼,在其他的地方已經存在相同或相似的片段時,你不應該使用copy & paste來運用原處的這段程式碼,相反的,你應該將既存的這段程式碼予以抽離出來成為獨立的method(同樣是提煉函式的手法),並在原處及新處,透過叫用method的方式,來運用這段程式碼。

另一個有趣的問題是,在過度冗長method的這個問題中,究竟一個method行數要有多長算是過長呢?這問題不好回答,基本上和method本身的內容以及實作的程式語言有關(畢竟不同程式語言的抽象表示力也不同)。而每個人習慣的長度也不同,但從問題的源頭推敲起,Martin Fowler所謂「語義的距離」我想才是衡量的標準所在。也就是說,從該method名稱所代表的語義與method中程式述句所代表的語義之間的落差究竟有多少。倘若method名稱本身代表著高階的語義,但其中包含的程式述句卻極其低階,這意謂著它所包含的程式述句,有需要被抽離成具備中階語義的method,以供此高階method之用。當這些程式述句被抽離成為中階語義的method時,高階method乃是由中階述句所表述,那麼其中存在的語義距離也就因而縮短了。

許多系統的程式碼都廣泛的存在本文中所提到的兩個問題,也同樣的深受其危害。這兩個問題雖然常見,但卻是許多程式碼品質問題的根本。倘若能夠妥善的利用重構的技巧來加以解決,對程式碼品質必有顯著的提昇效果。

Blogged with Flock

迴響:

發表迴響:
迴響功能已被關閉
把對母乳媽媽的感謝與支持傳出去

« 九月 2010
星期日星期一星期二星期三星期四星期五星期六
   
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
  
       
今日

Search this blog

Links

Weblog menu

Today's referrers

Feeds