Guru of the Week 條款14:類之間的關係(上篇) (轉)
GotW #14 Class Relationships Part I:namespace prefix = o ns = "urn:schemas--com::office" />
著者:Herb Sutter
翻譯:kingofark
[宣告]:本文內容取自網站上的Guru of the Week欄目,其著作權歸原著者本人所有。譯者kingofark在未經原著者本人同意的情況下翻譯本文。本翻譯內容僅供自學和參考用,請所有閱讀過本文的人不要擅自轉載、傳播本翻譯內容;本翻譯內容的人請在閱讀瀏覽後,立即刪除其。譯者kingofark對違反上述兩條原則的人不負任何責任。特此宣告。
Revision 1.0
Guru of the Week 條款14:類之間的關係(上篇)
難度:5 / 10
(你的面向設計水平怎麼樣?本條款將描述一個迄今為止仍有許多員在犯的一個錯誤——唔,當然,是關於類的設計的錯誤)
[問題]
應用程式一般有兩種不同的communication session(通訊會話),每一個communication session(通訊會話)有其自己特定的訊息(message protocol)。當然,這兩種協議也有相似之處,比如一些運算操作和一些訊息(message)可能是相同的。於是,當程式設計師想在BasicProtocol類裡面封裝一些運算操作和訊息的時候,就可能進行如下的設計:
class BasicProtocol /* : possible base classes */ {
public:
BasicProtocol();
virtual ~BasicProtocol();
bool BasicMsgA( /*...*/ );
bool BasicMsgB( /*...*/ );
bool BasicMsgC( /*...*/ );
};
class Protocol1 : public BasicProtocol {
public:
Protocol1();
~Protocol1();
bool sg1( /*...*/ );
bool DoMsg2( /*...*/ );
bool DoMsg3( /*...*/ );
bool DoMsg4( /*...*/ );
};
class Protocol2 : public BasicProtocol {
public:
Protocol2();
~Protocol2();
bool DoMsg1( /*...*/ );
bool DoMsg2( /*...*/ );
bool DoMsg3( /*...*/ );
bool DoMsg4( /*...*/ );
bool DoMsg5( /*...*/ );
};
程式碼中的成員可以透過掉用基類的函式來實施一些必要的操作,但是真正的傳輸操作(transmission)還是由它們自己來實施。程式碼中的每一個類當然都有可能還包含有其它的成員。在這裡我們假定所有與問題有關的成員都已經列在我們所看到的程式碼中了。
試著分析程式碼中的這種設計。有什麼需要修改的嗎?如果有,說明為什麼需要修改。
[解答]
本條款指出了在對“類之間的關係”之設計中,普遍存在的一個隱患。我們先回憶一下上面給出的程式碼:Protocol1和Protocol2這兩個類以public方式派生自基類BasicProtocol;基類BasicProtocol完成一些特定的任務。
這個問題的關鍵在於下面的這句話:
程式碼中的成員函式可以透過掉用基類的函式來實施一些必要的操作,但是真正的傳輸操作(transmission)還是由它們自己來實施。
問題出來了:程式碼很清楚的描繪了一個“is implemented in terms of(根據某物實現)”關係。[譯註1] 在C++中,這種關係意味著實施“private inheritance(私有繼承)”或者“membership(成員歸屬)”。但不幸的是,許多人一直以為這種關係意味著實施“public inheritance(公有繼承)”。他們把implementation inheritance(實現繼承)和interface inheritance(介面繼承)搞混了。事實上,implementation inheritance(實現繼承)和interface inheritance(介面繼承)是完全不相同的,人們對於此的迷惑正是源於我們在本條款中所要討論的問題。[譯註2][註釋1]
下面的幾條線索更為詳細的說明了這裡的問題:
1. BasicProtocol類沒有提供虛擬函式(virtual function)(這裡不包括那個解構函式;關於解構函式的問題,會在下面的敘述中提到)。[註釋2] 這也就是說,它並不準備以polymorphically(多型)的方式來被使用,而這便意味著不應該使用公有繼承(public inheritance)機制。
2. BasicProtocol類裡面沒有protected函式或protected成員。這也就是說,類中不存在“derivation interface(派生介面)”,而這便意味著不應該以任何方式對BasicProtocol進行繼承——不管是以public方式還是以private方式。
3. BasicProtocol封裝了一些特定的操作,但它並不像其派生類那樣能夠實施自己的傳輸操作。這也就是說,BasicProtocol物件即不像其派生類的物件那樣工作(WORKS-LIKE-A),也不如其派生協議類的物件那樣有用(USABLE-AS-A)。公有繼承(public inheritance)應該只模塑(model)出一種(也是唯一一種)關係:即一個真正的介面上的IS-A(是一個)關係,它遵循Liskov substitution principle(里斯科夫替換原則)。[譯註3] 為了明確起見,我通常將其稱為WORKS-LIKE-A(像某物一樣工作)和USABLE-AS-A(如某物一樣有用)。[註釋3]
4. 這些派生類僅僅只使用了BasicProtocol的公共介面(public interface)。這也就是說,它們並沒有因為派生自BasicProtocol而得到什麼額外的好處。我們完全可以用一個輔助性的BasicProtocol物件來完成它們所承擔的任務。
這樣一來,我們就需要對程式碼做一些修改了:
首先,既然在設計時並不打算讓BasicProtocol作為基類被其它類繼承,那麼它的虛擬函式(virtual function)就是不必要的了,應該被去掉。其次,BasicProtocol應該改成一個諸如MessageCreator之類的名稱,使其不至於誤導別人,造成理解上的錯誤。
那麼,在經過了上述修改之後,應該採用哪一種方式來模塑(model)“is implemented in terms of(根據某物實現)”這種關係呢?採用private inheritance(私有繼承)方式,還是membership(成員歸屬)方式?
這個問題的答案很容易記住:
[學習指導]:在模塑(model)“is implemented in terms of(根據某物實現)”關係時,總是採用membership(成員歸屬)方式。只有在萬不得已確實需要形成繼承關係的時候——即當你需要能夠訪問protected成員,或者需要覆寫(overr)虛擬函式的時候——才採用private inheritance(私有繼承)方式。絕不要僅僅因為要複用程式碼就採用public inheritance(公有繼承)方式。
採用membership(成員歸屬)方式,使我們可以更好的把一些問題分開來考慮,因為這時候使用者之類(using class)成了一個普通的客戶端,只能對受用者之類(used class)的公共介面進行訪問。所以,請採用membership(成員歸屬)方式,這樣你就會發現你的程式碼簡潔多了、可讀性強了、更易維護了。簡單的說吧,你的程式碼會在花費方面節省多了!
*********************************************
[註釋1]:碰巧的是,那些易於犯這個錯誤的程式設計師們經常會生成極深的繼承層次。基於如下兩個原因,這樣做大大加重了維護工作的負擔:
l 增加了不必要的複雜性
l 強迫去了解許多不必要的類介面,即使他們僅僅只是想使用某一個特定的派生類。
另外,由於增加了不必要的vtable,以及不需要使用vtable的類的間接性,這樣做對的使用和程式也有影響。如果你發現自己也經常生成深層的繼承層次,那你就該審視一下你的設計方法了,看看你是不是已經養成了這個壞習慣。其實,較深的層次體系很少被使用,甚至基本上就不是一個好東西……如果你不相信,並仍然認為“如果沒有很多繼承關係的話,就不能算是物件導向了”,那麼你就應該去看看標準庫——那是一個很好的例子,足可以說明你想錯了。
[註釋2]:即使BasicProtocol本身也是派生自另一個類,這裡的結論也是不變的,因為BasicProtocol還是沒有提供任何新的虛擬函式(virtual function)。如果有某個BasicProtocol的基類真的提供了虛擬函式(virtual function),那也只不過說明,是那個基類希望以polymorphically(多型)的方式來被使用,而不是BasicProtocol;於是我們至多也就應該繼承自那個基類(而不是繼承自BasicProtocol)。
[註釋3]:的確,當你以public方式進行繼承從而得到一個介面的時候,如果基類既有你需要的介面,又有其特定的某些實現,那麼其中的一些實現你會附帶著得到。這種效果幾乎總是可以在設計上予以迴避,但也並不總是需要採取特別純正的“one responsibility per class(一個類一個職責)”這樣的方法。
[譯註1]:關於“is implemented in terms of”,請參看tt Meyers《Effective C++》條款40和條款42。
[譯註2]:請參看Scott Meyers《Effective C++》條款36:區分interface inheritance(介面繼承)和implementation inheritance(實現繼承)。
[譯註3]:關於Liskov substitution principle(里斯科夫替換原則),請參看侯捷先生譯的Scott Meyers《Effective C++》條款35中相關的討論。
(完)
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10752043/viewspace-991380/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 【java】類之間的關係Java
- UML類圖--類之間的關係
- 類與類之間的基本關係
- 介面、抽象類、普通類之間的關係抽象
- Java設計模式-類之間的關係Java設計模式
- 類之間的6種關係詳解
- Window、WindowManager、View 之間的關係View
- git、github、gitlab之間的關係GithubGitlab
- pcl::PointCloud和pcl::PontCloud::Ptr之間的關係和轉換方式Cloud
- TLS與SSL之間關係TLS
- React、Ant Design、DvaJS之間的關係ReactJS
- Activity、View、Window之間關係的分析View
- 思考 TPS 與 RT 之間的關係
- Window, WindowManager和WindowManagerService之間的關係
- react、redux、react-redux之間的關係ReactRedux
- Linux Shell檔案之間的包含關係Linux
- GeoTools應用-JTS(Geometry之間的關係)
- Java類關係之代理(代理模式)Java模式
- CPU、記憶體、磁碟IO之間的關係記憶體
- Kubernetes和Docker之間的關係是什麼?Docker
- 網站和伺服器之間的關係網站伺服器
- ERP與精益生產之間的關係
- Web3和元宇宙之間的關係Web元宇宙
- Maven專案之間關係介紹Maven
- 用 NetworkX + Gephi + Nebula Graph 分析人物關係(上篇)
- 前端之DOM解析和渲染與CSS、JS之間的關係前端CSSJS
- 集合類關係
- 淺析 UART、RS232、TTL 之間的關係
- 深度剖析Margin塌陷,BFC,Containing Block之間的關係AIBloC
- dispaly、position、float之間的關係與相互作用
- 頁面中多個script塊之間的關係
- 探索“精益”與“智慧製造”之間的關係
- Python中怎樣改變集合之間的關係?Python
- 特殊特性與FMEA之間的關係是什麼?
- 備份集和備份片之間的關係
- 大資料技術與Hadoop之間的關係大資料Hadoop
- python 類關聯關係Python
- TPS和響應時間之間是什麼關係
- 位(bit)、位元組(Byte)、KB、MB、GB ... 之間的關係