程式碼變動所造成最大的成本,不在於你必須耗費時間人力改寫程式碼,而是在於必須確保程式中未更動的部分,不致於受到變動的部分所波及。
所以,當我們在擴充程式碼的時候,你會發現,多數時候都把力氣花在調整程式碼本身的結構性元素,例如改變函式的外貌式(Signature)、改變類別的介面(所擁有的函式)、增加類別、增加介面、調整類別與類別間的繼承關係……等。
除非是全新的函式或類別,否則,對於現有的程式碼,我們盡量只是搬動位置,而不大幅度的變更。這麼做的原因,是希望將變更造成的衝擊降到最小。
如果只搬動程式碼,調整程式碼與程式碼間的介面,發生副作用的機會也會因此降低許多。因為程式碼介面的相容與否,幾乎可以在編譯時期由編譯器檢查得知。
能夠保有介面的相容性,就可以避免大多數的副作用。
避免大幅修改既有程式碼
程式碼一旦遇上非擴充功能不可的情況,也就是必須對既有程式碼執行某種程度的修改時,就無法只憑藉著加入全新的程式碼,或者是搬動現有程式碼,就達成目的。這麼一來,程式改變所造成的影響層面便會大上許多。
所以,在程式碼演化時,應該要盡量選擇擴充的方案,去代替變異的方案。如果你需要一個新的功能,它和舊功能有點相像,而你認為舊功能在系統中是可以淘汰的,很多人或許會選擇直接將舊功能修改成為新功能。但是在實作上,你如何確定沒有任何既有程式碼(Legacy Code)與舊程式碼存在相依性呢?
倘若既有的程式碼與打算修改的程式碼中的事務邏輯(Business Logic),彼此之間存在實作上的相依性,那麼修改舊程式碼,就很容易造成副作用。因為實作相依性和介面相依性(例如相依於函式的引數列表、類別名稱)不同,後者能夠倚靠編譯器檢查。但倘若存在的是前者,即使能夠保持介面的相容性,也很容易引發副作用。
所以,盡量避免直接修改既有的程式碼,最好的方式還是增加新的程式碼,滿足新的需求,而倚賴舊程式碼的其餘客戶端程式碼,則漸進式地移轉到新程式碼,直到沒有任何客戶端程式倚賴舊程式碼為止,才將舊有的程式碼移除。
這樣的做法,好處是舊有的介面不變,舊有的實作也不變,只是增加新的介面和新的實作。任何倚賴舊有程式碼的用戶端程式碼,都不會受到影響。讓舊有的程式碼逐漸地淘汰,能夠緩和對整個系統的影響。
eXtreme Programming的方法是彼此交互支援的,缺一不可
雖然程式人都知道「要針對介面而寫,不要針對實作而寫(Program to interface, not implementation)」,但是在實務上,事務邏輯的相依性,有時真的很難避免。
利用程式設計的技巧,降低改變程式碼時造成的副作用,當然會有很大的作用。不過,再怎麼天才的程式設計師,都無法保證自己對程式碼所做的變動,不會產生任何副作用。
為此,我們會需要工具輔助。
現在有很多人標榜他們的開發方法論是採用eXtreme Programming,也就是所謂的XP。因為他們認為自己採用的開發方法是輕量級、不寫大量且完備的文件、需求變更頻繁、程式碼自然也變動得十分迅速。
我看到很多產品的開發,在內部都是採小量發行的做法。也就是說,在真正的對外發行之前,內部已經不知道發行過多少版本。這種開發的方式,有一個很主要的目的,是為了要靈活調整產品的規格及功能。
這樣的開發方法,乍看之下好像和XP十分符合,他們動態地調整規格及功能,也動態、迅速地改變程式碼。但是,許多宣稱採用XP的開發團隊,不過也只是半吊子(甚至一半都不到)的XP罷了。
要實踐XP,要落實12個基本的作法,但是,有許多團隊只取所需,但捨所不欲。
舉例來說,「小量發行」正是這12個基本的作法其中之一。許多團隊都喜歡小量發行,因為這使得他們得以在開發過程中,持續地擴充、修改他們的產品規格。
「小量發行」的作法很自然地會引發程式碼持續性演化。許多開發團隊沒有認知到,在XP的開發方法中,有許多是彼此交互支援以為後盾的。
就拿小量發行來說,它會導致程式碼變更,而頻繁的程式碼變更對開發來說,其實是很不利的。因為那意謂著變更引發的副作用,會長期發生在整個開發過程中。
所以XP利用「程式碼重構」解決頻繁修改程式碼時,程式可能會產生的結構問題,再利用「測試」解決可能會引發的副作用。
徹底執行單元測試,讓修改程式更有信心
倘若我們在修改程式碼的同時,能夠把握演化程式碼的方向及原則,即可盡量避免日後再重構的需要(事實上,演化程式碼時,幾乎都在運用重構的技巧)。但是,倘若我們採取了擴充之外的改變動作,那麼就會面臨到副作用時時刻刻的威脅。
為此,必須選用好的單元測試工具輔助我們修改程式碼。例如,對Java程式人來說,JUnit是個不錯的單元測試工具。而如果你是一名C++程式人,你或許會想選擇CppUnit進行程式碼的單元測試。
單元測試是針對程式碼中的單元加以測試。以eXtreme Programming為例,這個方法希望程式人在撰寫程式碼前,先寫好程式碼的測試案例。針對每一個程式單元,程式人都設計好相對應的測試案例,以便在單元的層級上,對程式碼執行測試。
徹底的單元測試,能讓我們在修改程式碼時更有信心,單元測試能夠做為程式人的依靠,當我們不小心製造出副作用時,可以盡可能地憑藉單元測試,找出問題所在。
倘若你的系統是那種程式碼變動十分頻繁的系統,那麼更應該徹底地落實單元測試。如果使用諸如Ant之類的自動化建構工具(Automatic Build Tool),更可以考慮將執行所有測試案例的工作,整合到日常的建構流程中。
例如,每天自動地從版本控制系統上簽出一份完整的程式碼,並且加以建構,然後執行所有的單元測試案例。越是頻繁地實行單元測試,越是能夠從頻繁的修改程式碼動作中,查覺到副作用及臭蟲的存在,當然也就越能提升「修改」行為的品質。
當你想要修改程式碼時,在設計面要留意的,還是盡量以擴充取代變異。雖然這麼做,你會短暫地讓系統中依舊保有看似無用的程式碼,但是隨著漸進式地淘汰掉舊有的程式碼,就能夠緩和修改所造成的影響。
想要持續演化程式,所面臨的天敵,就是無處不發生,而且難以預期的副作用。我們必須嚴肅又審慎地看待任何一段修改可能造成的、不在預期中的效應,而單元測試工具便是我們在這天敵無時無刻威脅下的最好後盾。
作者簡介:
王建興
清華大學資訊工程系的博士研究生,研究興趣包括電腦網路、點對點網路、分散式網路管理、以及行動式代理人,專長則是Internet應用系統的開發。曾參與過的開發專案性質十分廣泛而且不同,從ERP、PC Game到P2P網路電話都在他的涉獵範圍之內。
程式碼的演化之路(1)持續讓程式碼保持進步的能力
程式碼的演化之路(2)擴充新功能,記得善用重構的技巧
程式碼的演化之路(3)要小心副作用,注意類別與繼承的管理
程式碼的演化之路(4)持續小幅度改變,才是安全的調整方式
熱門新聞
2025-01-13
2025-01-14
2025-01-13
2025-01-14
2025-01-13
2025-01-10