小酌重構系列[11]——提取基類、提取子類、合并子類
概述
繼承是面向對象中的一個概念,在小酌重構系列[7]——使用委派代替繼承這篇文章中,我“父子關系”描述了繼承,這是一種比較片面的說法。后來我又在UML類圖的6大關系,描述了繼承是一種“is a kind of”關系,它更偏向于概念層次,這種解釋更契合繼承的本質。本篇要講的3個重構策略提取基類、提取子類、合并子類都是和繼承相關的,如果大家對繼承的理解已經足夠深刻了,這3個策略用起來應該會得心應手。
提取基類
圖說
初定關系
下圖左定義了Department和Employee類,雖然這兩個類有一些共同的特質——“獲取年度成本”、“獲取名稱”。
在你第一眼看到這張圖時,會覺得Department和Employee會是什么關系?通常情況下,大家可能會覺得Department和Employee是一個聚合關系。
從這兩個類的特質分析,它們具有一些共同點——“獲取年度成本”、“獲取名稱”。
如果希望重用這些共通點,我們該怎么做呢?當然是使用繼承啦。
拿Employee繼承Department嗎?毛線啊,Department和Employee在語義上不存在“is a kind of”關系啊。我們總不能說“Employee is a kind of department”吧。
Well,這條路子走不通,那我們換個概念來理解Department和Employee吧——Party。
Party是個什么玩意兒?在概念上,Party表示一個群體,群體可以是一個部門,也可以是一個人。
造句時刻
前面講了,繼承關系可以用"is a kind of"來描述,現在咱們就用"is a kind of"造幾個句子試試。
"Department is a kind of party."
"Employee is a kind of party."
"Party"概念這樣理解起來是否通順一些了呢?我覺得是通順的。
如果你覺得它不通順,咱們換個姿勢再來造2個句子,用被動式造句。
"Party can be a kind of department."
"Party can be a kind of employee."
"Party"是否也能作為Department和Employee的抽象概念呢?我仍然覺得是(如果您覺得還不是,我也沒轍了)。
確定關系
經過這么一番折騰后,我們終于得以見證以下關系的產生。
注意,我們仍然保留了Department和Employee的聚合關系。
看了這幾幅圖,是否會引起你的思考呢?
一些看似沒有特定關系的事物,可以通過抽象更高層次的概念,讓它們產生其它層面的一些關系。
Department和Employee看似沒有繼承關系,然而通過抽象出"Party"這個概念,讓它Department和Employee產生了另外一層關系——同為Party的子類。
示例
重構前
這段代碼定義了一個Dog類,并定義了兩個方法:EatFood()和Grrom(),分別表示“進食”和“訓練”行為。
public class Dog
{
public void EatFood()
{
// eat some food
}
public void Groom()
{
// perform grooming
}
}
EatFood()和Groom()是大多數動物的公有行為,當追加新的動物class時,也可能會有這兩個行為,我們應考慮將這兩個行為提取到基類。
重構后
public class Animal
{
public void EatFood()
{
// eat some food
}
public void Groom()
{
// perform grooming
}
}
public class Dog : Animal
{
}
小提示:在提取基類時,如果當下只有一個類,你需要根據場景去分析“將來”可能發生的事情,這個例子較好地體現了這一點。
Of course,你可以不必這么早地去提取基類,等到代碼出現2~3次重復以后再去提取也不遲。
提取子類
圖說
有人可能覺得基類中的方法或屬性越多越好,這樣子類只需要很少的代碼就能完成很多的功能。
我們在基類中定義方法或屬性時,一定要事先確定基類的職責。
初回
下圖的Animal類定義了4個方法:Eat()、Run()、Fly()和Swim(),分別表示進食、奔跑、飛翔和浮游行為。
確實所有的動物都要進食,但世界上基本上不存在水陸空三棲動物,你以為這是傳說中的龍嗎?
第2回
現在你添加了一個類Mammal,它繼承了Animal類。
Eat()和Run()是哺乳動物的共通行為,Fly()和Swim()并不是所有哺乳動物都具備的能力。
第3回
再豐富一下咱們的動物世界,把鳥類和魚類也加進來( 哺乳動物終于不那么寂寞了,可以吃海鮮和燒雞了)。
咱們不搞特殊化,從一般化上去認知Mammal、Bird和Fish。
一般來說,Mammal具備Eat()和Run()行為,Bird具備Eat()和Fly()行為,Fish具備Eat()和Swim()行為。
這幅圖如果用代碼來展現,代碼量會非常少。
雖然使用繼承可以減少代碼量,可以一定程度上達到代碼復用的目的,但這不意味著準確性,也不意味著較低的復雜性。
以上內容已經說明了“不準確性”,即子類的行為不那么準確,那是基類強加給子類的。
接著,我們來看看復雜性,上面這幅圖為例,我們從兩個角度來判斷復雜性:
- 基類 + 子類的方法總數:一共16個方法
- 每個子類的方法總數:每個子類都有4個方法
第4回
結合下面這幅圖,我們比較一下這兩種方式的復雜性:
- 基類 + 子類的方法總數:一共7個方法
- 每個子類的方法總數:每個子類都有2個方法
從數量上看,這兩種方式的復雜程度不言而喻。
終回
磨磨唧唧了這么久,用一幅圖來表示“提取子類”這個重構策略吧(綠色表示重構前,紅色表示重構后)。
示例
重構前
Registration類描述了學生選課的場景,學生選課有兩種情況——已注冊的課程和未注冊的課程。
public class Registration
{
public NonRegistrationAction Action { get; set; }
public decimal RegistrationTotal { get; set; }
public string Notes { get; set; }
public string Description { get; set; }
public DateTime RegistrationDate { get; set; }
}
public class NonRegistrationAction
{
}
Registration類的屬性NonRegistrationAction和Notes只有在未注冊的場景才會用到,所以可以考慮將這兩個屬性提取到子類。
重構后
重構時追加了NonRegistration類,這樣職責劃分就很清晰了:Registration類對應已注冊的選課場景,NonRegistration類對應未注冊的選課場景。
public class Registration
{
public decimal RegistrationTotal { get; set; }
public string Description { get; set; }
public DateTime RegistrationDate { get; set; }
}
public class NonRegistration : Registration
{
public NonRegistrationAction Action { get; set; }
public string Notes { get; set; }
}
public class NonRegistrationAction
{
}
小提示:在基類中定義方法和屬性時,請確定它們是否具備“一般性”。
合并子類
這個重構策略比較簡單,咱就不上圖示了,直接用示例來向大家說明吧 ,有點困了
。
示例
重構前
這段代碼定義了兩個類:Website和StudentWebsite,分別表示“網站”和“學生網站”( 難道還有“成人網站”?
) 。StudentWebsite繼承自Website,StudentWebSite擴展了一個IsActive屬性,表示“是否激活”。
public class Website
{
public string Title { get; set; }
public string Description { get; set; }
public IEnumerable<Webpage> Pages { get; set; }
}
public class StudentWebsite : Website
{
public bool IsActive { get; set; }
}
public class Webpage
{
}
重構后
實際上IsActive屬性完全適用于所有的Website實例,所以沒有必要單獨地聲明一個StudentWebsite類,可以將IsActive屬性定義到Website類中,并移除StudentWebSite類(難道我的“成人網站”說沒有就沒有了嗎?)
public class Website
{
public string Title { get; set; }
public string Description { get; set; }
public IEnumerable<Webpage> Pages { get; set; }
public bool IsActive { get; set; }
}
public class Webpage
{
}
小提示:不要盲目地創建子類,也不要盲目地在子類中定義方法和屬性,搞不好基類也要用呢。
本文鏈接: 文章作者:keepfool 文章出處:http://www.rzrgm.cn/keepfool/ 如果您覺得閱讀本文對您有幫助,請點一右下角的“推薦”按鈕,您的“推薦”將是我最大的寫作動力!歡迎看官們轉載,轉載之后請給出作者和原文連接。

繼承是面向對象中的一個概念,在小酌重構系列[7]——使用委派代替繼承這篇文章中,我“父子關系”描述了繼承,這是一種比較片面的說法。后來我又在UML類圖的6大關系,描述了繼承是一種“is a kind of”關系,它更偏向于概念層次,這種解釋更契合繼承的本質。本篇要講的3個重構策略提取基類、提取子類、合并子類都是和繼承相關的,如果大家對繼承的理解已經足夠深刻了,這3個策略用起來應該會得心應手。








浙公網安備 33010602011771號