要麼設計繼承並提供文件說明,要麼禁用繼承

2022-07-23 12:03:16 字數 3056 閱讀 8582

條目 18 中提醒你注意繼承沒有設計和文件說明的「外來」類的子類化的危險。 那麼對於專門為了

繼承而設計並且具有良好文件說明的類而言,這又意味著什麼呢?

首先,這個類必須準確地描述重寫每個方法帶來的影響。 換句話說,該類必須文件說明可重寫方法

的自用性(self-use)。 對於每個 public 或者 protected 的方法,文件必須指明方法呼叫哪些可重寫方

法,以何種順序呼叫的,以及每次呼叫的結果又是如何影響後續處理。 (重寫方法,這裡是指非

final 修飾的方法,無論是公開還是保護的。)更一般地說,乙個類必須文件說明任何可能呼叫可重

寫方法的情況。 例如,後台執行緒或者靜態初始化**塊可能會呼叫這樣的方法

好的 api 文件應該描述乙個給定的方法做了什麼工作,而不是描述它是如

何做到的。那麼,上面這種做法是否違背了這句格言呢?是的,它確實違背了!這正是繼承破壞了封裝

性所帶來的不幸後果。所以,為了設計乙個類的文件,以便它能夠被安全地子類化,你必須描述清楚那

些有可能未定義的實現細節。

為了繼承而進行的設計不僅僅涉及自用模式的文件設計。為了使程式設計師能夠編寫出更加有效的子

類,而無須承受不必要的痛苦,類必須以精心挑選的 protected 方法的形式,提供適當的鉤子

(hook),以便進入其內部工作中

測試為繼承而設計的類的唯一方法是編寫子類

如果你忽略了乙個關鍵的受保護的成員,試圖編

寫乙個子類將會使得遺漏痛苦地變得明顯。 相反,如果編寫的幾個子類,而且沒有乙個使用受保護的成

員,那麼應該將其設為私有。 經驗表明,三個子類通常足以測試乙個可繼承的類。 這些子類應該由父

類作者以外的人編寫

當你為繼承設計乙個可能被廣泛使用的類的時候,要意識到你永遠承諾你文件說明的自用模式以及

隱含在其保護的方法和屬性中的實現決定。 這些承諾可能會使後續版本中改善類的效能或功能變得困難

或不可能。 因此, 在發布它之前,你必須通過編寫子類來測試你的類。

還有一些類必須遵守允許繼承的限制。 構造方法絕不能直接或間接呼叫可重寫的方法。 如果違反

這個規則,將導致程式失敗。 父類構造方法在子類構造方法之前執行,所以在子類構造方法執行之前,

子類中的重寫方法被呼叫。 如果重寫方法依賴於子類構造方法執行的任何初始化,則此方法將不會按預

期執行。

如果你決定在為繼承而設計的類中實現 cloneable 或 serializable 介面,那麼應該知道,

由於 clone 和 readobject 方法與構造方法相似,所以也有類似的限制: clone 和

readobject 都不會直接或間接呼叫可重寫的方法。 在 readobject 的情況下,重寫方法將在子類

的狀態被反序列化之前執行。 在 clone 的情況下,重寫方法將在子類的 clone 方法有機會修復克

隆的狀態之前執行。 在任何一種情況下,都可能會出現程式故障。 在 clone 的情況下,故障可能會

損壞原始物件以及被轉殖物件本身。 例如,如果重寫方法假定它正在修改物件的深層結構的拷貝,但是

尚未建立拷貝,則可能發生這種情況。

最後,如果你決定在為繼承設計的類中實現 serializable 介面,並且該類有乙個

readresolve 或 writereplace 方法,則必須使 readresolve 或 writereplace 方法設定為

受保護而不是私有。 如果這些方法是私有的,它們將被子類無聲地忽略。 這是另一種情況,把實現細

節成為類的 api 的一部分,以允許繼承。

到目前為止,設計乙個繼承類需要很大的努力,並且對這個類有很大的限制。 這不是乙個輕率的

決定。 有些情況顯然是正確的,比如抽象類,包括介面的骨架實現(skeletal implementations)(詳見

第 20 條)。 還有其他的情況顯然是錯誤的,比如不可變的類(詳見第 17 條)。

但是普通的具體類呢? 傳統上,它們既不是 final 的,也不是為了子類化而設計和文件說明

的,但是這種情況是危險的。每次修改這樣的類,則繼承此類的子類將被破壞。 這不僅僅是乙個理論問

題。 在修改非 final 的具體類的內部之後,接收與子類相關的錯誤報告並不少見,這些類沒有為繼

承而設計和文件說明。

解決這個問題的最佳方法是禁止對在設計上和文件說明中都不支援安全子類化的類進行子類化。

這有兩種方法禁止子類化。 兩者中較容易的是宣告類為 final 。 另一種方法是使所有的構造方法都

是私有的或包級私有的,並且新增公共靜態工廠來代替構造方法。 這個方案在內部提供了使用子類的靈

活性,在條目 17 中討論過。兩種方法都是可以接受的。

這個建議可能有些爭議,因為許多程式設計師已經習慣於繼承普通的具體類來增加功能,例如通知和同

步等功能,或限制原有類的功能。 如果乙個類實現了捕獲其本質的一些介面,比如 set , list 或

map ,那麼不應該為了禁止子類化而感到愧疚。 在條目 18 中描述的包裝類模式為增強功能提供了繼

承的優越選擇。

如果乙個具體的類沒有實現乙個標準的介面,那麼你禁止繼承可能給一些程式設計師帶來不便。 如果你

覺得你必須允許從這樣的類繼承,乙個合理的方法是確保類從不呼叫任何可重寫的方法,並文件說明這

個事實。 換句話說,完全消除類的自用(self-use)的可重寫的方法。 這樣做,你將建立乙個合理安全

的子類。 重寫乙個方法不會影響任何其他方法的行為。

你可以機械地消除類的自我使用的重寫方法,而不會改變其行為。 將每個可重寫的方法的主體移動

到乙個私有的「幫助器方法」,並讓每個可重寫的方法呼叫其私有的幫助器方法。 然後用直接呼叫可重寫

方法的專用幫助器方法來替換每個自用的可重寫方法。

簡而言之,專門為了繼承而設計類是一件很辛苦的工作。你必須建立文件說明其所有的自用模式,

並且一旦建立了文件,在這個類的整個生命週期中都必須遵守。如果沒有做到,子類就會依賴父類的實

現細節,如果父類的實現發生了變化,它就有可能遭到破壞。為了允許其他人能編寫出高效的子類,你

還必須暴露乙個或者多個受保護的方法。除非意識到真的需要子類,否則最好通過將類宣告為

final ,或者確保沒有可訪問的構造器來禁止類被繼承

C 繼承設計

1 共有繼承意味著 is a 關係 是一種 2 繼承中父類的作用域相當於巢狀在子類中。注意上面的宣告,子類中所有名為mf1 mf3 的函式都被父類中mf1 mf3所覆蓋,即使函式有不同的引數型別那個也適用,而且無論是不是虛函式也同樣適用。我們只在意其名稱。3 解決上面的預設掩蓋行為的方法是使用usi...

設計繼承樹2

4 通過尋找使用共同行為的子類來找出更多抽象化的機會 我們觀察到這六種動物都有makenoise 和eat 兩種方法的共同部分,而wolf與dog可能有某些共同的行為,在lion tiger cat之間也是如此。5 完成類的繼承層次 因為動物本來就有組織化的層次 界 門 綱 目 科 屬 種 我們可以...

關於繼承的設計

我將成員變數稱之為類的屬性,將成員函式稱之為類的介面 關於子類繼承父類,比較讓我蛋疼的用法就是在子類中直接使用父類的成員變數,且不說父類完全有可能不是自己設計的,就算是自己設計的,也許已經過了n久了,再次使用父類裡面的屬性時都要想一下這個屬性到底是幹什麼用的.比如當時寫了乙個類 parent,而且也...