本文首先提出平臺相關代碼造成的兩個問題,然后針對這兩個問題循序漸進依次提出解決方案,在分析了前兩個方案弱點的基礎上,最后著重介紹一種基于多種設計模式的 Linux 平臺相關代碼的解決方案,并給出此方案的 C++ 實現。
Linux 平臺相關代碼帶來的問題
目前市場上存在著許多不同的 Linux 平臺(例如:RedHat, Ubuntu, Suse 等),各大廠商和社區都在針對自己支持的平臺進行優化,為使用者帶來諸多方便的同時也對軟件研發人員在進行編碼時帶來不少問題:
由于程序中不可避免的存在平臺相關代碼(系統調用等),軟件研發人員為了保證自己的產品在各個 Linux 平臺上運行順暢,一般都需要在源代碼中大量使用預編譯參數,這樣會大大降低程序的可讀性和可維護性。
接口平臺無關性的原則是研發人員必須遵循的準則。但是在處理平臺相關代碼時如果處理不當,此原則很有可能被破壞,導致不良的編碼風格,影響代碼的擴展和維護。
本文將針對這兩個問題循序漸進依次提出解決方案。
通過設置預編譯選項來處理平臺相關代碼
通過為每個平臺設置相關的預編譯宏能夠解決 Linux 平臺相關代碼的問題,實際情況下,很多軟件開發人員也樂于單獨使用這種方法來解決問題。
假設現有一動態庫 Results.so,SomeFunction() 是該庫的一個導出函數,該庫同時為 Rhel,Suse,Ubuntu 等三個平臺的 Linux 上層程序服務。(后文例子均基于此例并予以擴展。)
清單 1. 設置預編譯選項示例代碼如下:
// Procedure.cpp void SomeFunction() { //Common code for all linux ...... ...... #ifdef RHEL SpecialCaseForRHEL(); #endif #ifdef SUSE SpecialCaseForSUSE(); #endif #ifdef UBUNTU SpecialCaseForUBUNTU(); #endif //Common code for all linux ...... ...... #ifdef RHEL SpecialCase2ForRHEL(); #endif #ifdef SUSE SpecialCase2ForSUSE(); #endif #ifdef UBUNTU SpecialCase2ForUBUNTU(); #endif //Common code for all linux ...... ...... }
開發人員可以通過設置 makefile 宏參數或者直接設置 gcc 參數來控制實際編譯內容。
例如:
gcc -D RHEL Procedure.cpp -o Result.so -lstdc++ // Use RHEL marco
SpecialCaseForRHEL(),SpecialCaseForSUSE(),SpecialCaseForUBUNTU() 分別在該庫 (Results.so) 的其他文件中予以實現。
圖 1. 清單 1 代碼的結構圖
帶來的問題
SomeFunction() 函數代碼冗余,格式混亂。本例僅涉及三個預編譯選項,但實際情況中由于 Linux 版本眾多并且可能涉及操作系統位數的問題,增加對新系統的支持會導致預編譯選項不斷增多,造成 SomeFunction() 函數結構十分混亂。
新增其他平臺相關接口(例如:增加 SpecialCase3ForRHEL(),SpecialCase3ForSUSE(),SpecialCase3ForUBUNTU),會成倍增加代碼中預編譯宏的數量。
破壞了接口平臺無關性的原則。SpecialCaseForRHEL(),SpecialCaseForSUSE(),SpecialCaseForUBUNTU() 只是同一功能各個平臺的不同實現,屬于封裝內容,不應該分開暴露給調用者。
可見,簡單利用預編譯宏來解決平臺相關代碼產生的問題不是一個好的方法,并沒有解決本文開始提出的兩個問題。后文將通過三個方案依次解決這些問題。
解決方案 1:根據接口平臺無關性原則進行優化
實質上,SpecialCaseForRHEL(),SpecialCaseForSUSE(),SpecialCaseForUBUNTU() 只是同一功能在不同平臺上的實現,SpecialCase2ForRHEL(),SpecialCase2ForSUSE(),SpecialCase2ForUBUNTU() 亦如此。對于調用者,應該遵循接口平臺無關性的原則,使用統一的接口進行調用,這樣才能簡化代碼,使代碼易于維護。
清單 2. 解決方案 1 示例代碼如下:
// Procedure.cpp void SomeFunction() { //Common code for all linux ...... ...... SpecialCase(); //Common code for all linux ...... ...... SpecialCase2(); //Common code for all linux ...... ...... } void SpecialCase() { //Common code for all linux ...... ...... #ifdef RHEL SpecialCaseForRHEL(); #endif #ifdef SUSE SpecialCaseForSUSE(); #endif #ifdef UBUNTU SpecialCaseForUBUNTU(); #endif //Common code for all linux ...... ...... } void Special2Case() { //Common code for all linux ...... ...... #ifdef RHEL SpecialCase2ForRHEL(); #endif #ifdef SUSE SpecialCase2ForSUSE(); #endif #ifdef UBUNTU SpecialCase2ForUBUNTU(); #endif //Common code for all linux ...... ...... }
此方案的優點:
遵循了接口平臺無關性原則,同樣的功能只提供一個接口,每個平臺的實現屬于實現細節,封裝在接口內部。此方案提供了一定的封裝性,簡化了調用者的操作。
此方案的缺點:
預編譯宏泛濫的問題仍然沒有解決,每次新增功能函數,就會成倍增加預編譯宏的數量。同樣每次增加對已有功能新平臺的支持,也會不斷增加預編譯宏的數量。
可見,此方案部分解決了本文開始提出的兩個問題中的一個,但仍有問題需要繼續解決。
解決方案 2: 通過分層對進行優化
換一個角度來思考,可以在二進制層面對平臺相關代碼進行優化。通過對庫的結構進行分層來優化,為每個 Linux 平臺提供單獨的實現庫,并且把調用端獨立提取出來。如下圖所示:
圖 2: 方案 2 的結構圖
此方案單獨將調用端抽象出來,將每個平臺實現端的相關代碼提取出來做成一個單獨的庫(Rhel.so,Suse.so,Ubuntu.so)。SpecialCase() 為同一功能在不同平臺的實現,采用相同接口名。底層庫需要與 Results.so 庫同時發布,也就是說,Redhat 版本發布時需同時打包 Results.so 和 Rhel.so,其他版本亦然。
此方案的優點:
解決了預編譯宏泛濫的問題,通過二進制分層可以將代碼里的所有預編譯選項去掉。遵循了接口平臺無關性的原則。
可見,此方案很好地解決了本文開始提出的兩個問題。
此方案的缺點:
每次發布 Results.so 的時候,底層庫需要伴隨一起發布,導致可執行包文件數量成倍增加。而且很多小程序,小工具的發布往往采取單獨的二進制文件,不允許有底層庫的存在。
解決方案 3: 結合代理模式,橋接模式和單例模式進行優化
現在針對原始問題繼續進行優化,擯棄方案 2 采用的分層手法,在單一庫的范圍內利用 C++ 多態特性和設計模式進行優化。
目標效果:
源代碼中盡可能減少預編譯選項出現的頻率,避免因功能擴展和平臺支持的增加導致預編譯宏數量爆炸。
完全遵循接口平臺無關性的原則。
清單 3. 解決方案 3 調用端示例代碼如下:
// Procedure.cpp void SomeFunction() { //Common code for all linux ...... ...... XXHost::instance()->SpecialCase1(); //Common code for all linux ...... ...... XXHost::instance()->SpecialCase2(); //Common code for all linux ...... ...... }
圖 3:方案 3 的具體實現類圖
此方案結合改進的代理模式(Proxy),橋接模式(Bridge)和單件模式(Singleton),并利用 C++ 封裝、繼承和多態特性予以實現。
IHost 是頂層抽象接口類,聲明了實現端需要實現的功能函數以及調用端需要調用的接口函數。
圖 3 右半部分派生自 IHost 的各個類為實現端,在實現端,
為每個 Linux 系統單獨實現了一個類,相互之間無關聯性。該類實現了該操作系統平臺相關的功能(SpecialCase1() 和 SpecialCase2()),即實現了平臺相關代碼。每個實現類采取單件模式。
Init() 和 terminate() 用來初始化和清理操作。Init() 函數首先創建自己(單件模式),其次創建左側代理類(單件模式,見下段描述),最后將自己的內存地址通過 SetHost() 函數交給左側代理方。
圖 3 左半部分派生自 Host 的各個類為調用端,在調用端,
Host 類做了一層封裝,RhelHost 等派生類為實際的代理者(調用者),每個 Host 的派生類分別代表一種需求(調用方),是右側實現類的一個代理,例如 RhelHost 是 RhelOS 的代理,SuseHost 是 SuseOS 的代理,UbuntuHost 是 UbuntuOS 的代理。每個 Host 的派生類采取單件模式。
Host 類和 HostImp 類之間采用橋接的設計模式,利用 C++ 多態特性,最后通過 HostImp 類調用實現端類的實現。調用端的調用過程如下:
通過 RhelHost 的指針調用 SpecialCase(),由于 RhelHost::SpecialCase() 沒有覆蓋基類虛函數的實現,實際調用的是 Host::SpecialCase()。
Host 的所有調用被橋接到 HostImp 對應的函數。
由 HostImp 類調用確定的實現端的某一個對象的對應實現函數(HostImp 類的 SetHost() 函數記錄了右側類的對象內存地址)。
清單 4. 解決方案 3 框架主要源代碼如下:
// Host.h class IHost { public: virtual void SpecialCase1() = 0; virtual void SpecialCase2() = 0; }; class Host : public IHost { public: virtual ~Host() {}; void setHost(IHost* pHost) { m_pImp->setHost(pHost); } virtual void SpecialCase1() { m_pImp->SpecialCase1(); }; virtual void SpecialCase2() { m_pImp->SpecialCase2(); }; protected: Host(HostImp * pImp); private: HostImp* m_pImp; friend class HostImp; }; class RhelHost : public Host { public: static RhelHost* instance(); private: RhelHost(HostImp* pImp); }; RhelHost * RhelHost::instance() { static RhelHost * pThis = new RhelHost (new HostImp()); return pThis; } RhelHost:: RhelHost (HostImp* pImp) : Host(pImp) { } class RhelOS : public IHost { public: static void init() { static RhelOS me; RhelHost::instance()->setHost(&me); } static void term() { RhelHost::instance()->setHost(NULL); } private: virtual void SpecialCase1() { /* Real Operation */ }; virtual void SpecialCase2() { /* Real Operation */ }; }; // HostImp.h class HostImp : public IHost { private: HostImp(const HostImp&); public: HostImp(); virtual ~HostImp() {}; void setHost(IHost* pHost) { m_pHost = pHost; }; virtual void SpecialCase1() { if(m_pHost != NULL) m_pHost->SpecialCase1() } virtual void SpecialCase2() { if(m_pHost != NULL) m_pHost->SpecialCase2() } private: IHost* m_pHost; };
此方案的優點:
遵循接口平臺無關性原則。此方案將各平臺通用接口提升到最高的抽象層,易于理解和修改。
最大限度地降低預編譯選項在源代碼中的使用,實際上,本例中只需要在一處使用預編譯宏,示例代碼如下:
void Init() { #ifdef RHEL RhelOS::init(); #endif #ifdef SUSE SuseOS::init(); #endif #ifdef UBUNTU UbuntuOS::init(); #endif }
源代碼其他地方不需要添加預編譯宏。
實現端和調用端都通過類的形式進行封裝,而且實現端類和調用端類都可以自己單獨擴展,完成一些各自需要完成的任務,所要保持一致的只是接口層函數。擴展性和封裝性很好。
由此可見,此方案很好地解決了本文開始提出的兩個問題,而且代碼結構清晰,可維護型好。
接下來對上述源代碼繼續進行優化。上例 SuseHost/UbuntuHost/SUSEOS/UBUNTUOS 等類的實現被略去,實際上這些類的實現與 RhelHost 和 RHELOS 相似,可以利用宏來進一步優化框架代碼結構。
清單 5. 解決方案 3 框架主要源代碼優化:
#define HOST_DECLARE(name) class ##nameHost : public Host { public: static ##nameHost* instance(); private: ##nameHost(HostImp* pImp); }; #define HOST_DEFINE(name) ##nameHost* ##nameHost::instance() { static ##nameHost* pThis = new ##nameHost(new HostImp()); return pThis; } ##nameHost::##nameHost(HostImp* pImp) : Host(pImp) { } #define HOST_IMPLEMENTATION(name) class ##name##OS : public IHost { public: static void init() { static ##name##OS me; ##nameHost::instance()->setHost(&me); } static void term() { ##nameHost::instance()->setHost(NULL); } private: virtual void SpecialCase1(); virtual void SpecialCase2(); };
使用三個宏來處理相似代碼。至此,優化完成。從源代碼角度來分析,作為實現端的開發人員,只需要三步就可以完成操作:
調用 init() 函數;
使用 #define HOST_IMPLEMENTATION(name);? ? 例如:#define HOST_IMPLEMENTATION(DEBIAN)
實現 DEBIAN::SpecialCase1() 和 DEBIAN::SpecialCase2()。? ? 作為調用端的開發人員,也只需要三步就可以完成操作:
使用 #define HOST_DECLARE(name) 進行聲明;? ? 例如 : #define HOST_DECLARE(DEBIAN)
使用 #define HOST_DEFINE(name) 進行定義;? ? 例如: #define HOST_DEFINE (DEBIAN)
調用接口。? ? 例如: DEBIANHost::instance()->SpecialCase1();DEBIANHost::instance()->SpecialCase2();
可見,優化后方案的代碼清晰,不失為一個良好的平臺相關代碼的解決方案。
由于調用端和實現端往往需要傳遞參數,可以通過 SpecialCase1() 函數的參數來傳遞參數,同樣的這個參數類可以通過橋接的方式予以實現,本文不再詳述,讀者可以自己嘗試。
對方案 3 的擴展
擴展 1:對單一操作系統多對多的擴展
對于方案 3 的實現,也許有讀者會問,調用端只需要 Host 類不需要其派生類即可完成方案 3 中的功能,的確如此,因為方案 3 中的代理類一直是一對一的關系,即 RhelHost 代理 RhelOS,Redhat 下只存在這一對一的關系。但是實際情況下,單一系統下很可能存在多對多的關系。
例如,在單一操作系統中,可能需要同時實現多種風格的窗口。實際上,變成了多對多的代理關系。
圖 4:單一操作系統不同 c 風格窗口的實現類圖
Style1Host 代理 Style1Dialog,Style2Host 代理 Style2Dialog,Style3Host 代理 Style3Dialog,三個窗口同時并存,也就是說左側三個實現類的實例和右側三個代理類的實例同時存在。可見,方案 3 的設計模式擴展性良好,實現端和調用端都可以在遵循接口不變性的情況下單獨擴展自己的功能。
擴展 2:對多操作系統的擴展
方案 3 不僅可以針對 Linux 平臺相關代碼進行處理,還可以擴展到其他諸多場合。例如,現在的程序庫往往需要針對多個操作系統,包括 Windows, Linux, Mac。每個操作系統往往使用不同的 GUI 庫,這樣在實現窗口操作的時候必然涉及到平臺相關代碼。同樣可以用方案 3 予以實現。
圖 5:多操作系統的實現類圖
總結
本文開始提出平臺相關代碼造成的兩個問題,接著循序漸進提出解決方案。在分析了常用的設置預編譯選項方法的利弊的基礎上,提出了一種新的利用 C++ 多態特性,結合使用代理模式,橋接模式和單件模式處理平臺相關代碼的方案,并對這一方案予以擴展,給讀者提供了一種新的高效的處理平臺相關代碼的方法。
?
評論
查看更多