# 原則22:選擇定義并實現接口,而不是基類
**By D.S.Qiu**
**尊重他人的勞動,支持原創,轉載請注明出處:[http://dsqiu.iteye.com](http://dsqiu.iteye.com)**
抽象基類提供的是類繼承結構的公共祖先。接口描述實現類的原子級功能。兩者都更有千秋,卻不盡相同。接口是一種合約式的設計:實現接口的類必須提供所有期望函數的實現。抽象基類提供一組相關類的共有抽象。這是老套的,它是這樣的:繼承是“ is a ”的關系,接口是“ behavies like ”的關系。這些陳詞濫調已經說了很久了,因為它們的結構說明了彼此的不同:基類描述的是對象是什么,接口描述的是對象的表現方式。
接口描述的是一個功能集,更像是一個合約。 你可以在接口里創建任何占位符:方法,屬性,索引器和事件。實現接口的類必須提供接口定義的所有元素的具體實現。你必須實現所有的方法,提供所有屬性的訪問器和索引器和定義所有事件。你確定并提前相同的行為到接口中。你可以使用接口作為參數和返回值。你還可以有更多機會重用代碼因為不相關類可以實現同一個接口。更重要的是,其他開發者實現接口比繼承基類會更容易。
你不能在接口做的是不能提供任何成員變量。接口沒有任何實現,并且它們不能包含任何具體的數據成員。你的類要么全部實現接口的定義的所有元素,要么就沒有實現接口。當然,你可以通過創建擴展方法讓人覺得接口實現的錯覺。System.Linq.Enumerable 類包含對于30個聲明在 IEnumerable<T> 的擴展方法。擴展方法是實現 IEnumerable<T> 類的一劑良藥。你可以查看原則8:
```
public static class Extensions
{
public static void ForAll<T>(
this IEnumerable<T> sequence, Action<T> action)
{
foreach (T item in sequence)
action(item);
}
}
// usage
foo.ForAll((n) => Console.WriteLine(n.ToString()));
```
抽象基類可以提供給子類一些實現,可以描述一些共有的行為。你可以指定數據成員,具體方法,實現虛函數,屬性,事件和索引器。基類可以可以通過方法實現共有可重用功能。任何元素都可以是 virtual , abstract 或 nonvirtual 。抽象基類可以通過具體行為,而接口不行。
實現重用功能還有一個好處:如果你在基類添加方法,所有的子類都自動隱式的加強。在這個意義上,隨著時間的推移,,基類提供了一種高效的方式來擴展幾個類的行為:基類增加和實現功能,子類就立即合并這些行為。在接口中添加添加元素會破壞所有實現這個接口的類。它們沒有包含這個元素的實現就不能通過編譯。每個實現類都要更新以包含這個新元素。
如何選擇抽象基類和接口其實就是一個隨著時間的推移如何更好的支持你的抽象的功能的問題。接口是固定的,你發布的接口就是功能集的一個所有實現類要遵循的合約。基類可以隨著時間推移而擴展。這些擴展會變成每個子類的一部分。
這兩個模型可以混合復用的實現代碼同時支持多個接口。.NET 框架很明顯的例子就是 IEnumerable<T> 接口和 System.Linq.Enumerable 類。System.Linq.Enumerable 類包含大量定義在 System.Collection.Generic.IEnumerable<T> 接口中的擴展方法。這個分離有很重要的意義。任何類實現 IEnumerable<T> 直接就包含這些擴展方法。并且,還有額外一些沒有在 IEnumerable<T> 定義的方法。這意味著開發者沒有必要自己去實現這些方法。
檢查實現 IEnumerable<T> 的天氣觀察類。
```
public enum Direction
{
North,
NorthEast,
East,
SouthEast,
South,
SouthWest,
West,
NorthWest
}
public class WeatherData
{
public double Temperature { get; set; }
public int WindSpeed { get; set; }
public Direction WindDirection { get; set; }
public override string ToString()
{
return string.Format("Temperature = {0}, Wind is {1} mph from the {2}",Temperature, WindSpeed, WindDirection);
}
}
public class WeatherDataStream : IEnumerable<WeatherData>
{
private Random generator = new Random();
public WeatherDataStream(string location)
{
// elided
}
private IEnumerator<WeatherData> getElements()
{
// Real implementation would read from
// a weather station.
for (int i = 0; i < 100; i++)
yield return new WeatherData
{
Temperature = generator.NextDouble() * 90,
WindSpeed = generator.Next(70),
WindDirection = (Direction)generator.Next(7)
};
}
#region IEnumerable<WeatherData> Members
public IEnumerator<WeatherData> GetEnumerator()
{
return getElements();
}
#endregion
#region IEnumerable Members
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return getElements();
}
#endregion
}
```
WeatherStream 類要模擬出一系列的天氣觀察。為了實現這點它實現了 IEnumerable<WetherData> 。這就得實現兩個方法:GetEnumerator<T> 方法和類的 GetEnumerator 方法。后者的顯示實現可以讓客戶端代碼自然把泛型對象向上轉換為 System.Object 。
實現了這兩個方法 WeatherStream 類支持所有在 System.Linq.Enumerable 的擴展方法。這意味著 WeatherStream 可以是 LINQ 查詢的數據源:
var warmDays = from item in new WeatherDataStream("Ann Arbor") where item.Temperature > 80 select item;
LINQ 查詢語法會被編譯成方法調用。上面查詢會被翻譯為下面的調用:
var warmDays2 = new WeatherDataStream("Ann Arbor").Where(item => item.Temperature > 80). Select(item => item);
在上面的代碼,Where 和 Select 的調用看起來覺得它們是 IEnumerable<WeatherData> 的方法。但其實不是。這兩個方法像是屬于 IEnumerable<WeatherData> 因為它們是擴展方法。它們實際是 System.Linq.Enumerable 的靜態方法。編譯器翻譯這些調用為下面的靜態調用:
```
// Don't write this, for explanatory purposes
var warmDays3 = Enumerable.Select(Enumerable.Where( new WeatherDataStream("Ann Arbor"), item => item.Temperature > 80), item => item);
```
最后的這個版本是告訴你接口真的不可以包含實現。你通過使用擴展方法來模擬。 LINQ 就是在 System.Linq.Enumerable 類中創建 IEnumerable<T> 的擴展方法。
這讓我回到使用接口左右參數和返回值的主題。接口可以任意不相關的類實現。接口的編碼比基類的編碼給其他開發者提供了更多的靈活性。這樣非常重要所以 .NET 環境只支持單一繼承關系。
下面三個方法完成的工作是相同的:
```
public static void PrintCollection<T>(IEnumerable<T> collection)
{
foreach (T o in collection)
Console.WriteLine("Collection contains {0}",o.ToString());
}
public static void PrintCollection(System.Collections.IEnumerable collection)
{
foreach (object o in collection)
Console.WriteLine("Collection contains {0}",o.ToString());
}
public static void PrintCollection(WeatherDataStream collection)
{
foreach (object o in collection)
Console.WriteLine("Collection contains {0}",o.ToString());
}
```
第一個方法是可重用的,任何實現 IEnumerable<T> 的類都可以使用這個方法。除了 WeahterDataStream ,List<T> , SortedList<T> , 數組 和 LINQ 查詢的結果都可以使用。第二個方法對很多類也有用,但是只是用在不完美的非泛型 IEnumerable 上。最后的方法是最不能重用的。它不能被數組,ArrayList , DataTable , HashTable , ImageList 和其他集合類使用。在方法編碼上使用接口作為參數類是更加普遍和更加容易重用。
使用接口定義類的 API 會更具靈活性。WeatherDataStream 類可以實現返回 WeatherData 對象的集合的方法。那會是像這樣的:
```
public List<WeatherData> DataSequence
{
get { return sequence; }
}
private List<WeatherData> sequence = new List<WeatherData>();
```
這樣會遺留一個很脆弱的問題。有時,你會將 List<WeatherData> 改為 數組或 SortedList<T> 。任何改變都會破壞之前的代碼。當然,你可以改變參數類型,但也要改變類的 public 接口。改變類的 public 接口會引起大系統的更多改變;你需要在所有訪問 public 屬性的地方進行修改。
第二個問題是更直接和更令人困惑的:List<T> 類提供了改變它所包含的數據的多種方法。這個類的用戶可以刪除,修改甚至替換序列中的每個對象。這些絕大多數都不是你想要的。幸運的是,你可以限制使用這個類的用戶的權限。為了不直接返回內部對象的直接引用,你可以返回接口給你的使用者。這其實就是返回 IEnumerable<WeatherData> 。
當你的類以類暴露屬性,也就暴露了這個類的所有接口。使用接口,你就可以選擇哪些方法和屬性暴露給你的使用者。實現接口的類可以隨著時間推移而改變實現細節。
更重要的是,不相關的類可以實現相同的接口。假設你正在構建一個應用程序管理員工,客戶,和供應商。至少在類的結構是不相關的。但是它們共享了一些共有的功能。它們有名字,它們都要在程序的控制臺展示名字。
```
public class Employee
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string Name
{
get
{
return string.Format("{0}, {1}",LastName, FirstName);
}
}
// other details elided.
}
public class Customer
{
public string Name
{
get
{
return customerName;
}
}
// other details elided
private string customerName;
}
public class Vendor
{
public string Name
{
get
{
return vendorName;
}
}
// other details elided
private string vendorName;
}
```
Employee , Customer 和 Vendor 類都沒有繼承同一個基類。但是他們分享幾個屬性:名字(上面展示的),地址和聯系電話號碼。你可以提取這些屬性到接口中:
```
public interface IContactInfo
{
string Name { get; }
PhoneNumber PrimaryContact { get; }
PhoneNumber Fax { get; }
Address PrimaryAddress { get; }
}
public class Employee : IContactInfo
{
// implementation elided.
}
```
這個接口通過讓你知道構建這些不相關類的共有的任務來簡化你編程的工作:
```
public void PrintMailingLabel(IContactInfo ic)
{
// implementation deleted.
}
```
這是對于實現 IContactInfo 所有實體的例行工作。 Customer , Employee 和 Vendor 有相同的工作——但是只是因為你提取它們到接口中了。
使用接口同時衣蛾意味著你可以讓結構體 省去了拆箱的操作帶來的損耗。當你把結構體放入箱中,這個箱可以實現結構體支持的接口。當你通過接口指針訪問結構體,你不需要進行將結構體拆箱成要訪問的那個對象。例如,想象這個接口定義一個鏈接和一個描述:
```
public struct URLInfo : IComparable<URLInfo>, IComparable
{
private string URL;
private string description;
#region IComparable<URLInfo> Members
public int CompareTo(URLInfo other)
{
return URL.CompareTo(other.URL);
}
#endregion
#region IComparable Members
int IComparable.CompareTo(object obj)
{
if (obj is URLInfo)
{
URLInfo other = (URLInfo)obj;
return CompareTo(other);
}
else
throw new ArgumentException("Compared object is not URLInfo");
}
#endregion
}
```
當你可以很簡單地創建 URLInfo 對象的有序列表因為 URLInfo 實現了 IComparable<T> 和 IComparable 。即使代碼依賴的是類 IComparable 也有更少的 封箱和拆箱次數因為使用者可以調用 IComparable.CompareTo() 而不用對對象進行拆箱操作。
基類描述和或實現相關具體子類的共有行為。接口描述的是不相關的具體類型可以實現的原子功能。都各有千秋。類定義了你創建的類是什么。接口描述實現類的功能行為。一旦你理解這些不同,你就可以創建更高效設計更好面對變化。使用類結構定義相關的類。使用接口暴露實現類的功能。
小結:
這個原則通過一系列的講解告訴接口和類的區別,其實就是說在軟件設計過程中更多關注的是對象能做什么,而不是對象是什么,越多行為的抽象,后期的問題就越少,多使用接口!
歡迎各種不爽,各種噴,寫這個純屬個人愛好,秉持”分享“之德!
有關本書的其他章節翻譯請[點擊查看](/category/297763),轉載請注明出處,尊重原創!
如果您對D.S.Qiu有任何建議或意見可以在文章后面評論,或者發郵件(gd.s.qiu@gmail.com)交流,您的鼓勵和支持是我前進的動力,希望能有更多更好的分享。
轉載請在**文首**注明出處:[http://dsqiu.iteye.com/blog/2083404](/blog/2083404)
更多精彩請關注D.S.Qiu的博客和微博(ID:靜水逐風)
- 第一章 C# 語言習慣
- 原則1:使用 屬性(Poperty)代替可直接訪問的數據成員(Data Member)
- 原則2:偏愛 readonly 而不是 const
- 原則3:選擇 is 或 as 而不是強制類型轉換
- 原則4:使用條件特性(conditional attribute)代替 #if
- 原則5:總是提供 ToString()
- 原則6:理解幾個不同相等概念的關系
- 原則7:明白 GetHashCode() 的陷阱
- 原則8:優先考慮查詢語法(query syntax)而不是循環結構
- 原則9:在你的 API 中避免轉換操作
- 原則10:使用默認參數減少函數的重載
- 原則11:理解小函數的魅力
- 第二章 .NET 資源管理
- 原則12:選擇變量初始化語法(initializer)而不是賦值語句
- 原則13:使用恰當的方式對靜態成員進行初始化
- 原則14:減少重復的初始化邏輯
- 原則15:使用 using 和 try/finally 清理資源
- 原則16:避免創建不需要的對象
- 原則17:實現標準的 Dispose 模式
- 原則17:實現標準的 Dispose 模式
- 原則18:值類型和引用類型的區別
- 原則19:確保0是值類型的一個有效狀態
- 原則20:更傾向于使用不可變原子值類型
- 第三章 用 C# 表達設計
- 原則21:限制你的類型的可見性
- 原則22:選擇定義并實現接口,而不是基類
- 原則23:理解接口方法和虛函數的區別
- 原則24:使用委托來表達回調
- 原則25:實現通知的事件模式
- 原則26:避免返回類的內部對象的引用
- 原則27:總是使你的類型可序列化
- 原則28:創建大粒度的網絡服務 APIs
- 原則29:讓接口支持協變和逆變
- 第四章 和框架一起工作
- 原則30:選擇重載而不是事件處理器
- 原則31:用 IComparable&lt;T&gt; 和 IComparer&lt;T&gt; 實現排序關系
- 原則32:避免 ICloneable
- 原則33:只有基類更新處理才使用 new 修飾符
- 原則34:避免定義在基類的方法的重寫
- 原則35:理解 PLINQ 并行算法的實現
- 原則36:理解 I/O 受限制(Bound)操作 PLINQ 的使用
- 原則37:構造并行算法的異常考量
- 第五章 雜項討論
- 原則38:理解動態(Dynamic)的利與弊
- 原則39:使用動態對泛型類型參數的運行時類型的利用
- 原則40:使用動態接收匿名類型參數