關於 C++ 類層次結構的設計方法學,note-to-self + keynote + cross-reference 式筆記
本文精煉於 [CPP LANG] 12.4, 15.2 的 BBWindow 示例,只涉及 design
Syntax 參考 [CPP LANG] Ch12, 15; [CPP PRIMER] Ch17, 18
Play with bits 參考 [CPP OBJMODEL] 5.2
keyword: class hierarchy, multiple inheritance, abstract class, virtual base class, abstract factory, clone
目錄
IValBox: 取得用戶輸入整數的 GUI 元素之抽象類,它不綁定具體 GUI 元素,如 slider 滑塊, dial 撥盤
IValSlider: 滑塊式 IValBox 實現,類似的還有 IValDial 撥盤式實現,代表 IValBox 類層次中具體的 GUI 元素。這些類可以進一步擴展,如從 IValSlider 派生出 PopupIValSlider
BBWindow: 第三方提供的 GUI 元素實現,類似 MFC 的 CWnd 等。IValBox 的類層次依靠 BBWindow 的類層次實現 GUI 特性(畫圖之類)。BBWindow 只是形式名,它可以替換,其意義就像將 MFC 換成 Qt 一樣,這使得 IValBox 的類層次能夠減小對特定 GUI 元素實現的依賴
UML: Old Hierarchy
設計要義:
使用 BBWindow 不是 IValBox 概念的基本部分。過分依賴 BBWindow,使得 BBWindow 難於替換
可變數據是實現的部分,當它侵入接口(抽象類)時,會影響接口的靈活性
Two-type interfaces: public interface vs. protected interface
interface: 有譯接口,有譯界面;有時代表函數,有時代表函數之聚集處(類、名字空間),憑上下文判斷。按 [EFFECT CPP] Item 18 的說法:每一種接口都是客戶與你的代碼互動的手段
public interface: 給用戶使用
protected interface: 給派生類使用
Strict Guide: Data members are better kept private so that writers of derived classes cannot mess with them.
推論: A protected interface should contain only functions, types, and constants.
更多 private data member 的討論見 [EFFECT CPP] Item 22
Over strict?
Old Hierarchy 是不是 bad design?
實際上我用 MFC 時,大多數時候都這樣做,這是大多數人用 GUI 框架的方法
問題不在絕對的 bad design 或 good design,而在於目標是什麼?Application Development vs. Library Development, Cross-platform vs. Dedicated-platform
下面的改進設計比 Old Hierarchy 靈活而光鮮,但也帶來的額外的負擔(如理解和維護),Pros and Cons 自行抉擇
UML: Separate Implementation and Interface
接口線:接口繼承形成的層次
實現線/擴展線:實現繼承形成的層次
設計要義:
public 繼承 vs. protected/private 繼承
另一種表述是:接口繼承 vs. 實現繼承,見 [EFFECT CPP] Item 40。注意 Item 34 和這個設計無關,雖然標題相同,但那個是 for member function 的,這裡是 for class 的
public 繼承塑模 "is-a" 的關系(見 [EFFECT CPP] Item 32),而 protected/private 塑模 "implemented-in-terms-of" 的關系(見 [EFFECT CPP] Item 39)
protected 和 private 的區別:private 之實現止於直接派生類,而 protected 之實現可以進一步擴展
替代技術:public 繼承 + Composition 復合
當用於實現域時,復合塑模 "implemented-in-terms-of" 的關系(和 protected/private 繼承相同),見 [EFFECT CPP] Item 38
采用 protected/private 繼承還是復合的判斷,見 [EFFECT CPP] Item 39,如繼承會造成 EBO (Empty Base Optimization) 空白基類最優化
這是分離實現和接口後得到的益處
UML: Substitute Implementation
圖中的 BBSlider 表示 BBIValSlider 可藉由以存在的更特定的 GUI 元素實現,而不是更一般的 BBWindow,就像是從 MFC 的 CWnd 改為了 CSliderCtrl
使用同樣的方法進行擴展就能得到 Big One:
UML: Substitute Implementation, Complicated
無需糾結上圖具體含義,這裡的設計推論更有意思:一個系統展現給用戶的應該是一個抽象類的層次結構,其實現所用的是一個傳統的層次結構
有點教條吧?試問,傳統的層級結構從何而來?我們要做一個橫跨多種 GUI 框架之上的框架麼?
用 virtual 基類消除 replicated base class 重復基類,見 [EFFECT CPP] Item 40
這種用法的目的有二:
UML: Share Implementation
這裡 Diamond-shaped Inheritance 鑽石形繼承的意義:讓實現 PopupIValSlider 的類(即 BBPopupIValSlider)共享實現 IValSlider 的類(即 BBIValSlider)中的實現,以減少編碼
於是另一個有意思的推論:組成應用之接口的抽象類的所有派生都應該是 virtual 的,[EFFECT CPP] Item 40 也說 public 繼承應該總是 virtual
但是現實不能如此,最簡短的反駁是效率因素,見 [CPP LANG] 15.2.5 和 [EFFECT CPP] Item 40
可藉由無 data member class 進行重復基類方式的優化
於是,饒了一圈又回來了
構造函數不可能是 virtual 的,道理很簡單:不知道對象的確切類型,又如何構造它(構造函數的實質是對象內布局的 bits 初始化)
抽象工廠和 Clone 模式被戲稱為 virtual constructor 虛擬構造函數,因為它們用 virtual 函數迂回完成構造函數的任務:根據某些線索創建對象
絕對的抽象創建(沒有線索)是不可能的,即沒有語法支持(virtual 構造函數),也沒有邏輯意義:當你想要鉛筆時,可以說我要鉛筆,也可以說我要鉛筆盒中的東西(帶線索的抽象),但不能只說我要東西(不帶線索的抽象)
Why abstract class?
UML: Abstract Factory
減小對象創建時,對特定實現類(構造函數)的依賴,如當創建 IValDial 時,必須使用 BBIValDial 或 LSIValDial 的構造函數
當抽象類層次結構較復雜時,並且有從一個實現系統變為另一個實現系統(如從 BBWindow 變為 LSWindow)的預期時,需要一種一次性裝入實現系統中各種創建對象的方法,這時抽象工廠就會發揮作用:
1、2 是程序初始化階段執行的設置,3、4 是程序例行階段的行為
於是,抽象工廠是和抽象類層次伴生的
UML: Clone Pattern
Why clone?
手上有一個對象,只知道它的抽象類型(確切類型已丟失),要復制這種對象的大量副本,並且副本要和其確切類型一致
可以定義一個 Clonable 抽象基類,以規約 clone 函數,但不是必須的
Clone 模式可從函數 override 的返回值類型的 covariance 協變中受益。VC 2005+ 支持協變