繼承與接口
概述:了解在Microsoft Visual Basic .NET中的類繼承與接口實現的區別。 目標 研究繼承與接口使用背后的概念 學習何時使用類繼承,何時使用接口實現 要求 為了充分利用本文,讀者必須具備以下基礎: 熟悉Microsoft® Visual Basic® .NET語言 熟悉Microsoft Visual Basic 6.0 基本理解繼承中的術語 目錄 面向對象編程:為什么編程? 繼承層次 創建和實現接口 比較類繼承與接口實現 對象合成 與 Visual Basic 6.0的區別? 小結 面向對象編程:為什么麻煩? 只有一小部分Visual Basic 6.0程序員發現他們在構建Visual Basic 6.0窗體時需要創建類模塊,而不是使用自動創建的窗體。另外,大多數的確在Visual Basic 6.0中創建過類模塊的程序員,創建創建類模塊的原因,只是因為要構建ActiveX®控件,并且不得不使用類模塊才能做到。 在Visual Basic .NET中,就象在所有其它語言中一樣,面向對象編程(OOP)不是一個選擇,而是一種需求。每段代碼都是某種類型,如類、接口、結構(結構是值類型,類似于Visual Basic 6.0中的用戶自定義類型),或枚舉值的一部分。甚至在Visual Basic .NET中看上去獨立的過程實際上也是作為隱藏類的共享方法實現的。 為何OOP如此強大?為什么微軟一直要求大家學習用OOP在Visual Basic .NET中編程? 處理復雜性與變化 OOP解決兩個普遍的軟件開發問題:處理復雜性和處理變化。利用OOP就容易設計和使用復雜的軟件系統,并且易于修改這些系統而無需打亂它們。 Visual Basic 6.0程序員需要用ADO或DAO對象模型獲得和操作數據,設想,如果每個操作都必須調用獨立的函數,而不是使用對象(如記錄集)的方法或屬性,完成這樣的工作會有多么艱巨。例如,如果要用ADO或DAO向列表框中裝入數據,大約要編寫10行代碼。而直接使用ODBC API函數,大約要編寫50行代碼!可以看到,OOP方法更容易實現。 再例如,考慮微軟Windows® API。如果要花大量時間調用Windows® API或使用其它面向過程的APIs,就會發現,為一個任務要調用哪個函數或過程,是多么容易出錯、多么難記。因此,通過創建相關對象,把問題模型化,對程序員來說,要比使用一長列的過程或函數要友好的多。 編寫和調試代碼是困難的,但大多數程序員喜歡跟蹤問題,設計也一種靈活方式解決,然后“玩弄”代碼,直到一切都滿足了客戶需求。這是有趣的人的一部分,但并不是事情的結局。所有程序員都害怕的一件事是:不得不研究其他人編寫的代碼(或他們在很久以前剛開始編寫代碼時所寫的代碼),不得不設計也程序各個部分如何搭配,如何進行修改以滿足新需求——所有這些都必須不能引起意想不到的后果。不幸的是,在大多數系統生命周期中,維護代碼所消耗程序員的時間,比創建這些代碼的時間還要多,這就是這些系統的結局。 通過清楚地分離暴露的公有類屬性、方法和事件與類成員的隱藏實現,在對象中封裝功能就支持進行修改。只要公有接口受到保護,私有實現就可以安全實現,這就是目標。下面將研究確保這個目標實現的方式。 繼承與接口 除了通過在對象的方法和屬性中封裝功能,使復雜系統易于創建和修改外,OOP還支持類繼承和接口實現,通過這種方式,新類就能安全地加入到現有系統中而不必修改現有代碼。通常將這兩種方法比作準備變化,研究每種方法何時是合適的,并討論第三種方法——對象組合,此方面經常是最好的選擇。 繼承層次 在所有存在于Visual Basic .NET中OOP技術中,繼承對來自Visual Basic 6.0的程序員來說是最不熟悉的,因為在Visual Basic 6.0中不存在繼承。甚至是Visual Basic 6.0程序員樂于使用的非Visual Basic 對象模型,如ADO或Office庫,也很少需要理解繼承。 通過繼承可以創建新類,它是現有(基)類的變體,并且在任何最初調用基類的情況中可以用新繼承類替換。例如,有一個名為SalesOrder的類,從這個類中派生一個名為SalesOrder的類: Public Class WebSalesOrder Inherits SalesOrder 現在任何使用SalesOrder類型對象的方法,都將保持不變,并且也能處理WebSalesOrder對象。如果SalesOrder有一個Confirm方法,那么就可以確保WebSalesOrder 類也有一個Confirm方法。但如果在SalesOrder類中Confirm方法被標記為可重載的,那么就可以在WebSalesOrder類中創建新的Confirm方法來重載原有方法: Public Overrides Sub Confirm 可能原始Confirm方法向客戶發送傳真,而WebSalesOrder 類中的Confirm方法使用電子郵件。重要的是,一個最初被設置為可以接受SalesOrder類型對象的方法,現在如果向它傳遞WebSalesOrder對象,那么此方法也能正常運行,并且該方法調用對象的Confirm方法后,客戶將收到電子郵件而不是傳真。舊代碼不需要做任何修改就可以調用新代碼。 另一方面,假設有一個Total,對兩個類來說其運行情況完全相同。派生類WebSalesOrder不需要對這個方法作任何修改——它的實現將自動從基類繼承,并且可以調用任何WebSalesOrder對象的Total方法,使此對象的行為就象SalesOrder對象一樣。 多態性 多態性,或可替代性,是繼承是明顯的一個優點:不論何時創建了派生類對象,在使用基類對象的地方都可以使用此派生類對象。WebSalesOrder對象 "is a" SalesOrder對象,WebSalesOrder對象必須能實現SalesOrder對象的所有功能,即使是它以自己獨特的方式。 多態性很多OOP優點的關鍵因素,在本文后面可以看到,它不僅存在于繼承中,還存在于接口中。利用多態性,不同類型的對象就可以處理交互時使用的一組通用消息,并且是以它們各自的方式進行。 訂單確認代碼不需要知道如何執行確認,也不需要知道被確認訂單的類型。它只關心它是否能夠調用所處理的訂單對象的Confirm方法,它要依賴此對象處理確認細節: Public Sub ProcessOrder(order As SalesOrder) order.Confirm 在調用ProcessOrder過程時,需要向它傳遞SalesOrder或WebSalesOrder對象,傳遞任何一個對象程序都能運行。 虛方法和屬性 只有派生類重載基類方法時才用到虛擬,虛擬可以是繼承中最具神秘性的概述。其神秘性在于.NET 運行時自動找到并運行被調用方法或屬性的最特殊實現。 例如,調用上面示例中的order.Confirm方法將會調用WebSalesOrder類的Confirm方法,此處SalesOrder.Confirm方法被重載了。然而,調用order.Total方法則會調用SalesOrder類的Total方法,因為在WebSalesOrder類中沒有創建專有的Total方法。 抽象類和方法 抽象是包含了至少一個必須被重載的方法或屬性的類。例如,創建了一個沒有實現訂單確認的SalesOrder類。因為不同類型的訂單必須以不同的方式確認,這樣就要從SalesOrder類派生類并重載Confirm方法,提供它自己的實現。 這意味著永遠不能創建SalesOrder對象。相反,只能使用派生類創建對象,填充如何執行訂單的細節。由于這個原因SalesOrder類將被標記為MustInherit: Public MustInherit Class SalesOrder Public MustOverride Sub Confirm() Public Sub Total() '計算總數的代碼 End Sub 如果必須重載所有類成員,那么這個類就被稱為純抽象類。例如,SalesOrder類可以只包含必須被派生類重載的方法和屬性。但只要有一個成員必須被重載,這個類就必須用MustInherit屬性標記為抽象類。 類型檢查與向下造型 如果有些訂單需要確認有些訂單不需要確認,結果會怎么樣呢?可以創建不包含Confirm方法的SalesOrder基類,而只在需要確認的派生類中增加Confirm方法。但這樣會出現一個問題:現在希望創建一個程序處理所有類型的訂單而在需要時才進行確認。如何知道哪個訂單需要確認呢? 為此程序提供一個SalesOrder類型的參數,以允許所有從SalesOrder派生的類型都能傳入。但這樣就需要在調用Confirm方法前對所有的確認類型進行測試。而且,一旦發現訂單類型是可確認的,那么就需要象下面這樣從SalesOrder類(此類中不存在Confirm方法)向下造型到派生類: Public Sub ProcessOrder(order As SalesOrder) If TypeOf(order) Is WebSalesOrder Then CType(order, WebSalesOrder).Confirm ElseIf TypeOf(order) Is EmailSalesOrder Then CType(order, EmailSalesOrder).Confirm ' 等等 這種類型的代碼很難維護。每次在系統中從SalesOrder派生一個類后,必須修改此方法。舊代碼與新代碼的這種耦合正是所要避免的。 這就同在此過程中實現不同類別的銷售訂單的確認代碼一樣糟糕 If TypeOf(order) Is WebSalesOrder Then ' 在此編寫確認 WebSalesOrder的代碼 ElseIf TypeOf(order) Is EmailSalesOrder Then '在此編寫確認 EmailSalesOrder的代碼 ' 等等 這種方式更為糟糕:每次創建新類型時都必須修改此程序,而且每種銷售對象也不再自己進行確認,結果使程序員很難添加新類型的訂單。程序員如何才能知道,在處理新類型訂單時哪些地方需要添加代碼呢? 另一種方法是創建名為ConfirmableSalesOrder的中間抽象類型(從SalesOrder類中派生,帶有必須重載的方法Confirm)。然后從ConfirmableSalesOrder中派生需要進行確認的類型,其它類型直接從SalesOrder派生。程序必須檢查傳入的SalesOrder對象是否是ConfirmableSalesOrder類型,如果是,將使用該類型調用Confirm方法。 If TypeOf(order) Is ConfirmableSalesOrder Then CType(order, ConfirmableSalesOrder).Confirm 要使用Confirm方法,仍需Ctype造型轉換。不過,通過虛擬性,調用將自動傳遞到創建訂單對象的類中,并運行在該類中定義的Confirm方法。 問題看上去解決了,但這只是臨時解決方案。猜出為什么了嗎?假定下一步要處理的事情是:某些類型的訂單需要信用卡。需要信用卡的訂單有一經過了確認,而另一些沒有。出現問題了吧。 .NET中不存在多重繼承 可以從SalesOrder中派生CreditCheckableSalesOrder類型。一些訂單類型從ConfirmableSalesOrder派生,另一些從CreditCheckableSalesOrder中派生,但需要確認又需要信用卡的訂單會怎么樣呢?.NET Framework中對繼承的一種限制是一個類型只能從一個基類型中派生。 訂單類型不能既從ConfirmableOrder派生又從CreditCheckableOrder派生。這可能被認為是專橫的或被誤導的限制,但這樣做有好多原因。在C++中支持多重繼承。然而,所有其它流行的面向對象的語言,包括Java,都不允許多重繼承。(一些高級語言,如Eiffel,曾試圖設計出不同類型的多重繼承,用于.NET 的Eiffel甚至在微軟.NET平臺上提供了多重繼承的樣子。) 多重繼承最大的問題是,當編譯器需要找到虛方法的正確實現時,會出現不確定性。例如,設想Hound 和 Puppy都從Dog中派生,而又BabyBasset從Hound 和 Puppy繼承: 圖 1. 多重繼承的問題 假設Dog有一個可重載的Bark方法。Hound重載了它使它聽起來象怒號,Puppy也重載了它使它聽起來象尖叫,但BabyBasset沒有重載Bark。如果創建了BabyBasset對象,然后調用它的方法,結果會怎樣呢,怒號還是尖叫? .NET Framework要求,派生類只能有一個基類,從而阻止了這種問題的發生。這種限制也意味著每個類最終從單個曾祖父輩System.Object類派生。 單路徑類繼承意味著.NET對象中以被System.Object類型作為參數的方法處理。單路徑類繼承在碎片收集中是至關重要的,因為碎片收集器要釋放被不可訪問對象占用的內存,而如果是這種情況,它就能處理所有類型的對象。 允許所有對象都向上造型(upcast)為通用類型,展現其優點的一個更熟悉的例子是事件處理程序: Public Sub MyEventHandler(By Val sender As _ System.Object, By Val e As System.EventArgs) 這個處理程序可以連接到來自任何對象或對象的任意組合的事件,這是因為參數sender是System.Object類型,而任意.NET對象都能代替它。如果必要,可以使用System.Reflection.GetType()識別sender對象是何種類型。 Creating and Implementing Interfaces創建和實現接口 繼承的這種微妙性是非常有趣的,但對于需要確認和/或信用卡的銷售訂單,應怎樣做呢?答案是使用接口。 類的多重繼承引發的問題是由繼承鏈中通用方法間的潛在沖突引起的。但是,對于所繼承的類是沒有具體實現的純抽象類,情況會如何呢?在這種情況下,多重繼承不會引發任何問題,因為沒有具體實現,也就不會引起沖突。這就是接口所提供的功能:繼承一組方法和屬性說明,但不關心其具體實現,因此從多個接口中繼承不會引起問題。 雖然經常使用短語“接口繼承”,但正確的術語是接口實現。一個接口從另外一個接口繼承是可能的,因此能夠將接口操縱方法集擴展到包含它所繼承的接口的方法。然而,要在Visual Basic .NET類中使用接口,就要實現這些接口而不是繼承它們: Public Interface IConfirmable Sub Confirm() End Interface Public Class WebSalesOrder() Inherits SalesOrder Implements IConfirmable Public Sub Confirm() Implements IConfirmable.Confirm ' 確認web 訂單的代碼 End Sub ' 另一個WebSalesOrder 代碼 End Class (在C#中,冒號被用于表達類繼承和接口實現。這可能就是它通常在接口名前加前綴“I”的原因。通過這種方式,C#程序員就能容易地區分基類和接口。) 可以創建一些銷售訂單類型,一些實現了IConfirmable,一些實現了ICreditCheckable,而還有一些兩者都實現了。要檢查訂單是否需要確認,所使用的代碼與檢查訂單是否是從特定類型繼承的,并將其造型為那種類型的代碼是一樣的: Public Sub ProcessOrder(order As SalesOrder) If TypeOf(order) Is IConfirmable Then CType(order, IConfirmable).Confirm 接口多態性 接口也提供了派生類所具有的多態性優點。例如,可以將任何實現了Iconfirmable的類對象傳遞給需要Iconfirmable參數的方法: Public Sub ConfirmOrder(order As IConfirmable) order.Confirm End Sub 如果WebSalesOrder 和 EmailSalesOrder都實現了,那么就可以將任一對象傳遞給ConfirmOrder方法。當調用order.Confirm時,在適當類中實現的確認代碼將運行。即使沒有將類的方法命名為Confirm,但只要將它標記為實現了IConfirmable接口的Confirm方法,程序就能正常運行。 Public Class WebSalesOrder() Inherits SalesOrder Implements IConfirmable Public Sub ConfirmWebOrder() _ Implements IConfirmable.Confirm ' 確認Web訂單的代碼 End Sub 自由命名是一個有用的特性。如果類實現了兩個不同的接口,而恰好接口中具有相同名字的方法,就要利用這種特性了。 比較類繼承與接口實現 創建派生類與實現接口間最重要的技術差別是,派生類只能從一個基類繼承,但一個類可以實現多個接口。 從設計角度講,繼承是表達一種特殊類型的關系。如果WebSalesOrder是一種特殊類型的SalesOrder,那么就要考慮使用派生類了。 然而,要小心,當區分派生類與基類的專有性也是其它類需要支持的特性時,不要使用繼承。對于向類增加這種類型的特性或能力時,接口實現提供了更大的靈活性。 繼承用于構建框架 設計有用的繼承層次需要詳細規劃,并且清楚如何使用繼承。要把它當作有經驗的軟件設計師(而他正在創建框架,程序員要利用此框架構建眾多應用程序)需要完成的任務,而不是當作在簡單地構建特定應用程序時所使用的戰略。 .NET Framework自身就包含了許多正在被使用的繼承實例,并且需要創建派生類才能執行很多通常的編程活動。例如,可以從ApplicationException類派生自己專有的異常類,以存儲定制的錯誤信息。當釋放定制事件后,通過從EventArgs派生一個定制類將信息發送到事件處理程序。要創建特定類型的集合,可以從CollectionBase類中派生。每次在Visual Studio .NET中創建Windows窗體時,就從Windows.Forms.Form基類中派生了一個類。 你應當習慣從框架設計師提供的基類中派生類,但在創建自己的基類時要小心。要確保正在表達一個清晰的層次,并且分析出了客戶端程序員要重載的行為。 創建自己的接口時也要小心,但使用接口比使用繼承更不容易走到死角,因此在可以進行選擇的情況下優先考慮它們。 對象組合 當在考慮創建自己的繼承層次時,不要過多地被重用代碼的呼聲所影響。重用自身不是創建派生類的足夠原因。 與其使用繼承允許新對象使用現有對象的代碼,不如利用稱為組合,包容,聚合或封裝的技術。在Visual Basic 6.0中你可能使用過這種技術,但在Visual Basic 6.0中并不存在繼承。 例如,要創建WebSalesOrder類,此類重用了SalesOrder類中的所有代碼,并增加了一些新代碼,那么就在WebSalesOrder類中聲明并創建SalesOrder對象的一個實例。可以公開暴露內部SalesOrder對象,也可將其保存為私有。 代理 如果SalesOrder類中有一個Total方法,通過簡單地調用私有SalesOrder實例的Total方法,WebSalesOrder類也就有了Total方法。將方法調用(或屬性調用)傳遞到內部對象的技術通常稱為代理,但不要將它與.NET中利用代理對象創建回調函數或事件處理程序的用法混淆。就象在Visual Basic 6.0中一樣,通過利用WithEvents關鍵字聲明被包含的對象,被包含對象的事件可以暴露在封裝類中。 將接口實現與組合結合起來 使用對象組合與代理的主要缺點是,不能自動獲得象派生類那樣的多態性。如果WebSalesOrder對象簡單地包含了SalesOrder對象,而不是從另一對象中派生,那么就不能將WebSalesOrder對象傳遞到需要SaleOrder類型作為參數的方法。 通過創建ISalesOrder接口,而WebSalesOrder實現此接口,就可以克服這個缺點。也可以為被包含SalesOrder對象的方法和屬性提供代理。或,如果需要,WebSalesOrder也可以獨立的實現接口中的方法和屬性,而不是代理到SalesOrder對象。這與派生類的重載類似。 將對象組合與代理同接口實現結合起來,就可以重用代碼,實現多態性,而無需設計令人頭痛的繼承。由于這個原因,當需要對類進行擴展或提供專有功能時,優先考慮這種方法。 與Visual Basic 6.0的區別是什么? 在Visual Basic 6.0中,每次創建一個類模塊時,都自動創建一個同名的類接口。考慮下面的Visual Basic 6.0代碼: ' Visual Basic 6.0 代碼 Dim myObject As MyClass Set myObject = New MyClass 在這段代碼中,第一次使用MyClass時,指向了包含空方法和屬性的隱藏接口,第二個MyClass是實現了這些方法和屬性的具體類。Visual Basic 6.0對你屏蔽了接口的使用,而接口的使用對于底層的COM管道是非常重要的。 Visual Basic 6.0中的Implements關鍵允許明確使用接口,并利用存在于Visual Basic .NET中基于接口的多態性。然而,類實現必須使用合并了接口名的命名轉換: ' Visual Basic 6.0 IConfirmable 類模塊 Public Sub Confirm() End Sub
' VB6 WebSalesOrder 類模塊 Implements IConfirmable Private Function IConfirmable _Confirm() ' 此處實現確認訂單的代碼 End Function 任何以這種方式實現了IConfirmable 接口的Visual Basic 6.0對象都可傳遞給需要Iconfirmable類型作為參數的程序,從而在Visual Basic .NET中支持了接口提供的同種類型的多態性: ' Visual Basic 6.0 或 Visual Basic .NET Public Sub ConfirmOrder(order As IConfirmable) order.Confirm End Sub 雖然在Visual Basic .NET中使用接口的語法更復雜、不易混淆,但最大的變化是對繼承的支持。從舊類中派生新類,利用舊類的功能在Visual Basic 6.0中是不可能的,因此沒有辦法重載選擇的方法。 支持繼承是一個很重要的變化,但這并不是因為迫切需要創建基類,并從中派生新類。它的最大價值是能夠繼承C#、C++或其它.NET語言程序員所使用的框架類。如果在Visual Basic .NET中創建了基于繼承的框架,那么其它.NET程序員就可以使用此框架。這就使Visual Basic .NET同其它.NET位于同一水平,打破了過去隔離Visual Basic 程序員的屏障。 小結 本文學習了類繼承與接口實現間的區別。繼承支持專有性不斷增加的類層次框架,它們共享一些代碼,并進行了定制。接口允許多個不相關的類共享可預測的方法和屬性。接口和繼承提供了多態性,這樣一般性的過程就可以使用不同類型的對象。還學習了如何利用對象組合而不是繼承重用和擴展實現代碼,對象組合如何與接口結合起來支持多態性。所有這些技術都用于創建和修改復雜的軟件系統,以增加新功能,而最小化研究以前代碼的需要。
|