為什么要使用接口?而不直接使用類呢?
目錄
2 為什么要使用接口?而不是直接實現呢?
2.1 面向接口編程
2.2 軟件設計中有關接口的原則
2.3 我所遇到的使用接口的場景
3 接口和抽象類有什么不同?
3.1 相同點
3.2 區別
4 使用接口還是抽象類?
4.1 IS A VS CAN-DO 關系
4.2 易於使用
4.3 版本控制
5 C#中的接口
5.1 接口隱式方法實現
5.2 接口的實現加virtual的情況
5.3 接口的顯示方法實現
5.4 泛型接口
5.5 泛型和接口約束
6 參考資料
1 什么是接口
- 接口是一種用來定義程序的協議,它描述可屬於任何類或結構的一組相關行為。
接口是一組規則的集合,它規定了實現本接口的類或接口必須擁有的一組規則。體現了自然界“如果你是……則必須能……”的理念。
接口是在一定粒度視圖上同類事物的抽象表示。因為“同類事物”這個概念是相對的,它因為粒度視圖不同而不同。
2 為什么要使用接口?而不是直接實現呢?
接口的使用並非總是從設計的角度來考慮。接口和C#其他語法現象一樣,共同構成了C#整個語言體系。
接口的意義在於 抽象、不拘細節,從而使同類事物在同一高度具有通用及可替代性。
關於解耦,並不是接口能解耦,而是抽象能解耦 接口只是手段,如果兩個事物有必然聯系,那么就不會出現完全解耦,只能耦合轉移。
—— from http://bbs.csdn.net/topics/380040137
在系統分析和架構中,分清層次和依賴關系,每個層次不是直接向其上層提供服務(即不是直接實例化在上層中),而是通過定義一組接口,僅向上層暴露其接口功能,上層對於下層僅僅是接口依賴,而不依賴具體類。
- 系統靈活性增強
當下層需要改變時,只要接口及接口功能不變,則上層不用做任何修改。甚至可以在不改動上層代碼時將下層整個替換掉,就像我們將一個WD的60G硬盤換成一個希捷的160G的硬盤,計算機其他地方不用做任何改動,而是把原硬盤拔下來、新硬盤插上就行了,因為計算機其他部分不依賴具體硬盤,而只依賴一個IDE接口,只要硬盤實現了這個接口,就可以替換上去。
- 不同部件或層次的開發人員可以並行開工
就像造硬盤的不用等造CPU的,也不用等造顯示器的,只要接口一致,設計合理,完全可以並行進行開發,從而提高效率。
那么具體什么時候用,什么時候不用呢?在常見的三層架構中,有以下幾個層次,分別進行說明:
- 界面層
也就是展示層,直接呈現給用戶的,可能不同的軟件有不同的呈現方式,比如Web,WinForm,甚至移動APP,在這個層次,我認為是沒有必要寫太多的接口。
- 業務邏輯層
這個層次,業務邏輯,可以根據需要使用接口。如果是直接讀寫數據庫什么的,就直接用調用數據庫訪問層的接口。如果是與多個第三方接口進行交互,那么就需要接口,不同的渠道各自實現。
- 數據訪問層
數據訪問層,最好使用接口,比如數據庫訪問,這種可以根據不同的數據庫實現相應的接口向業務邏輯層提供服務。
可能在開發的時候,一開始我們並沒有想到要使用接口。可能簡單就用一個類實現了。到后面新的需求過來的時候,發現代碼需要重構,要用接口和抽象類等等。這個也需要看個人編碼的習慣。有的人就長篇大論一個類完成所有的邏輯。這樣的開發人員,應該是很少見過好的代碼,如果見過的話,后面肯定會精簡做到更好。而另外一些人可能一開始就能嗅出來哪些地方需要使用接口,哪些地方使用抽象類,這也是一種思維方式。前面一種只管開發當前的功能。而后面一種則會考慮到以后的擴展。總而言之,需要根據不同的情況進行考慮。
2.1 面向接口編程
面向接口編程:面向接口編程和面向對象編程並不是平級的,它並不是比面向對象編程更先進的一種獨立的編程思想,而是附屬於面向對象思想體系,屬於其一部分。或者說,它是面向對象編程體系中的思想精髓之一.
2.2 軟件設計中有關接口的原則
我一直認為這個問題,應該從設計的角度來講。在軟件設計的六大設計原則中,與接口直接相關的就有以下兩個:
- 依賴倒置原則
高層模塊不應該依賴底層模塊,二則都應該依賴其抽象,抽象不應該依賴細節;細節應該依賴抽象。
問題由來:類A直接依賴類B,假如要將類A改為依賴類C,則必須通過修改類A的代碼來達成。這種場景下,類A一般是高層模塊,負責復雜的業務邏輯;類B和類C是低層模塊,負責基本的原子操作;假如修改類A,會給程序帶來不必要的風險。
解決方案:面向接口編程,將類A修改為依賴接口I,類B和類C各自實現接口I,類A通過接口I間接與類B或者類C發生聯系,則會大大降低修改類A的幾率。
- 接口隔離原則
定義:客戶端不應該依賴它不需要的接口;一個類對另一個類的依賴應該建立在最小的接口上。
問題由來:類A通過接口I依賴類B,類C通過接口I依賴類D,如果接口I對於類A和類B來說不是最小接口,則類B和類D必須去實現他們不需要的方法。
解決方案:在設計接口的時候要精簡單一,將臃腫的接口I拆分為獨立的幾個接口,類A和類C分別與他們需要的接口建立依賴關系。也就是采用接口隔離原則。
- 總結
單一職責原則告訴我們實現類要職責單一;里氏替換原則告訴我們不要破壞繼承體系;依賴倒置原則告訴我們要面向接口編程;接口隔離原則告訴我們在設計接口的時候要精簡單一;迪米特法則告訴我們要降低耦合。而開閉原則是總綱,他告訴我們要對擴展開放,對修改關閉。接口在設計模式中,有很多的靈活應用。
2.3 我所遇到的使用接口的場景
- WCF服務的契約就是接口
- 數據庫訪問層
定義數據庫訪問層的接口,然后不同的數據庫類型(MySQL/SQL Server)實現不同的接口,向業務層提供服務。這樣如果說從SQL Server數據庫遷移到MySQL數據庫,業務層幾乎不需要怎么改動,直接用MySQL的進行訪問就可以了。 在數據訪問接口層的參數通常都是IDbConnection這樣的接口,而不是具體的類。體現了,依賴倒置原則。
3 接口和抽象類有什么不同?
這個主要是以C#語言為基礎來講的。
3.1 相同點
- 都可以被繼承
- 都不能被實例化
- 都可以包含方法聲明
- 派生類必須實現未實現的方法
3.2 區別
- 語法上
接口 | 抽象類 |
---|---|
接口只能定義屬性、索引器、事件、和方法聲明,沒有普通成員變量 | 抽象類沒有此限制 |
接口不能有構造方法(這簡直是廢話) | 抽象類可以有構造方法 |
接口中的所有方法必須都是抽象的 | 抽象類中可以包含非抽象的普通方法 |
接口中的方法只能是public類型的(默認) | 抽象類中的抽象方法的訪問類型可以是public,protected |
接口可以用於支持回調 | 而繼承並不具備這個特點 |
實現接口的類中的接口方法卻默認為非虛的,(實現類的派生類,不可以再重寫實現類接口方法,但派生類可以再顯示實現接口的方法) 實現接口的類中的接口方法可以聲明為virtual(這樣實現類的派生類還可以重寫該方法). |
抽象類實現的具體方法默認為虛的 |
- 其他方面
接口 | 抽象類 |
---|---|
接口是一個行為規范 | 抽象類是一個不完整的類,需要進一步細化 |
接口可以被多重實現 | 抽象類只能被單一繼承 |
接口大多數是關系疏松但都實現某一功能的類中 | 抽象類更多的是定義在一系列緊密相關的類間 |
接口是為了滿足外部調用而定義的一個功能約定, 因此反映的是事物的外部特性 | 抽象類是從一系列相關對象中抽象出來的概念, 因此反映的是事物的內部共性 |
4 使用接口還是抽象類?
4.1 IS A VS CAN-DO 關系
- IS A 關系用抽象類
一般到具體的這樣一個關系,就用抽象。
邏輯相關,並且有相同的功能的可以使用抽象類,不用每個接口都去寫。
- CAN DO 關系用接口,值類型用接口
接口是對同類事物的橫切面的一個抽象。體現能的邏輯關系。在設計接口的時候,依據接口隔離原則,接口的方法都是必須的,最少的。實現類如果要使用一個接口,那么它必須實現接口的所有方法。即全都能。
- 接口和抽象類可以同時使用。
兩件事情實際上可以同時做:定義一個接口,同時提供一個實現了這個接口的基類。
4.2 易於使用
- 定義一個從基類派生的新類型通常比實現一個接口的所有方法容易得多。基類型可以提供大量功能,所以派生類可能只需要針對其行為稍作改動。
- 使用接口,則新類型必須實現所有的成員。
4.3 版本控制
- 向基類添加一個方法,派生類型將繼承新方法。一開始使用的就是一個能正常工作的類型。用戶的源代碼甚至不需要重新編譯。
- 向接口添加一個方法,會強迫接口的繼承者更改其源代碼並重新編譯
5 C#中的接口
下面看幾個C#接口的例子及實現。其實單純的為了應用接口而應用接口是沒什么意思的,我的理解,接口一定是跟軟件設計相關,它是比面向對象更高一個層次,下面這些例子,第一個例子很常見,我們平常就是這么用的。第二個例子不是特別常見,因為接口的實現類,一般不會允許再可以有派生類,一般來講,都會是直接實現。第三個例子,只是看看我們接口復雜的用法,相信工作中很少會遇到這樣寫得。但如果這樣寫一定有這樣寫得道理。
5.1 接口隱式方法實現
代碼如下:
/// <summary> /// IMessage /// </summary> interface IMessage { void ShowMessage(); }
/// <summary> /// ConsoleMessage /// </summary> class ConsoleMessage : IMessage { #region IMessage 成員 /// <summary> /// ShowMessage /// </summary> public void ShowMessage() { Console.WriteLine("ConsoleMessage.ShowMessage()"); } #endregion }
測試代碼如下:
/// <summary> /// Main /// </summary> /// <param name="args">args</param> static void Main(string[] args) { // 1、不加virtual實現接口 ConsoleMessage consoleMsg = new ConsoleMessage(); // 結果:ConsoleMessage.ShowMessage() ((IMessage)consoleMsg).ShowMessage(); // 結果:ConsoleMessage.ShowMessage() consoleMsg.ShowMessage(); }
這是最常用的情況。一個接口,一個實現類。可以通過接口調用方法, 也可以通過實現類調用方法。
5.2 接口的實現加virtual的情況
代碼如下:
/// <summary> /// IMessage /// </summary> interface IMessage { void ShowMessage(); } /// <summary> /// VirtualMessage /// </summary> class VirtualMessage : IMessage { /// <summary> /// ShowMessage /// </summary> public virtual void ShowMessage() { Console.WriteLine("VirtualMessage.ShowMessage()"); } } /// <summary> /// ExtendVirtualMessage /// </summary> class ExtendVirtualMessage : VirtualMessage { /// <summary> /// ShowMessage /// </summary> public override void ShowMessage() { Console.WriteLine("ExtendVirtualMessage.ShowMessage()"); } }
測試代碼:
/// <summary> /// Main /// </summary> /// <param name="args">args</param> static void Main(string[] args) { ///3、重寫VirtualMessage的ShowMessage接口 ExtendVirtualMessage extendVirtualMsg = new ExtendVirtualMessage(); // ExtendVirtualMessage.ShowMessage() ((IMessage)extendVirtualMsg).ShowMessage(); // ExtendVirtualMessage.ShowMessage() ((VirtualMessage)extendVirtualMsg).ShowMessage(); // ExtendVirtualMessage.ShowMessage() extendVirtualMsg.ShowMessage(); }
實現類的派生類重寫接口方法,可用接口,實現類,實現類的派生類去調用方法,但結果都是一致的。
5.3 接口的顯示方法實現
顯示接口方法實現的定義:將定義方法的那個接口的名稱作為方法名前綴(例如IDisposable.Dispose),就會創建顯式接口方法實現。注意C#不允許在顯式接口方法指定可訪問性(比如public或者private)。但是編譯器生成方法的元數據時,可訪問性會自動設為private
代碼如下:
/// <summary> /// IMessage /// </summary> interface IMessage { void ShowMessage(); } /// <summary> /// ConsoleMessage /// </summary> class ConsoleMessage : IMessage { #region IMessage 成員 /// <summary> /// ShowMessage /// </summary> public void ShowMessage() { Console.WriteLine("ConsoleMessage.ShowMessage()"); } #endregion } /// <summary> /// EIMIMessage /// </summary> class EIMIMessage : ConsoleMessage, IMessage { /// <summary> /// ShowMessage /// </summary> public new void ShowMessage() { Console.WriteLine("EIMIMessage.new ShowMessage()"); } #region IMessage 成員 /// <summary> /// ShowMessage /// </summary> void IMessage.ShowMessage() { Console.WriteLine("EIMIMessage.IMessage.ShowMessage()"); } #endregion }
測試結果如下:
/// <summary> /// Main /// </summary> /// <param name="args">args</param> static void Main(string[] args) { // 4、顯示實現接口等綜合類 EIMIMessage eimiMsg = new EIMIMessage(); // EIMIMessage.IMessage.ShowMessage() ((IMessage)eimiMsg).ShowMessage(); // ConsoleMessage.ShowMessage() ((ConsoleMessage)eimiMsg).ShowMessage(); // EIMIMessage.new ShowMessage() eimiMsg.ShowMessage(); Console.Read(); }
1、EIMIMessage 實現IMessage接口,顯示實現接口方法ShowMessage,顯示實現的接口方法,只能通過接口去調用。所以結果是:EIMIMessage.IMessage.ShowMessage()
2、EIMIMessage 的基類ConsoleMessage,如果轉換成:ConsoleMessage,則調用的就是ConsoleMessage類實現的方法。
3、EIMIMessage 類繼承ConsoleMessage,是不能夠繼承ConsoleMessage.ShowMessage的方法,只能通過new 關鍵字重新寫一個方法。因此用EIMIMessage 的對象去調用,則是重寫的這個類。
謹慎使用顯示接口方法實現
- 沒有文檔解釋類型具體是如何實現一個EIMI方法,也沒有Microsoft Visual Studio“智能感知”支持
- 值類型的實例在轉換成接口時裝箱
- EIMI不能由派生類型調用
5.4 泛型接口
泛型接口的優點:
- 泛型接口提供了出色的編譯時類型安全性
有的接口比如(非泛型Icomparable接口)定義的方法使用了Object參數或Object返回類型。在代碼中調用這些接口方法時,可以傳遞對任何類型的實例的引用。但這通常不是我們期望的
- 處理值類型的時候,裝箱次數會減少
static void TestInterface() { Int32 x = 1; Int32 y = 2; // x轉換為接口類型本身是要裝箱的 IComparable<Int32> c = x; // CompareTo方法本來就接受int類型,所以 y不需要裝箱 c.CompareTo(y); // 類型安全,編譯不通過 c.CompareTo("2"); }
- 類可以實現一個接口若干次
如同時實現Int32的IComparable和string的IComparable
5.5 泛型和接口約束
- 可以將泛型約束為多個接口
public static class SomeType { private static void Test() { Int32 x = 5; Guid g = new Guid(); // 對M調用能通過編譯,因為Int32實現了IComparable和IConvertible M(x); // 編譯時錯誤,因為Guid只實現了IComparable,沒有實現IConvertible M(g); } private static Int32 M<T>(T t) where T : IComparable, IConvertible { Console.Write(t); return 0; } }
- 傳遞值類型減少裝箱次數
C#編譯器為接口約束生成特殊IL指令,導致直接在值類型上調用接口的方法而不裝箱。不用接口約束便其他方法讓C#編譯器生成這些IL指令。 如果值類型實現了一個接口方法,在值類型的實例上調用這個方法不會造成值類型的實例裝箱。
6 參考資料
- 抽象類和接口的區別,使用場景
- 為什么要用接口(從編程角度)
- 《CLR via C#》