概述
子類可以繼承父類的字段、屬性和方法,使用“繼承”可以較大程度地復用代碼。
在使用繼承時,務必要確定代碼中定義的“父類”和“子類”確實存在客觀的“父子關系”,而不要去做“為了代碼復用而使用繼承”的事情,這是舍本逐末的做法,也是濫用繼承的體現。
濫用繼承會破壞類之間客觀存在的關系,也會模糊代碼所體現的語義。
使用委派代替繼承
繼承的誤區
當多個類具有相似的屬性、方法時,使其中一個類變成基類,其他的類去繼承該基類實現代碼復用。
當將應用場景中的某一個類提升為基類時,需要慎重考慮這個類是否確實和其他類存在“父子關系”,如果不存在“父子關系”,則不構成繼承關系。
如何確定類之間的繼承關系?
應該從現實的業務和語義去理解(這是一句無關痛癢的廢話)。
舉個不那么恰當的例子,雖然大頭兒子和隔壁王叔叔長得實在是太像了,但是在法律上,大頭兒子繼承小頭爸爸是合法的,而不是去繼承隔壁的王叔叔。
(PS:我也不知道大頭兒子是不是小頭爸爸親生的)
繼承的濫用
下圖是Java 中的Vector和Stack類,為了讓IsEmpty()方法得以復用,Stack繼承了Vector。
但Vector和Stack是不同的數據結構,二者沒有明顯的“父子關系”,這是典型的濫用繼承的做法。
委派代替繼承
現在問題來了,當兩個類確實不存在繼承關系,並且一個類依賴於另外一個類的方法時,應該如何構建這2個類的關系?
這就是本文要說明的重構策略——使用委派代替類繼承
繼承是一種強關聯的關系,而委派是一種弱關聯的關系。
示例
重構前
這段代碼定義了Sanitation類和Child類和類,用於描述“小孩洗手”這件事情。
在這件事情中,Sanitation類表示衛生設施,比如水龍頭和洗手液,可供人們洗手,當然也可供小孩洗手;Child表示小孩,他是利用這些衛生設施去洗手。
Sanitation類提供了WashHands()方法,並讓Child繼承Sanitation。
public class Sanitation { public string WashHands() { return "Cleaned!"; } } public class Child : Sanitation { }
從語義上分析:
2. 小孩不是從衛生設施里面蹦出來的,而是誕生自父母的受精卵,二者之間本身是一個間接關系。
二者之間僅存在“利用”關系,不存在“繼承”關系。為了體現“利用”語義,我們應該使用委派。
重構后
將這段代碼進行如下調整:
2. 在Child類中定義Sanitation屬性。
3. 在Child類中定義WashHands()方法,並調用Sanitation屬性的WashHand()方法。
public class Sanitation { public string WashHands() { return "Cleaned!"; } } public class Child { private Sanitation Sanitation { get; set; } public Child() { Sanitation = new Sanitation(); } public string WashHands() { return Sanitation.WashHands(); } }
大家可能會對重構后的代碼產生疑問,代碼量比之前多了,Child還需要依賴於Sanitation類,這能帶來什么好處?
僅從代碼角度去看,這確實如你所想,它確實沒有帶來什么好處。
但我覺得代碼應該能夠體現客觀事實和語義——“小孩利用衛生設施去洗手”,而不是“小孩因為繼承了衛生設施,才具備洗手的行為”。
我個人一直比較提倡一個觀點——代碼層面的所見即所得,當我們看到一段代碼時,就知道這段代碼能做什么事情,不需要過多的修辭和注釋,不多不少,恰如其分。用一個詞概括,就是“言出法隨”。
就好比這篇文章:寫了10年的代碼,我最怕寫這段代碼中的一些“干貨”(沒錯,我確實不認為這是什么“干貨)。
“判斷字符串是否為Email”,本來就是一件很簡單的事情,為何不能直截了當地去描述呢?
public static bool Email(this String str) { bool result = false; if (!string.IsNullOrEmpty(str) && System.Text.RegularExpressions.Regex.IsMatch(str, @"^([0-9a-zA-Z]+[-._+&])*[0-9a-zA-Z]+@([-0-9a-zA-Z]+[.])+[a-zA-Z]{2,6}$")) { result = true; } else { result = false; } return result; }
“判斷字符串是否為Email”這件事情用一句話就能說清楚它的語義和邏輯——“字符串不為空,且匹配Email正則表達式”。
public static bool IsEmail(this String str) { string emailPattern = @"^([0-9a-zA-Z]+[-._+&])*[0-9a-zA-Z]+@([-0-9a-zA-Z]+[.])+[a-zA-Z]{2,6}$"; return !string.IsNullOrEmpty(str) && Regex.IsMatch(str, emailPattern); }
這段代碼沒有用到語法糖,也不是什么裝x行為,我也不是為了體現2行代碼就一定比10行代碼好。