企業(yè)網(wǎng)站的建立視頻廣州各區(qū)風險區(qū)域最新動態(tài)
本章內(nèi)容
本章涵蓋了一些與C++API設(shè)計相關(guān)的設(shè)計模式和慣用法。
“設(shè)計模式(Design Pattern)”表示軟件設(shè)計問題的一些通用解決方案。該術(shù)語來源于《設(shè)計模式:可復用面向?qū)ο筌浖幕A(chǔ)》(Design Patterns: Elements of Reusable Object-Oriented Software)
本書不會涵蓋所有模式,只討論一些和API設(shè)計有關(guān)的。
還會涉及一些C++的慣用法,它們并非是真正的通用設(shè)計模式,但卻是C++API設(shè)計的重要技巧。
本章討論的技巧如下:
1. Pimpl
Pimpl 意思是 Pointer to Implementation,指向?qū)崿F(xiàn)的指針。
(Pimpl 不是嚴格意義上的“設(shè)計模式”,而是受制于C++語法特定限制的變通方案,可以看作是 橋接 設(shè)計模式的一種特例)
它主要解決的是C++語法中的一個問題:類的private成員其實只在內(nèi)部使用,但是在定義時還需要寫在.h文件中公開。因此,Pimpl 的做法是只在.h中定義一個指向?qū)崿F(xiàn)的指針,那就可以將一些private成員放在.cpp中的實現(xiàn)中,這樣就在.h文件中隱藏了private成員。
舉例:
使用Pimpl的方法
舉例代碼:
.h文件中:
class AutoTimer
{
public:explicit AutoTimer();~AutoTimer();
private:class Impl;Impl* mImpl;
}
隨后,在.cpp中可以定義Impl
類并實現(xiàn)具體的邏輯。
一個值得考慮的設(shè)計問題是:Impl
類中放置多少邏輯?有以下選擇:
- 僅私有變量
- 私有變量+私有方法
- 公有類的所有方法。(而共有類的方法只是對
Impl
類中方法的簡單包裝)
每種選擇都適應于不同情況。一般情況下推薦 2 。
另外,使用 Pimpl 時需要注意:
- virtual 函數(shù)不能放在
Impl
類中,否則公有類的子類無法繼承。 Impl
類可能還需要一個公有類的指針以方便其調(diào)用公有類的方法。
使用Pimpl的類的復制
使用 Pimpl 的類無法進行默認的復制,因為復制出的對象會和原對象指向同一個Impl
類變量。
這個問題有兩種方法解決:
- 禁止復制
- 顯示定義復制語義
Pimpl與智能指針
使用 Pimpl 時容易犯錯的一點是:構(gòu)造時忘記分配它,析構(gòu)時忘記銷毀它。
為此,可以采用 智能指針 或者 作用域指針。
Pimpl的優(yōu)點
- 信息隱藏。
- 降低耦合
- 加速編譯
- 更好的二進制兼容性。(就算
Impl
類實現(xiàn)發(fā)生變化,公用類的對象也不會改變二進制數(shù)據(jù)) - 惰性分配(可以選擇只在需要時分配)
Pimpl的缺點
- 額外的分配與銷毀
Impl
類對象會增加性能開銷。 - 給開發(fā)者帶來了不便:很多函數(shù)調(diào)用時需要加上
mImpl->
,如果Impl
類需要調(diào)用公有類的方法也需要通過指針。 - 編譯器不能再檢查const:
Impl
類的成員更改,公有類的const無法檢查出。
2. 單例
“單例”設(shè)計模式確保一個類僅存在一個實例,并提供唯一的全局訪問點。
它可以看作是一種更優(yōu)雅的全局變量,但是相比全局變量有一些優(yōu)點:
- 確保這個類只能創(chuàng)建一個實例。
- 控制對象的分配與銷毀。
- 可以支持線程安全。
- 避免污染全局命名空間。
其基本實現(xiàn)很簡單,而本篇重點討論的是:
- 如何更健壯地實現(xiàn)。
- 它也有些缺點,但很多人都有濫用單例的趨向。為此這里也提供一些替代方法。
在C++中實現(xiàn)單例
其基本實現(xiàn)很簡單:
class Singleton
{
public:static Singleton &GetInstance();
};
Singleton &Singleton::GetInstance()
{static Singleton instance;return instance;
}
有一些做法可以增加健壯性:
- 聲明私有默認構(gòu)造函數(shù):防止用戶創(chuàng)建新的實例。
- 聲明私有復制構(gòu)造函數(shù)和賦值操作:防止用戶復制。
- 聲明私有析構(gòu)函數(shù):防止用戶刪除。
GetInstance()
返回引用而非指針:防止用戶刪除。
單例的線程安全
上面的 GetInstance()
并非線程安全的。
常規(guī)的處理方式是加互斥鎖。但這樣會增加開銷。
要優(yōu)化此類激進的加鎖行為,可以采用DCLP(Double Check Locking Pattern),即加互斥鎖前先判斷instance是否存在。
但是DCLP不能保證任何編譯器和處理器下都能正常工作。
所以,也許你不應該嘗試保證GetInstance()
是線程安全的(畢竟對使用C++這樣對并發(fā)缺乏內(nèi)在支持的語言來說,實現(xiàn)線程安全總會遇到這些困難)。如果你真的需要它線程安全并且性能最高,可以考慮避免惰性實例化模型(即不要在需要他時再實例化),比如:
- 靜態(tài)初始化:在cpp文件中main函數(shù)調(diào)用之前調(diào)用
GetInstance()
。 - 顯式API初始化:在一開始就調(diào)用
GetInstance()
,調(diào)用時可以加互斥鎖。而GetInstance()
的內(nèi)部就不用加互斥鎖了。
替代方案:依賴注入
初始化時傳入需要的實例的指針,而不是內(nèi)部再使用GetInstance()
獲得實例。
替代方案:單一狀態(tài)
假設(shè)狀態(tài)的初始化不需要控制,或者不需要使用單例對象存儲,那么就可以使用“單一狀態(tài)”,即:
類本身不保持是單例,但是其所有成員(或者說“狀態(tài)”)都是static。
替代方案:會話上下文
《設(shè)計模式》作者指出,單例有可能導致拙劣的設(shè)計。使用時候需要思考,“單例”是否真的是正確的模式?
需求是會變的,未來有些對象可能會需要支持多個實例。
因此,需要盡早考慮引入 “會話(session)” 或 “上下文(context)” 的概念。這是在強調(diào):使用單一的實例維護所有相關(guān)的狀態(tài),而非使用多個單例。
3. 工廠模式
工廠模式是關(guān)于創(chuàng)建的設(shè)計模式,本質(zhì)上是構(gòu)造函數(shù)的泛化,可以回避C++構(gòu)造函數(shù)的限制:
- 沒有返回值。這樣無法返回錯誤等其他信號。
- 命名限制。這樣相同參數(shù)的構(gòu)造函數(shù)只能有一個。
- 靜態(tài)綁定創(chuàng)建。這樣無法在運行時動態(tài)決定類型。
- 不允許虛構(gòu)造函數(shù)。限制同上。
從使用層面上看,工廠方法僅是一個普通的方法,調(diào)用時返回對應類的實例:
class RendererFactory
{
public:IRenderer* CreateRenderer(string type)
}
但這里的 IRenderer 是一個抽象基類(Abstract Base Class,簡稱ABC)。抽象基類是包含純虛函數(shù)的類,不能被實例化。(另外要注意,抽象基類的析構(gòu)函數(shù)需要聲明為虛的)。
這樣,用戶可以使用參數(shù)動態(tài)決定要創(chuàng)建的類型。
另外作為擴展,可以讓工廠類提供注冊函數(shù),這樣用戶可以自己添加新的類型
static void RegisterRender(string type, CreateCallback cb);
4. API包裝器
基于另一組API來包裝接口是一項常見的API設(shè)計任務(wù)。比如,你在維護一個遺留的代碼庫,相比重構(gòu)代碼,你更愿意封裝一套新的,更簡潔的API,以隱藏所有的底層遺留代碼。
下面,按照包裝器層和原始接口的差異程度遞增地劃分:
4.1 代理(Proxy)
一對一地,將函數(shù)調(diào)用轉(zhuǎn)發(fā)到具有相同形式的另一個接口。
案例:
- 實現(xiàn)原始對象的惰性實例。
- 實現(xiàn)對原始對象的訪問控制。
- 支持 “調(diào)試” 模式或者 “演習(DryRun)” 模式。
- 保證原始對象的線程安全
- 可以讓多個代理對象共享相同的原始對象。
- 應對原始對象將來被修改的情況。
4.2 適配器
一對一地,將接口轉(zhuǎn)換為一個兼容但是不完全相同的另一個接口。
優(yōu)點:
- 可以轉(zhuǎn)換數(shù)據(jù)類型。
- 強制API始終保持一致性。
- 包裝API的依賴庫
4.3 外觀模式
外觀模式能夠為一組類提供簡化的接口。它實際上定義了一個更高層次的接口,使得底層類更易于使用且對用戶隱藏。
(一個例子:用一個“酒店助手類”簡化了“預定房間”、“預定晚餐”、“預定出租車”等事務(wù)。)
用途:
- 隱藏遺留代碼。
- 創(chuàng)建更便捷的API。
- 支持簡化功能或替代功能的API。
5. 觀察者模式
觀察者模式為了解決這樣的問題:
實現(xiàn)復雜的任務(wù)通常需要多個對象一起合作完成。為了讓A可以調(diào)用B,較為簡單的方法就是A.cpp包含B.h,但是這樣產(chǎn)生了編譯時依賴,迫使想要復用A時也必須引入B。
觀察者模式就是 “發(fā)布/訂閱” 范式的一個具體實例。
實現(xiàn)觀察者模式的典型做法是引入兩個概念:
- Subject,主體,也就是發(fā)布者。
- Observer,觀察者,也就是訂閱者。
代碼上,Subject僅知道Observer接口類即IObserver
,他將維護IObserver
列表
class IObserver
{virtual void Update() = 0;
}class ISubject
{
std::vector<IObserver*> ObserverList
}
隨后,觀察者通過繼承IObserver
來實現(xiàn)觀察者對象。
然后,在使用時,Subject就可以訂閱(Subscribe)若干Observer,并在需要的時候通知(Notify)它們。
這樣,Subject和Observer就沒有編譯時依賴關(guān)系,它們的關(guān)系是運行時動態(tài)創(chuàng)建的。