[VS2010/.NET 4.0 自我修煉-02.02] 使用「類別圖表」設計介面以及抽象類別
本篇要介紹的是使用「類別圖表」來設計介面 (Interface) 以及抽象類別 (Abstract Class)。
首先會對介面跟抽象類別做個簡單的介紹,然後再示範如何利用「類別圖表」來設計。
一、介面跟抽象類別是什麼?
1. 介面 (Interface)
介面,指的是透過介面可以讓不同繼承子樹的類別具有相同特徵,
讓原本沒有繼承關係的類別或結構可以透過介面有某種關連性,
而且實作某介面的類別,就具有該介面所規範的成員,
有某個屬性可以用,或是可以做某件事,所以稱之為介面。
介面內可以有多種成員,但跟類別不同,
介面只包含方法、屬性、事件或索引的簽章 (回傳值、型別以及名稱的宣告),
且前述成員內容的實作 (Implementation) 是在實作此介面的類別或結構中完成,
而且所有成員都必須被實作。
例如我們手上有一個介面 (ICreature):
using System;
namespace ClassLibrary1
{
interface ICreature
{
BloodType BloodType { get; set; }
Guid ID { get; set; }
string Name { get; set; }
/// <summary>
/// 吸收養分
/// </summary>
void AbsorbNutrients();
}
}
而且在某個類別 (Human) 中去實作這個介面:
using System;
namespace ClassLibrary1
{
public class Human : ClassLibrary1.ICreature
{
public BloodType BloodType
{
get
{
throw new NotImplementedException();
}
set
{
throw new NotImplementedException();
}
}
public Guid ID
{
get
{
throw new NotImplementedException();
}
set
{
throw new NotImplementedException();
}
}
public string Name
{
get
{
throw new NotImplementedException();
}
set
{
throw new NotImplementedException();
}
}
public void AbsorbNutrients()
{
throw new NotImplementedException();
}
}
}
我們可以看到在上述類別 Human 中,所有出現的屬性以及方法都是定義在介面中的,
而且所謂的實作會將所有方法內容自動設置為 throw new NotImplementedException(),
這樣就會在執行階段拋出一個例外,提醒設計人員這一部分還沒實作內容,
避免 coding 人員因為找不到剛被實作出來的成員,或是忘記修改那些成員的內容而直接上線。
2. 抽象類別 (Abstract Class)
在抽象類別中的「抽象」兩個字指的是「未完整定義」,
正因為沒有完整的定義,所以我們不能建立他的實體物件,
只能透過繼承之並覆寫其所有的抽象成員來實作出一個具有該型別的類別 (多型)。
跟介面差不多的是,在抽象類別裡面也可以定義數種成員,就跟類別一樣,可以有欄位、方法、事件、屬性。
但是需注意,能夠宣告為 abstract 的成員只有方法、屬性跟事件這三種而已。
例如我們有一個抽象類別如下:
using System;
namespace ClassLibrary1
{
public abstract class MammaliaBase
{
/// <summary>
/// 髮色欄位
/// </summary>
string _HairColor;
/// <summary>
/// 生存事件
/// </summary>
private event EventHandler _OnLiving;
/// <summary>
/// 心臟跳動
/// </summary>
public void HeartBeat()
{
Console.WriteLine("Heart Beating");
}
/// <summary>
/// 叫媽媽
/// </summary>
/// <returns></returns>
public abstract string CallMama();
/// <summary>
/// 髮色屬性
/// </summary>
public string HairColor
{
set { this._HairColor = value; }
get { return this._HairColor; }
}
/// <summary>
/// 生命週期(年)
/// </summary>
public abstract int LifeTimeLong
{
set;
get;
}
/// <summary>
/// 生存事件屬性
/// </summary>
public event EventHandler OnLiving
{
add { this._OnLiving += value; }
remove { this._OnLiving -= value; }
}
/// <summary>
/// 出生事件屬性
/// </summary>
public abstract event EventHandler OnBorned;
}
}
其中欄位的部分是不能有 abstract 這個修飾詞的。
而在方法、事件、屬性的部分,除了 abstract 之外,還可以有一般 (可宣告主體) 的成員。
而繼承抽象類別的類別必須覆寫以上所有的抽象成員,如下:
using System;
namespace ClassLibrary1
{
public class Human : MammaliaBase
{
public override string CallMama()
{
throw new NotImplementedException();
}
public override int LifeTimeLong
{
get
{
throw new NotImplementedException();
}
set
{
throw new NotImplementedException();
}
}
public override event EventHandler OnBorned
{
add
{
//do something here
}
remove
{
//do something here
}
}
}
}
其中覆寫方法以及屬性的內容跟實作介面的時候一樣,預設全部都會是 throw new NotImplementedException(),
這一點的理由跟上面第 1 點是一樣的。
而事件則是預設不做主體宣告,須手動將主體架構出來。
3. 介面與抽象類別的使用時機不同
【表面上看來,介面有點像是完全沒有任何方法被實作的抽象類別,但實際上兩者在語義與應用上是有差別的。
「繼承某抽象類別的類別必定是該類別的一個子類」,由於同屬一個類型,只要父類別中也有定義同名方法,
您就可以透過父類別型態來操作子類實例中被重新定義的方法,也就是透過父類別型態進行多型操作,
但「實作某介面的類別並不被歸屬於哪一類」,一個物件上可以實作多個介面。】
以上這段話是從 良葛格學習筆記 摘錄下來的,雖然這位前輩是走 Java 路線,不過對於介面跟抽象類別的不同處描述得很清楚。
搭配下列的類別圖來看,可以輔助我們理解上述這段話的意思:
在上面這張圖中,我們可以看到有許多類別,根類別是一個抽象類別,另外還有一個介面。
對繼承了抽象類別的第一層類別而言,只有 Human 這個類別實作了 ICreature 介面,另外兩個類別則沒有實作它。
接著來看第一層類別底下的三棵繼承子樹,可以很明顯的看到三棵子樹完全切開,也就是不具有繼承關係,
但在類別 Class4 以及 Class5 的地方可以看到他們各自實作了介面 ICreature,這樣就說明了一件事情,介面跟繼承是無關的~~~
二、設計介面
1. 在「類別圖表」的設計檢視空白處點一下滑鼠右鍵,選擇「加入」,再選擇「介面」
2. 編輯一下介面名稱、存取修飾詞以及檔名 (ICreature),好了之後點一下「確定」按鈕
3. 之後可以看到「類別圖表」裡面出現一個新項目,而且「方案總管」也多了一支 ICreature.cs 檔案
4. 編輯一下類別細節如下:
(此處因為篇幅關係,所以省略太細節的編輯部分,但大致上都跟第一篇「[VS2010/.NET 4.0 自我修煉-01] 初探 Visual Studio 2010 Ultimate 內建的 UML 工具」所介紹的編輯方式一樣~~~)
而在設計檢視中,所有成員內容都是以斜體來呈現,表示所有成員都需被實作
5. 來看看編輯後的類別圖表項目以及介面內容
6. 要實作介面的操作很簡單,假設有一個類別 Class1,要實作介面 ICreature 只要選擇工具箱裡面的「繼承」,
用滑鼠左鍵點一下類別 Class1 (圖 i) 再點一下介面 ICreature (圖 ii) 即可,這樣在類別圖表就可以看到類別 Class1 實作了介面 ICreature (圖 iii),
而且 VS 會自動實作 (這邊的實作不是明確實作) 該介面的所有成員
(i) 點一下類別 Class1
(ii) 點一下介面 ICreature
(iii) 自動實作成員
三、設計抽象類別
1. 一樣在「類別圖表」的設計檢視空白處點一下滑鼠右鍵,選擇「加入」,再選擇「抽象類別」
2. 編輯一下抽象類別名稱、存取修飾詞以及檔名 (MammaliaBase),好了之後點一下「確定」按鈕
3. 之後可以看到「類別圖表」裡面出現一個新項目,而且「方案總管」也多了一支 MammaliaBase.cs 檔案
4. 編輯一下類別細節如下:
(此處也因為篇幅關係,所以省略太細節的編輯部分,但大致上也都跟第一篇「[VS2010/.NET 4.0 自我修煉-01] 初探 Visual Studio 2010 Ultimate 內建的 UML 工具」所介紹的編輯方式一樣~~~)
特別的是在設計檢視中可以看到,有幾個成員的內容是以斜體來呈現,以表示這些成員具有 abstract 修飾詞
亦即必需被實作
5. 來看看編輯後的類別圖表項目以及抽象類別內容
6. 繼承抽象類別的操作跟介面一樣,假設有一個類別 Class2,
要繼承抽象類別 MammaliaBase 只要選擇工具箱裡面的「繼承」,
用滑鼠左鍵點一下類別 Class2 (圖 i) 再點一下抽象類別 MammaliaBase (圖 ii) 即可,
這樣在類別圖表就可以看到類別 Class2 繼承了抽象類別 MammaliaBase (圖 iii),
而且 VS 會自動實作該抽象類別的所有抽象成員
(i) 點一下類別 Class2
(ii) 點一下抽象類別 MammaliaBase
(iii) 自動實作抽象成員
四、結論
繼前兩篇跟廢話沒兩樣的結論之後,筆者這次認真的想了一下~~~ (毆)
其實介面跟抽象類別真的很像,重點就在於使用時機,還有所謂的繼承跟需實作成員的分配。
1. 時機的重點在於「是不是需要繼承」的判斷!
例如在做系統分析的階段,發現有好幾個不同繼承樹體系的類別都需要某些特徵跟方法,
但因為是不同體系,所以沒辦法透過繼承來讓他們具有上述條件,
這個時候就是介面出動的最佳時機。
2. 實作成員的分配跟有無繼承比較有關係,因為要判斷出哪些成員適合放在介面,哪些適合放在抽象類別,
例如本篇中的介面 ICreature 裡的 AbsorbNutrients 方法,是指「吸收養分」,這一點只要是生物都需要;
而抽象類別 MammaliaBase 裡的 CallMama 方法,是指「叫媽媽」,這一點則不一定所有生物都需要。
這樣就可以決定哪些要放在介面,哪些要放在抽象類別。
以前有位同事告訴筆者,「不管你做什麼,只要你覺得合理,通常都是對的。」
比較深入一點的說法是,「不管你怎麼設計,只要合乎自然界法則的,通常都是對的。」
筆者希望無論做什麼設計,都會是合乎常理的~~~ 以上這點跟各位共勉。