大型專案開發: 隔離 (《大規模C++程式設計》書摘)

Horky發表於2015-07-20

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

實現細節與隔離

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

  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+

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

相關文章