面向對象--接口與抽象類、對象和接口(轉)


原文:http://blog.sina.com.cn/s/blog_7d69dc150102w8cw.html

抽象類與接口的區別及應用

  抽象類(Abstract Class)與接口(Interface)是面向對象程序設計中兩個重要的概念。由於兩者在自身特性及應用方法上存在諸多相似性,如都不能實例化、都可以被繼承(嚴格來說對於接口應該叫做實現),這么一來,在許多人心中抽象類與接口的界限非常模糊,對何時該使用抽象類、何時該使用接口更是感到困惑。

本文的目的是通過對兩者的討論與比較,幫助讀者認清抽象類與接口在思想本質及應用場合方面的區別,如能做到這一點,讀者便可以得心應手地根據具體情況正確選擇和使用抽象類與接口。

1. 抽象類與接口是面向對象思想層面概念,不是程序設計語言層面概念

  如若想正確認識抽象類與接口,首先要弄清楚的一點是,這兩個概念均屬於面向對象思想層面,而不屬於某種程序設計語言。例如,C#中用interface關鍵字聲明的語言元素,我們叫它“接口”,其實這是不准確的,准確來說,這應該叫做“接口在C#語言中的實現機制”。

面向對象思想包含許多概念,而不同面向對象語言對這些概念的具體實現機制各有不同。例如,C++中並沒有一種關鍵字對應於C#中的interface,那么C++中就沒有接口的概念了嗎?非也!在C++中,如果想定義一個接口,可以通過將一個類中所有方法定義為純虛方法[①]來做到。

這里可以看到,同樣是接口,C#中用interface關鍵字來定義,而C++通過創建一個只包含純虛方法的類來定義,這就是同一種概念在不同具體語言中具有不同的實現機制。類似的,C++中也沒有abstract關鍵字用於定義抽象類,而是如果一個類中至少含有一個純虛方法且它的方法不全為純虛方法,則這個類被稱為抽象類。

通過上面的分析可以看出,如果僅僅停留在語言層面去認知抽象類與接口,是無法准確理解兩者的真諦的,因為不同語言對同一概念的實現機制有很大差別。如果一個C#初學者簡單將兩者理解為“用abstract修飾的類是抽象類,用interface定義的語言元素是接口”,那么當他接觸C++時一定會感到困惑,因為C++里既沒有abstract也沒有interface,而是通過類中純虛方法的情況確定這是個類、是個抽象類還是個接口。

明確了上面的問題,我們就可以給出抽象類與接口的真正定義了。

抽象類是不能實例化的類,但是其中的方法可以包含具體實現代碼。

接口是一組方法聲明的集合,其中應僅包含方法的聲明,不能有任何實現代碼。

以上對抽象類和接口的定義與任何具體語言無關,而是從面向對象思想角度進行的定義,不同語言可以有不同的實現機制。

從上面的定義中,我們可以發現兩者在思想層面上的一項重大區別:抽象類是類(Class),接口是集合(Set,兩者從本質上不是一種東西。這是我們總結出的第一個區別。請讀者受累將上面加粗的字能放聲朗十遍,聲音越大越好,但是如果被室友或鄰居扔雞蛋請不要找我。

2. 抽象類是本體的抽象,接口是行為的抽象

  在開始這一節之前,我想先請問各位一個問題,“我是一個人”和“我能呼吸”分別表達了“我”和“人”以及“我”和“呼吸”的關系,那么這兩句話表達的是一種關系嗎?如果你能很容易區分前者表示“是一個”的關系,而后者表示“能”的關系,那么恭喜你,你一定也能很容易區分抽象類和接口。

在閱讀這一節時,請讀者務必謹記上面這個問題以及下面這句話:

抽象類表示是一個(IS-A關系的抽象,接口表示能(CAN-DO關系的抽象。

請照例將上面的大聲話朗讀十遍。

好的,請各位擦干凈頭上的雞蛋,我們繼續。

從上面粗體字中我們可以看出,抽象類和接口有一個共性——它們都是“某種關系的抽象”,只不過類型不同罷了。其實如果將上面那句話的前半句中的“抽象類”改為“類”也是正確的,這並不奇怪,上文我們說過,抽象類只不過是一種特殊的類罷了。

下面我們先來解釋IS-A關系。其實英語中的IS-A關系在漢語中可以解釋為兩種情況,當IS-A用在一個對象和一個類之間時,意思是“這個對象是類的一個實例”,例如關羽是一個對象,我們可以說“GuanYu IS-A General”,其中General(將軍)是個類,這表示關羽是將軍類的一個實例。而當IS-A用在兩個類之間時,我認為叫做IS-A-KIND-OF更為准確,表示漢語中的“是一種”,如“General IS-A Person”,表示將軍這個類是人這個類的一種,換用面向對象術語可以如下表述:General是Person的子類(Sub Type),Person是General的父類或超類(Super Type),General繼承自Person。

這后一種IS-A關系,就是抽象類所表達的關系。分析到這里可以看出,抽象類所表達的關系其實就是面向對象三大特性之一——繼承(Inheritance),也就是說,抽象類所表達的關系,與一般類與類之間的繼承並無區別,而抽象類相比普通類,除了不能實例化外,也並無區別。之所以出現抽象類,是因為在較高抽象層次上,某些方法(往往是純虛方法)無法實現,必須由其子類按照各自不同的情況具體實現。因為它含有純虛方法,所以將這種類實例化在道理上講不通,但我們又希望將這些子類中共有的部分抽象出來減少代碼重復,於是就有了抽象類——它包含可復用部分,但又不允許實例化。

因此,抽象類的使用動機是在不允許實例化的限制下復用代碼。請牢記這個動機。

接着再說說接口和CAN-DO關系。

我們知道,面向對象編程的基本思想就是通過對象間的相互協作,完成程序的功能。具體來說,在面向對象編程中,要求每個類都隱藏內部細節(這叫封裝性),僅對外暴露一組公共方法,對象間就通過互相調用彼此的公共方法完成程序功能。

可以看到,面向對象思想中,對象和對象間根本不需要了解,調用者甚至可以完全不知道被調用者是誰,只要知道被調用者“能干什么”就行了。這就如同撥打110報警一樣,你根本不知道對方長什么樣、穿什么衣服、結沒結婚、有沒有孩子,你也不知道對方在哪,對象是誰,但是你知道對方一定“能接警”,所以你可以順利完成報警。

這種“能干什么”就是CAN-DO關系,當我們把這種CAN-DO關系抽象出來,形成一個CAN-DO關系的集合,這就是接口了。那么使用接口的動機又是什么呢?動機之一是松散耦合。我們知道“低耦合”是面向對象程序設計中一個重要原則,而很大一部分耦合就是調用關系,面向對象中術語叫“依賴”。如果沒有接口,調用者就要緊依賴於被調用者,就如同在沒有110報警的年代,你只認識一個接警員,不知道其他接警員的電話,那么當你報警時,你必須給這個接警員打電話才行,如果哪天這個接警員休假或病了,你就無法報警了,除非你再去認識一個接警員。這時,我們說你緊依賴於這個接警員,也叫緊耦合。但有了110報警后就不一樣了,我們將“可接警”看作一個接口,接口中有一個方法“接警”,而撥通110后,電話那頭的人一定是實現了這個接口的,這時報警人不再依賴於具體接警員,而是依賴於“可接警”接口,這就叫做松依賴。

所以說,接口又可以看作一組規則的集合,它是對調用者的保證,對被調用者的約束。如上例中,可接警對報警人(調用者)保證調用對象可接警,同時約束接警部門必須把一個實現了這個接口的人安排在接警電話前面。哪怕這是個機器人或剛進行了兩個小時接警培訓的保潔員都沒關系。

使用接口的另一個動機就是實現多態性[②]

下面想象你被分配到一個全新的研發小組做主管,第一天上班的早晨,一群人站在你面前等着你訓話,你完全不認識他們,也不知道他們各自的職務,但是你可以說一句“都去工作吧”,於是大家作鳥獸散,程序員去寫程序,會計去核對賬目,業務員出門聯系客戶……當你這樣做的時候,你就利用接口實現了多態性。因為你知道,他們都實現了“可工作”這個接口,雖然各個人員對“工作”具體的實現不一樣,但這不要緊,你只要調用他們的“工作”方法,他們就各自做自己的事情了。如果你不能面向接口去利用多態性,你就要一個個說:“程序員去寫程序,會計去核賬,業務員快出門聯系客戶……”,這實在非常的費勁。

對這一節的內容做一個總結:

抽象類表示是一個(IS-A關系的抽象,它抽象了類的本體,其使用動機是在不允許實例化的限制下復用代碼。接口表示能(CAN-DO關系的抽象,它抽象了類的行為,其使用動機是松散對象間的耦合以及實現程序多態性。

好的,照例念十遍吧,不過這次我允許你默念,因為我怕這次飛來的不是雞蛋而是磚頭。

經過上面的分析,我想你已經可以很容易在抽象類與接口間做出選擇了。如果你是為了將一系列類的公共代碼抽出,減少代碼的重復,並且這些類與抽象出來的類可以表述為IS-A關系,就用抽象類;如果你是為了將一個或一組行為抽象出來,用以松散對象間耦合或實現多態性,那就用接口吧。

3. C#中抽象類與接口的探討

這一節我們討論C#語言中一個是人盡皆知的區別:C#中,一個類最多只能繼承一個抽象類,但可以實現多個接口。

如果能充分理解抽象類對應於IS-A而接口對應於CAN-DO,則對這個約束不會感到奇怪。因為從邏輯上來說,一個類在所有相同抽象層次的類中只能“是其中一個”,但“能干多種事情”。這里的相同抽象層次指互相不存在繼承關系的一個全集。

例如,{豬,牛,狗,貓} 可以看作具有相同抽象層次,其某個下層類只能是其中一個的子類,一個類不可能既是牛的子類又是豬的子類,但有可能既是牛的子類又是動物的子類,例如奶牛,這是因為“動物”與“牛”不在一個抽象層次上,“牛”本身就是“動物”的一個子類。

一般的,如果ClassAClassB的子類,同時也是ClassC的子類,那么一定存在ClassBClassC的子類或ClassCClassB的子類。

換句話說,一個類同時繼承兩個互相沒有繼承關系的類在邏輯上是不成立的。這就說明了為什么C#中不允許同時繼承一個以上的抽象類。如果一個類要繼承兩個抽象類,那么從邏輯上來說,兩個抽象類之間必然也存在繼承關系,因此只需讓該類繼承較具體的那個抽象類即可。例如,本來的設計為“奶牛”同時繼承“牛”和“動物”,但很容易發現,“牛”和“動物”已經存在繼承關系,“牛”是繼承於“動物”的,因此可將繼承關系修改為“奶牛”只繼承“牛”,而讓“牛”繼承於“動物”,這樣就消除了多重繼承。

而接口的CAN-DO關系在邏輯上不存在這樣的矛盾,所以C#允許實現多個接口,具體為什么請讀者自己思考。

順便說一句,C++中允許多重繼承是因為C++中非抽象類、抽象類和接口都用類來實現,而沒有在語言層面區分成不同的語言元素,其實如果設計良好,也是不應該出現對抽象類的多重繼承的,C#在語言層面上進行了約束,更有利於良好的設計,而C++對這方面比較靈活,需要開發者自己把握,因此C++對於初學者把握抽象類與方法更困難一些。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM