大型項目開發: 隔離 (《大規模C++程序設計》書摘)

書中第六章 隔離。 主要在撰述什麼需要定義在頭文件?什麼應當移到編譯單元中?
核心仍然是先區分接口定義與實現細節。實現細節的改變會導致客戶代碼的重新編譯,從邏輯上也表示與客戶代碼間可能存在着強耦合。

實現細節與隔離

主要考察以下實現細節,它們會在接口中引入實現細節,也是需要考慮進行隔離的內容:

  1. 繼承
  2. 分層
    簡單的說就是類的成員中有另一個類的實例時,如Foo mFoo. 這個類就會依賴於Foo的定義。而轉爲持有地址時,即將關係從HasA改爲HoldA時,就不存在這個問題。也就是定義爲Foo* mFoo;或Foo& mFoo; 這也是Google C++ Coding Style曾經就減少頭文件依賴建議過的方式,後來則去掉了這項建議,改爲:”不要爲了使用前置聲明,將成員變量改爲指針類型”, 因爲它反而增加了邏輯上的複雜度,比如額外的判空處理。
  3. 內聯函數
  4. 私有成員
  5. 保護成員
  6. 編譯器生成的默認實現函數,如拷貝。
  7. 包含指令,即頭文件的包含。
  8. 默認參數
  9. 枚舉
    在一些大型項目中,一些存有基本枚舉類型的頭文件,最後變成沒人敢改,而更願意新增頭文件。其實還不如放到具體的域或類中定義。

後面作者對各個細節推薦一些手法,相對比較簡單。後面則介紹了幾個常用手法:

  1. 協議類(接口類)
  2. Opaque Pointer和PIMPL
  3. Wrapper (封裝器), 即引入中間層。

過程接口

考慮到上層代碼對底層的操作需求,作者提出了過程接口(The Procedural Interface),可以結合常見的API來理解,它是一組函數的集合,出現在組件的頂部,並將功能的一個子集暴露給用戶。作者概括了編程接口的要求:

  1. 接口必須提供必要的功能來操縱底層系統。
  2. 接口一定不能暴露專屬的實現細節。
  3. 底層組織的變化必須與客戶端程序相隔離。
  4. 與該接口相關的開銷一定不能過大。

在實現方式上,以面向對象的Wrapper來實現這樣的需求最佳的,而過程接口將針對無法簡單使用獨立的封裝類來實現的系統。其實一個大型系統也是可以拆分出不同的領域,分別以Wrapper的形式來實現的。可以對比WebView的接口,以及Blink中的web層次。
書中主要是探討了針對所持有對象的操作。上面也提到的Opaque Pointer,還特別說明了Handle(句柄)模式來管理動態分配的對象。

一個過程接口既不是面向對象的也不是特別美觀,但它確有一個很大的優點:過程接口總是能夠用於將大系統的組織與客戶端程序相隔離–即使在設計的早期階段並沒有考慮這樣的接口。

隔離或不隔離

隔離會引入一些開銷,選擇是否進行隔離的常見原因包括:

  1. 暴露 (被使用的範圍,或者扇入)
  2. 訪問數據的性能
  3. 創建對象的性能
  4. 開發成本 (在沒有明確理由的情況強行隔離,會引入額外的開發工作)
  5. 組件的數量 (可能會新增組件,增加維護成本)
  6. 組件的複雜性 (引入新的複雜度,導致難以理解和維護)

作者提供兩套經驗值供決策時參考(中文編譯的圖表不太嚴謹,第5章有圖標錯,這裏明明是兩個表,卻合成了一個表。)。

訪問的相對開銷

  1. 內聯函數傳遞值 : 1
  2. 內聯函數傳遞指針 : 2
  3. 非內聯函數,非虛函數 : 10
  4. 虛函數機制 : 20

創建相對於單獨分配的成本

  1. 自動 (棧上) : 1.5
  2. 動態 (堆上) : 100+

作者最後討論隔離決策時,建議是否進行隔離取於被使用的範圍,性能要求的高低,以及成員函數的大小(是否輕量級)。性能要求高不要隔離,輕量級的實現也不需要隔離。其實就是隔離本身會引入開銷,如果爲了隔離引入的開銷過式,或者引入更不穩定的複雜度,就不要急於隔離。而對於大型、廣泛使用的對象則要儘早隔離。

發佈了220 篇原創文章 · 獲贊 29 · 訪問量 174萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章