Cocoa 繼承類
Cocoa 繼承類是本文要介紹的內容,象Application Kit這樣的框架都定義某種程序模型。由于這個模型具有一般性,很多不同類型的應用程序都可以共享。也由于這個模型具有一般性,框架中的某些類是抽象類或有意沒有完成也并不奇怪。一個類通常會完成很多低級別的、公用的代碼,而將工作的相當一部分留下來,或者以安全而又一般的“缺省”方式來完成。
應用程序通常需要創建子類來填充超類留下的缺口,提供框架類缺少的東西。子類是向框架添加具體應用程序行為的基本途徑。定制子類的實例在框架定義的對象網絡中代替其超類的位置,并通過繼承從超類得到與框架中其它對象協同工作的能力。舉例來說,如果您創建了一個NSCell的子類,則這個新類的實例可以出現在NSMatrix對象中,就象NSButtonCell、NSTextFieldCell、以及其它框架定義的cell對象一樣。
在制作子類時,一個主要的任務就是實現一組由超類(或者超類采納的協議)聲明的具體方法。重新實現超類的方法被稱為對該方法進行重載。
何時進行方法的重載
框架類中定義的大多數方法都是完全實現的,您可以對其進行調用,以得到它們提供的服務。您很少需要重載這種方法,而且也不應該試圖這樣做。依賴于這些類的框架只是做它們應該做的事—既不多,也不少。在某些場合下,您可以對這些方法進行重載,但是沒有真正的原因需要這么做,框架版本的方法已經足夠了。但是,正如您可能實現您自己的字符串比較函數、而不是使用strcmp函數那樣,如果您愿意,可以選擇重載框架的方法。
然而,有些框架方法的設計目的就是為了被重載的,您可以通過這種方式向框架加入程序的具體行為。這些方法在框架中的實現對應用程序通常價值很小,或者沒有價值,但會在其它框架方法發出的消息中被調用。應用程序必須實現自己的版本,為這些方法加入新的內涵。
調用還是重載?
一般來說,您自己并不調用,至少不直接調用在子類中重載的框架方法。您只要簡單地重新實現這些方法,然后將它留給框架就好了。實際上,越是那些實現應用程序具體行為的版本,您自己的代碼對它調用的可能性就越小。這有一個很好的原因。在一般意義上,框架類負責聲明一些公共方法,您作為開發者可以有兩種使用方式:
調用這些方法,使類提供的服務為您所用
對這些方法進行重載,將您的代碼引入到框架定義的程序模型中
有些時候,一個方法會同時符合上述兩種情況,既可以通過被調用提供有價值的服務,也可以被策略性地重載。但是一般來說,一個方法如果可以被調用,就已經由框架完全定義好了,不需要在您的代碼中進行精化;如果該方法需要在子類中重新實現,則說明框架為該方法分派了特殊的工作,而且會在恰當的時候對其進行調用。圖3-2顯示了這兩種一般類型的框架方法。
圖 調用一個框架方法,該方法又通過消息調用一個重載了的方法
使用Cocoa框架進行面向對象編程的大部分工作是實現一些方法,而您的程序只是間接地、通過框架安排的消息使用這些方法。
重載方法的類型
您可以選擇在子類中定義幾個不同類型的方法:
某些框架方法是完全實現的,且其設計的目的是被別的框架方法調用。換句話說,即使您重新實現這些方法,通常也不在其它代碼的其它地方調用。它們提供特定的服務—數據或行為,這些服務是程序執行過程中某些地方的代碼要求的。這些方法存在于公共接口中只有一個原因—就是讓您在需要的時候可以對其進行重載,這使您有機會用自己的算法來替代框架使用的算法,或者對框架的算法進行修改和擴展。
這種類型的方法的一個例子是NSMenuView類定義的trackWithEvent:方法。NSMenuView類實現這個方法是為了滿足看得見的需求—處理菜單跟蹤和菜單項的選擇,但是如果您希望實現不同的行為,則可以對其進行重載。
另一類方法負責做一些與具體對象有關的決定,比如是否打開某個屬性,或者是否讓特定的策略起作用??蚣転檫@種方法實現一個缺省版本,從而提供一種工作方式,如果您需要有所改變,就必須實現自己的版本。在大多數情況下,實現就是簡單地返回YES或者NO,或者對某個值進行計算,而不是使用缺省值。
NSResponder類的acceptsFirstResponder方法就是一個典型的例子。系統向視圖對象發送消息中包含acceptsFirstResponder消息,用于詢問它們是否響應按鍵或鼠標點擊事件。缺省情況下,NSView對象在這個方法中返回NO—大多數視圖對象并不接收按鍵輸入。但是某些視圖對象卻是可以的,因此它們必須重載acceptsFirstResponder方法,使之返回YES。
某些方法必須被重載,但只是增加一些處理,而不是完全取代框架的實現。這種方法的子類版本對超類版本的行為進行增強。您的程序在實現這種方法時,很重要的一點是要吸收被重載方法,即向super(超類)對象發送消息,調用框架為該方法定義的版本。
這類方法通常是繼承鏈中的每個類都希望有所貢獻的。舉例來說,可以自行歸檔的對象必須遵循NSCoding協議,并且實現initWithCoder:和encodeWithCoder:方法。但是,一個類在對自己特有的實例變量進行編解碼的時候,必須調用相應方法的超類版本。
有些時候,方法的子類版本希望“重用”超類的行為,然后在***的結果中加入一些小變化。比如NSView類的drawRect:方法,執行某些復雜描畫的視圖子類可能希望在描畫結果中加上一個邊界,這樣就要首先調用super版本的方法。
某些框架方法什么事情都不做,或者只是返回一些試驗性的缺省值(比如self),避免運行時或編譯時的錯誤。這些方法的設計目的就是為了被重載。即便是最基本的行為,框架也無法為它們定義,因為它們執行的任務全部和具體程序相關。對于這種方法,沒有必要通過向super發送消息來調用框架的實現。
子類重載的大部分方法都是這種類型。比如NSDocument類的dataOfType:error:和readFromData:ofType:error:(還有其它)方法,在您創建基于文檔的應用程序時必須被重載。
對一個方法進行重載并不一定很難。通過認真地重寫方法中的一兩行代碼,您常常就能顯著改變超類的行為。在實現自己版本的方法時,也不是完全從頭開始,您可以借助Cocoa框架提供的類、方法、和類型。
什么時候需要使用子類
和了解類的哪些方法需要重載—并真正地實施重載—一樣重要的是,識別哪些類需要被繼承。有些時候,這些決定可能是很明顯的,而在另一些時候,做這樣的決定則相當不簡單。下面的一些設計上的考慮可以指導您做這樣的選擇。
首先,連接框架。您應該熟悉每個框架類的目的和能力。您希望做的事情可能已經在某個類中實現了,或者如果您發現希望完成的任務在某個類中已經差不多完成了,那就很幸運了,那個類很可能是您需要的定制類的超類。子類化是重用現有的類、并根據需要將它具體化的過程。有些時候,一個子類需要做的所有工作,就是對一個方法進行重載,并使它的行為和超類版本輕微不同。其它子類可能在超類的基礎上增加一兩個屬性(以實例變量的形式),然后實現一些訪問和操作這些屬性的方法,從而將它們集成到超類的行為中。
在決定子類在類層次中的位置時,還有其它一些有益的考慮。您希望開發的應用程序、或者應用程序的一部分的本質是什么?有些Cocoa架構對子類有些要求。舉例來說,如果您開發的是一個多文檔的應用程序,則Cocoa基于文檔的架構就要求您生成NSDocument類的子類,可能還有其它類。如果要讓您的應用程序可以通過腳本進行控制(也就是說,可以響應AppleScript命令),可能必須生成諸如NSScriptCommand這樣的腳本類的子類。
另一個因素是子類的實例在應用程序中發揮的作用。模型-視圖-控制器模式是Cocoa的主要設計模式,它將對象的角色做如下分配:出現在用戶界面上的對象屬于視圖對象,模型對象負責保存應用程序數據(和對該數據進行操作的算法),控制器對象則負責協調視圖對象和模型對象(詳細信息請參見"模型-視圖-控制器設計模式"部分)。
了解一個對象的作用可以收窄其超類的選擇范圍。如果您的類實例是實現定制描畫和事件處理的視圖對象,可能應該選擇NSView作為超類;如果您的應用程序需要一個控制器類,則可以使用某個復活類(比如NSObjectController類),或者如果您希望有不同的行為,也可以從NSController或NSObject派生出子類;如果您的類是一個典型的模型類—比如代表一個電子表格數據中的行的類—則可能應該從NSObject派生出子類,或者使用Core Data框架。
然而生成子類有時并不是解決問題的***辦法,可能有更好的辦法可以選擇。如果您只是希望為某個類增加一些便利方法,就可以通過創建范疇來實現,而不需要生成子類;或者,您也可以借助基于Cocoa開發“工具箱”資源的很多其它設計模式之一來實現,比如委托、通告、和目標-動作模式(在"和對象進行通訊"部分中描述)。在確定使用某個候選超類之前,先掃描一下它的頭文件(或參考文檔),看看是否有什么委托方法、通告、或者其它機制可以實現您需要的功能,而又不需要生成子類。
類似地,您也可以考察一下框架協議的頭文件或文檔。通過采納協議,您既可以完成目標,又可以規避復雜子類的困難工作。舉例來說,假定您希望管理菜單項的激活狀態,則可以在定制的控制器子類中采納NSMenuValidation協議,而不必從NSMenuItem或NSMenu派生子類來得到這個行為。
和某些框架方法不用于被重載一樣,一些框架類(比如NSFileManager、NSFontPanel、和NSLayoutManager)也不用于生成子類。如果您確實希望有這樣的子類,則應該謹慎處理。某些框架類的實現是相當復雜的,和其它類的實現、甚至是操作系統的不同部分緊密結合在一起。通常情況下,我們很難正確復制框架方法的行為,或者預期這種方法可能有的依賴性和效果。您對一些方法實現的修改,可能帶來深遠的、不可預見的、以及不希望的結果。
在某些情況下,您可以通過對象的合成來克服這種困難。對象的合成是一種將多個對象裝配到一個“宿主”對象中的通用技術,宿主對象負責管理這些對象,并獲得復雜而又高度定制的行為(參見圖 3-3)。您不必直接從一個復雜的框架超類繼承子類,而是創建一個定制類,然后將超類的實例作為類的一個實例變量。定制類自身可能相當簡單,可能直接從NSObject根類繼承就可以了。雖然從繼承的角度來看是簡單了,但是該類負責對嵌入的實例進行操作、擴展、和增強。對于客戶對象來說,該類在某些方面就象是復雜超類的子類,雖然它可能并不共享超類的接口。
Foundation框架中的NSAttributedString類就是對象合成的一個實例。NSAttributedString以實例變量的形式保有一個NSString對象,并通過string方法將它暴露給客戶代碼。NSString是一個具有復雜行為的類,包括字符串編碼、字符串檢索、以及路徑處理。NSAttributedString則在這些行為的基礎上加入了新的能力,可以將字體、顏色、對齊、以及段落風格這樣的信息附加到某個范圍的字符中,而且在不生成NSString子類的前提下實現這個增強。
圖 對象合成
有些時候,看起來很明顯的超類候選者其實并不是***的選擇。您可能知道,Cocoa在大多數情況下都用NSView對象來進行描畫。但是如果您正在設計的描畫程序或CAD程序可能具有成千上萬個圖形元素,則應該考慮使用您自行定制的圖形元素類,而不是從NSView繼承。從對象尺寸的角度看,一個NSView對象攜帶很多實例數據,而您定制的類的圖形元素實例可以是“輕量級”的,但又包含NSView對象中用于描畫的所有信息。