面對Legacy Code,你可以選擇整個「砍掉重練」,也可以選擇用漸進、演化的方式來翻新它。前者可以畢其功於一役,毋需顧慮和舊有程式碼的複雜相依性關係。而後者則可邊移動、邊瞄準、邊射擊,不斷視情況做調整,將每次整新 Legacy Code 的風險降低,也可以很快看到成效,並且為翻新工作帶來更多的成就感和信心。

如果你想要用演化的方式來翻新你的Legacy Code,那麼有一個方式是我喜歡用的,你可以參考我的做法。在前文中提到,我偏好處理 Legacy Code 的步驟是:(1)決定更新的邊界(2)依據邊界範圍設計介面(3)實作新介面的轉接器實作(4)把客戶端程式碼升級到新的介面版本(5)依計畫將轉接器實作更新為全新實作。前一回略談這五個步驟,而這一回,試著更細節性、偏重實作面的說明。

決定更新的邊界
「決定更新的邊界」談的就是決定要翻新的範圍,當你面對一整個系統,或是很大的範圍都是 Legacy Code 時,你可能不會想要全部都一次翻新掉,你多半只會想翻新其中的一個子系統,甚至是子系統中的一個模組。一次翻新一個局部,也符合漸進、演化的宗旨。

你所決定要翻新的系統局部,最好要有一個大致上的邊界,也就是有著一定的模組性。從程式碼之間的相依性來看的話,這個局部也就是模組,應該在模組內的內聚力高,而模組對外的耦合力低。在模組之外、卻又和模組有所互動的程式碼,我們稱為「客戶端程式碼(client code)」。從這個名稱的語義來看,客戶端程式碼是把模組本身當做是一個服務來使用,所以它成了這個模組的客戶了。

有些既有的「模組」,其實不那麼的模組化。它所提供的「服務」,其實也不是那麼的精確。就像是「模組」設計當初,其實並沒有一個明確的介面設計存在,所以模組允許客戶端程式碼長驅直入,直取模組內部,藉以取用模組所提供的服務。但是,對翻新工作來說,基於一個明確的介面設計來做絕對是好事,不單只是符合當前設計的潮流。所以我們要翻新的組件就包括了「介面」、「翻新模組程式碼」、以及「客戶端程式碼」三個。「介面」一刀畫開了「翻新模組程式碼」及「客戶端程式碼」,明確將它們定義出來。

依據邊界範圍設計介面
因此,決定了「更新的邊界」之後,下一個步驟就是「依據邊界範圍設計介面」。設計介面的工作很重要,因為你可能需要先分析在舊有的系統中,客戶端程式碼是如何和模組中的程式碼互動,它們究竟需要取用多少模組所提供的服務。如果尚沒有明確的「服務」概念,你甚至必須先分析出來,重新定義、設計模組為客戶端程式碼所提供的服務。

所完成的「介面」必須滿足現有客戶端程式碼對模組服務的「需求」。也就是說,不會因為重新設計、調整了介面,就使得現有的客戶端程式碼,在失去了原本所依賴的舊有程式碼之後,無法運行。因此,新設計出來的介面涵蓋的範圍,必大於等於原有客戶端程式碼的需求。如果你只考慮到現有的情況,那麼介面就會等於現有的客戶端需求,若是你考慮到未來的通用性及擴增可能性,那麼設計出來的介面所能提供的服務,就會比原有的還多了。

設計此介面除了考慮對需求及未來擴充性的滿足之外,同時也必須考慮到對現有程式碼的變動盡可能的小。因為,當你重整介面之後,不論是客戶端程式碼或是模組內的程式碼,都必須有所變動。對客戶端程式碼而言,它們必須被調整成僅依賴此一介面,而不再直接依賴模組內的程式碼。而對模組內的程式碼來說,也必須進行一些修改,甚至加強之後,才能提供新介面想要提供給外界的功能服務。因此,此介面在設計時,除了考慮到需求是否滿足、日後是否易於擴充之外,也必須同時綜合考量介面兩邊的程式碼接下來的變動幅度。對於那些已經被現有客戶端程式碼依賴的服務,最好是做一點額外的加工,就可以將模組內的程式碼轉換過去。

實作新介面的轉接器實作

一旦決定好介面設計之後,這個演化式的翻新方法,其精神便在採用一個「轉接器(adapter)」的方式來實作介面。有了新介面之後,我們可以開始就現有的模組內程式碼,做小幅的加工,來實作出這個介面。這些小幅度的加工就是「轉接器」的作用。它的目的應該只是轉接,也就是轉換介面傳來的輸入參數,將輸出結果轉換成為介面所需的輸出結果。實作還是原本就有的實作,轉接器並不改變 Legacy Code 裡原本的實作方式。至於那些尚未被現有客戶端所依賴的服務,一開始是空的實作也無妨。

有了介面、有了新的轉接器實作之後,你就可以針對轉接器做單元測試。一方面因為真正的實作理論上沒有任何變動,所以,加工工作不至於帶來嚴重的副作用。二來,有了定義清楚的介面,你會更易於檢驗此介面背後實作的正確性。

把客戶端程式碼升級到新的介面版本
完成轉接器實作之後,下一步便是「把客戶端程式碼升級到新的介面版本」。因為目前客戶端程式碼還是直接依賴模組內的程式碼,因此,你必須把它們修改成為依賴新版介面。原先客戶端程式碼中,可能是像散彈打鳥地存取模組內部的程式碼,例如,它們不會固定集中呼叫特定類別所提供的函式,而是零散、各自取用不同的類別;而有了代表模組服務的介面之後,客戶端和模組內部程式碼的錯綜複雜關係,就必須予以斬斷,改為只倚賴新設計的介面。

如果你所用的語言,提供類似一些物件導向程式語言的權限控制存取修飾詞,那麼這就會很好做。你只需要把提供介面實作的「轉接器」類別設為公開(public)存取,而其他的類別都設為模組內存取(像 Java 就稱為 package 存取)的權限,即使客戶端程式碼所依賴的模組內類別都依然存在,但是因為權限修飾詞限縮了,所以編譯後,客戶端程式碼依賴的部份就會引發編譯時的錯誤訊息,使得你可以逐一修正這些對內部的依賴關係,而不致於遺漏。

確定可以編譯之後,代表你成功斬斷了客戶端程式碼對 Legacy Code 的依賴,將它們轉換到升級後的介面版本。接著你可以測試整個系統是否正確運作,這應該不致於有太多意料之外的事,因為我們所做的變動是那麼小,我們加了介面、加了轉接的程式碼、調整客戶端的依賴關係而已,背後的實作一點也沒有變動,這是這個方法的好處。

依計畫將轉接器實作更新為全新實作
一旦你可以得到基於轉接器實作的新介面,又能正常運作的系統,那麼,接下來就是進可攻退可守的局面了。即使目前的轉接器實作還是舊版的、你可能不那麼喜歡、有著一些已知缺陷的 Legacy Code,但起碼,客戶端程式碼已經升級到一個新版的介面上了,這意謂著客戶端本身的程式碼已經進化了,它們不再像散彈般的亂入模組之內、難以控制,相反的,它們應該整齊畫一,顯現出新設計的優良之處。

再者,有了新介面做為阻隔,你可以逐步汰換掉以 Legacy Code 為基礎的實作,每次換掉一些,介面以外都維持不同,所有的變動都只會發生在介面之內,也就是把轉接器實作換成你希望的新版實作。每次的汰換都拿掉部份 Legacy Code,解決了部份陳年老碼的問題,也為系統換上了一些新血。

這個漸進法的好處就是,每次的變動都不大,使得你可以很快完成變動,並且評估效應,而且風險也更好控制。而且,每一次的小變動之後,依舊可以維持系統正常運作。這正是演化的精神所在。你不用等到完成一個大工程之後,才可以看到結果,而是每次完成一個小工程,都可以看到成效,不斷鼓舞你和你的團隊持續將所有的 Legacy Code 都換掉。

 

作者簡介


Advertisement

更多 iThome相關內容