本文是“松結對程式設計”系列的第十三篇。(松結對程式設計欄目目錄)
松結對程式設計包括L型代碼結構的一個很大的問題,是在於由於人們複用了太多的代碼,以至於所有代碼牽一髮而動全身。這很容易導致一個底層庫改動後,很多地方編譯不通過,或編譯通過但運行不通過。
本人曾經擔任過底層庫的編寫者,在修改和維護底層庫的過程中遇到了很多問題,也發現了一些訣竅,下面是一些編碼層級的處理訣竅。這些訣竅其實都寫在教科書上,學習和使用起來也很簡單,只是到用的時候,被很多人忽略了。
1. 預設參數如果要給一個函數增加一個新的變數,因這個變數的不同而產生新的行為方式,那麼預設參數是一個很好的避免之前的代碼需要相應改動的方法。比如:
public static MvcHtmlString ImageLink(string text, string url, bool showInNewWindow = false, bool showText = true, bool showImage = true, string title = null, string outerLink = null, string imgUrl = null, string imgSuffix = "16.png", string imgCssClass = "icons", string updateTargetId = null, string display = null, string onSuccess = null, bool runOnSuccessWhileLeft = false, string id = null, string imgStyle = null, string cssClass = null, string textColor = null, bool displayAsText = false, string displayAsTextSuffix = null, string displayAsLinkSuffix = null, WebViewPage returnTo = null, string returnUrl = null)
這個被維護過無數次甚至有點過度設計的函數,能顯示為文字連結,能自動搜尋表徵圖,能作為在Ajax串連使用,能根據當前值選擇顯示為文字還是連結……但最樸素的用法,始終保持為Text和Url兩個參數,比如:
@MFCUI.ImageLink(item.Title, "/MFC/Items/Edit?id=" + item.ID)
這樣初期的、簡單的調用就不用修改。2. 重載或重寫一個函數如果變化太大,重載乃至重寫一個新的函數會更好一些。下面這一大堆,都是火星人中為瞭解決個人對產品的可訪問性問題的,但具體應用情境有很小的變化。為了防止每次都多那麼幾行代碼產生變化,乾脆把差別封裝在函數內部。
public static IEnumerable<Product> ProductsAccessibleToUser(string user) { var productsAccessibleToUser = new List<Product>(); foreach (var teamID in Department.TeamsContainUser(user).Select(i => i.ID).ToList()) { productsAccessibleToUser.AddRange(ProductsAccessibleToTeam(teamID)); } return productsAccessibleToUser.Distinct(); } public static IEnumerable<int> ProductsAccessibleToUserIDs(string userName) { return ProductsAccessibleToUser(userName).Select(i => i.ID); } public static string ProductsAccessibleToUserIDsString(string userName) { return ProductsAccessibleToUserIDs(userName).Aggregate<int, string>(null, (current, id) => current + (id.ToString() + "_")); }不過,重載和預設參數之間要做一個平衡,比如上面那個ImageLink,很多參數都是交叉的,就不適合重載。當初曾經想把普通的LInk和AjaxLink分開,但發現分開或不分開,都需要textColor之類的參數,所以後來實際的做法是把裡邊的代碼再行分別建立函數,以減少單個函數的長度。3. 虛函數(多態,virtual,override)虛函數的目的,是為了“讓今天的代碼,調用明天的功能”。當年剛剛聽說虛函數有這麼大本事,大吃一驚。如果你正在擔心明天增加了新的功能,還得修改今天的代碼,那麼肯定就需要求助虛函數了。比如下面這個情境:程式員A正在寫一個顯示各種Notice的介面,而設計“各種Notice”的是B,而且他經常增加新的類型。這時候虛函數就可以派上用場。
public class NoticeItemAssigned : Notice { public override MvcHtmlString Information(string user) { var result = base.Information(user).ToString(); result = result.Replace("[P1]", UDCValue.ShowAsUsersLink(P1).ToString()); result = result.Replace("[P2]", UDCValue.ShowAsUsersLink(P2).ToString()); return new MvcHtmlString(result); } }比如上面的NoticeItemAssigned及其他類型的Notice,要顯示它們的Information,都只需要調用@notice.Information(user);但至於顯示出來的結果是什麼,則由其override後的函數來決定。
"為什麼我很少用虛函數?"很可能是用了太多的switch-case所致。如果用switch-case來處理各種notice的顯示結果,那麼就需要通過增加新的分支,來處理新的顯示方法。這個最大的壞處是:剛才說的A和B兩個程式員就要經常交叉工作。而一旦A離職了,B每次增加新的類型,都要去到A的代碼裡邊去改動,極其危險。火星人的11000行代碼中,只有6個switch-case,其中兩個似乎也可以用虛函數避免。
其他的還有很多手段來防止維護代碼影響之前的代碼調用,但這三個是最常用的,可以說在團隊代碼有交叉的時候,每個人都必備上述技能才可能和諧工作。剛開始一般只有師傅具備完全使用上述功能的能力,但隨著徒弟們的能力提升,可以逐步嘗試讓徒弟也掌握這些技能,從而可以協助師傅維護底層庫。