書中第六章 隔離。 主要在撰述什么須要定義在頭文件?什么應當移到編譯單元中?
核心仍然是先區分接口定義與實現細節。實現細節的改變會導致客戶代碼的又一次編譯,從邏輯上也表示與客戶代碼間可能存在着強耦合。
實現細節與隔離
主要考察下面實現細節。它們會在接口中引入實現細節。也是須要考慮進行隔離的內容:
- 繼承
- 分層
簡單的說就是類的成員中有還有一個類的實例時,如Foo mFoo. 這個類就會依賴於Foo的定義。而轉為持有地址時,即將關系從HasA改為HoldA時,就不存在這個問題。也就是定義為Foo* mFoo;或Foo& mFoo; 這也是Google C++ Coding Style以前就降低頭文件依賴建議過的方式,后來則去掉了這項建議,改為:”不要為了使用前置聲明,將成員變量改為指針類型”, 由於它反而添加了邏輯上的復雜度,比方額外的判空處理。 - 內聯函數
- 私有成員
- 保護成員
- 編譯器生成的默認實現函數,如拷貝。
- 包括指令,即頭文件的包括。
- 默認參數
- 枚舉
在一些大型項目中。一些存有基本枚舉類型的頭文件。最后變成沒人敢改,而更願意新增頭文件。事實上還不如放到詳細的域或類中定義。
后面作者對各個細節推薦一些手法。相對照較簡單。后面則介紹了幾個經常使用手法:
- 協議類(接口類)
- Opaque Pointer和PIMPL
- Wrapper (封裝器), 即引入中間層。
過程接口
考慮到上層代碼對底層的操作需求。作者提出了過程接口(The Procedural Interface),能夠結合常見的API來理解,它是一組函數的集合。出如今組件的頂部,並將功能的一個子集暴露給用戶。
作者概括了編程接口的要求:
- 接口必須提供必要的功能來操縱底層系統。
- 接口一定不能暴露專屬的實現細節。
- 底層組織的變化必須與client程序相隔離。
- 與該接口相關的開銷一定不能過大。
在實現方式上,以面向對象的Wrapper來實現這種需求最佳的。而過程接口將針對無法簡單使用獨立的封裝類來實現的系統。
事實上一個大型系統也是能夠拆分出不同的領域。分別以Wrapper的形式來實現的。能夠對照WebView的接口。以及Blink中的web層次。
書中主要是探討了針對所持有對象的操作。
上面也提到的Opaque Pointer。還特別說明了Handle(句柄)模式來管理動態分配的對象。
一個過程接口既不是面向對象的也不是特別美觀。但它確有一個非常大的長處:過程接口總是能夠用於將大系統的組織與client程序相隔離–即使在設計的早期階段並沒有考慮這種接口。
隔離或不隔離
隔離會引入一些開銷。選擇是否進行隔離的常見原因包括:
- 暴露 (被使用的范圍。或者扇入)
- 訪問數據的性能
- 創建對象的性能
- 開發成本 (在沒有明白理由的情況強行隔離。會引入額外的開發工作)
- 組件的數量 (可能會新增組件,添加維護成本)
- 組件的復雜性 (引入新的復雜度。導致難以理解和維護)
作者提供兩套經驗值供決策時參考(中文編譯的圖表不太嚴謹,第5章有圖標錯。這里明明是兩個表,卻合成了一個表。)。
訪問的相對開銷
- 內聯函數傳遞值 : 1
- 內聯函數傳遞指針 : 2
- 非內聯函數,非虛函數 : 10
- 虛函數機制 : 20
創建相對於單獨分配的成本
- 自己主動 (棧上) : 1.5
- 動態 (堆上) : 100+
作者最后討論隔離決策時,建議是否進行隔離取於被使用的范圍,性能要求的高低,以及成員函數的大小(是否輕量級)。性能要求高不要隔離。輕量級的實現也不須要隔離。
事實上就是隔離本身會引入開銷。假設為了隔離引入的開銷過式,或者引入更不穩定的復雜度。就不要急於隔離。而對於大型、廣泛使用的對象則要盡早隔離。