幅度最小的程式碼演化,是Method引數列的擴充。更動幅度稍大者,則是類別Method的新增(也就是類別介面的擴充)。無論影響的幅度有多大,只要是程式碼的擴充,都必須思考:我們想新增的事物,是否與舊有的事物存在著某種共通性?
基本上,無論是引數列的擴充,或Method的增加,重點都會放在共通部分的提取,以避免重複程式碼所造成的種種問題。此外,更需要留意的,是避免造成副作用,也就是在將共通部分提取出來之後,不會造成舊有程式碼行為有所變動。
每次的改變,都必須考慮演化的方式
類別的新增,是更大幅度地擴充型態。隨著需求的增加,你會開始擴充特定類別的介面,也就是增加它們的Method。倘若沒有適度地演化既有的程式碼,只是一味地把新的程式碼,以新Method的方式,添加到特定類別中,那麼很容易讓這些類別的規模和責任,超出它所應承擔的程度,因而形成超級類別(Super Class),也就是過度龐大的類別。
我曾提過「過大的類別(Large Class)」的問題。好的程式碼演化,就是要避免造成日後要重構的問題。當我們試著在現有的類別上增加新的Method時,程式人必須思考,當這些增加的Method加入後,是否有必要重新調整類別之間關係,而所謂調整類別之間的關係,包括:是否要增加新的類別?是否改變既有類別之間的繼承關係?是否增加新的介面?是否改變既有類別實作介面的關係?
每當你要在某個或某些類別加入新的Method時,都必須詢問自己是否已經滿足了必須抽取出新類別的條件,而不再將新增的Method加到原有的類別上,繼續擴充規模。這就是演化的精神所在,每次的改變都必須考慮到2件事,該怎麼演化,以及朝那個方向演化。
為類別加入Method時,必須關注Method之間的相關性
最簡單的類別抽取,就是一些公用類別(Utility Class)的抽取。撰寫某一類別時,你可能會為它寫下一些相當獨立,本身又提供特定作用的Method。例如,在某個類別裡寫下了一個叫做trimHTMLTags()的Method,作用是過濾掉字串中所有的HTML標籤。
在最初加入這個Method之際,或許看不出它有獨立成為一個類別的潛力。但是,隨著時間的過去,當我們再度為這個類別加上另一個叫做unescape()的Method,以便用來還原字串中被HTML跳脫掉的字元,例如’&’。在加入這個Method時,就應該發現兩個Methods之間存在很強的相關性──它們都是用來處理和HTML相關的字串操作。
最初為類別加入一個Method(例如trimHTMLTags()),這個Method和類別的相關性也許不是很足夠,但因為此類別需要用到它,而短期間也還看不出來它有獨立成為一個類別的需要,所以先讓它成為一個Private Method。
但是,隨著陸續加入新的Method到專案中,這些Method或許加至同一個類別,或許是加至不同的類別中,但你必須觀察,並且識別出它們之間的相關性,而且在出現足夠多的數目時(例如三個),將它們抽取出來,成為獨立的類別。
例如,原有trimHTMLTags(),進一步想增加unescape(),於是將trimHTMLTags()自原類別中抽離,連著unescape()一起加至新類別HTMLUtil中,然後讓原類別呼叫HTMLUtil中的Methods,以保持原類別中的舊有程式碼仍能作用。
演化必須留意對原有程式碼的衝擊
上述類別的抽取是最單純的情況,因為它不涉及類別繼承體系的更動,它只是增加新的類別,而這個新類別與原有類別之間只有很低的關聯性,並不存在繼承或被繼承的關係,因為增加的Methods在概念上,本來就不屬於原類別的一部分。
倘若新類別的加入會牽動到原有的繼承體系,那麼就會複雜一些。加入類別時,繼承體系可能會有的變化包括:
1. 加入一個獨立的類別,不改變任何繼承關係。
2. 加入一獨立子類別,讓它繼承現有的類別。
3. 加入一個獨立的子類別,打算讓它繼承現有的類別,但是發現它與它的某些兄弟類別之間存在某些共通性,於是將這些共通性抽取出來,成為這些子類別的共同父類別,而原有的父類別則升級為祖父類別。
其中最複雜的,莫過於第三種情況。在新增一個子類別時,你發現想要新增的類別,和既有的子類別們存在共通性,它們有共用的Methods。但是因為這些Methods並不在父類別的介面中,因此,倘若再度於新類別中複製相同的程式碼,便會引發重複程式碼的問題。
這些重複的程式碼,搬移至父類別其實也不妥,因為並非所有的子類別都有此共同的特性,部分子類別才會動用到的程式碼,如果置於父類別並不恰當,因為父類別所描述的,應為所有子類別的共通特性。
因此,在這種情況下,你必須為部分具有共通特性的子類別,重新建立新的父類別。在繼承體系中這些子類別被往下降一層,而新立的父類別則位居它們原先的層級,原有的父類別則搖身一變,成了這些類別的祖父類別。
任何程式碼的演化,都必須留意對原有程式碼所造成的衝擊。在上述繼承結構的調整中,雖然部分介面被移至了新立的父類別,但由於仍然繼承了新立的父類別,而新父類別也繼承了祖父類別,所以對這些子類別來說,介面(也就是具備的Methods)仍舊可以保持不變,理想情況下,原先和這些子類別有關連的類別,也都不會受到影響。
時時留意各種徵象,在必要時刻進行必要的演化
除了繼承體系可能受到影響外,有時新增的類別也會引發我們加入新的介面。例如,原先撰寫RDBMSAuthenticator類別,後來打算增加LDAPAuthenticator,這時發現二者之間會有共通的介面,但沒有可共用的程式碼。所以,為它們設定一個共通的介面是最適合的。
為什麼要為它們增加一個規範共通介面的介面呢?在許多情況下,你會希望套用一些生成設計模式(Creational Patterns),來處理有著共通介面、卻是不同實作的類別的生成。
例如,對於像RDBMSAuthenticator及LDAPAuthenticator的類別,你可能會想套用Simple Factory Method設計模式處理它們的生成動作。當已有RDBMSAuthenticator,而想進一步增加LDAPAuthenticator時,你必須意識到,需要套用Simple Factory Method,於是,你得為它們增加一個新的介面以描述Simple Factory Method產物的共通介面。
程式碼的演化是漸進式的,你得在每個可能的時間去判斷是不是已到了進行某種演化的時機。做為程式人,倘若你希望程式碼的發展,總是持續朝向比較健康的方向前進,那麼就得時時注意各種演化的徵象是否已經出現,並且在必要時讓程式碼進行我們想要的演化方式。
作者簡介:
王建興
清華大學資訊工程系的博士研究生,研究興趣包括電腦網路、點對點網路、分散式網路管理、以及行動式代理人,專長則是Internet應用系統的開發。曾參與過的開發專案性質十分廣泛而且不同,從ERP、PC Game到P2P網路電話都在他的涉獵範圍之內。
相關連結
程式碼的演化之路(1)持續讓程式碼保持進步的能力
程式碼的演化之路(2)擴充新功能,記得善用重構的技巧
熱門新聞
2026-01-12
2026-01-12
2026-01-16
2026-01-12