C語言的模塊化設(shè)計和面向?qū)ο缶幊蘝第1頁
C語言的模塊化設(shè)計和面向?qū)ο缶幊蘝第2頁
C語言的模塊化設(shè)計和面向?qū)ο缶幊蘝第3頁
C語言的模塊化設(shè)計和面向?qū)ο缶幊蘝第4頁
C語言的模塊化設(shè)計和面向?qū)ο缶幊蘝第5頁
全文預(yù)覽已結(jié)束

下載本文檔

版權(quán)說明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請進(jìn)行舉報或認(rèn)領(lǐng)

文檔簡介

C語言的模塊化設(shè)計和面向?qū)ο缶幊谭诸悾篊面向?qū)ο?C++/Java2011-03-0221:04108人閱讀評論(0)收藏舉報來自網(wǎng)易杭州研發(fā)技術(shù)總監(jiān)“云風(fēng)”BLOG的幾篇面向?qū)ο笤O(shè)計的文章C語言對模塊化支持的欠缺繼續(xù)昨天的話題。隨便列些以后成書可能會寫的東西。既然書的主題是:怎樣構(gòu)建一個(稍具規(guī)模的)軟件。且我選擇用C為實現(xiàn)工具來做這件事情。就不得不談?wù)Z言還沒有提供給我們的東西。模塊化是最高原則之一(在《Unix編程藝術(shù)》一書中,Unix哲學(xué)第一條即:模塊原則),我們就當(dāng)考慮如何簡潔明快的使用C語言實現(xiàn)模塊化。除開C/C++,在其它現(xiàn)在流行的開發(fā)語言中,缺少標(biāo)準(zhǔn)化的模塊管理機(jī)制是很難想象的。但這也是C語言本身的設(shè)計哲學(xué)決定的:把盡可能多的可能性留給程序員。根據(jù)實際的系統(tǒng),實際的需要去定制自己需要的東西。對于巨型的系統(tǒng)(比如Windows這樣的操作系統(tǒng)),一般會考慮使用一種二進(jìn)制級的模塊化方案。由模塊自己提供元信息,或是使用統(tǒng)一的管理方案(比如注冊表)。稍小一點的系統(tǒng)(我們通常開發(fā)接觸到的),則會考慮輕量一些的源碼級方案。首先要考慮的往往是模塊的依賴關(guān)系和初始化過程。依賴關(guān)系可以放由鏈接器或加載器來解決。尤其在使用C語言時,簡單的靜態(tài)庫或動態(tài)庫,都不太會引起大的麻煩。C++則不然,C++的某些特性(比如模板類靜態(tài)成員的構(gòu)造)必須對早期只供C語言使用的鏈接器做一些增強(qiáng)。即使是精心編寫的C++庫,也有可能出現(xiàn)一些意外的bug。這些bug往往需要對編譯,鏈接,加載過程很深刻的理解,才能查出來。注:我并不想以此來反對使用C++做開發(fā)。我們需要著重管理的,是模塊的初始化過程。對于打包在一起的一個庫(例如glibc,或是msvcrt),會在加載時有初始化入口,以及卸載時有結(jié)束代碼。我想說的不是這個,而是我們自己內(nèi)部拆分的更小的模塊的相互依賴關(guān)系。誰先初始化,誰后初始化,這是一個問題。在C++的語言級解決方案中,使用的是單件模塊。要么由鏈接器決定以怎樣的次序來生成初始化代碼,這,通常會因為依賴關(guān)系和實際構(gòu)造次序不同而導(dǎo)致bug(注:我在某幾本C++書中都見過,待核實。自己好久不寫C++也沒有實際的錯誤例子);要么使用惰性初始化方案。這個惰性初始化也不是萬能的,并且有些額外的開銷。(多線程環(huán)境中尤其需要注意)我使用C語言做初期設(shè)計的時候,采用的是一種足夠簡單的方法。就是,以編碼規(guī)范來規(guī)定,每個模塊必須存在一個初始化函數(shù),有規(guī)范的名字。比如foo模塊的初始化入口叫intfoo_init()規(guī)定:凡使用特定模塊,必須調(diào)用模塊初始化函數(shù)。為了避免模塊重復(fù)初始化,初始化函數(shù)并不直接調(diào)用,而是間接的。類似這樣:mod_using(foo_init);mod_using負(fù)責(zé)調(diào)用初始化函數(shù),并保證不重復(fù)調(diào)用,也可以檢查循環(huán)依賴。在這里,我們還約定了初始化成功于否的返回值。(在我們的系統(tǒng)中,返回0表示正確,1表示失敗)然后定義了一個宏來做這個使用。#defineUSING(m)if(mod_using(m##_init,#m)){return1;}注:我個人反對濫用宏。也盡可能的避免它。這里使用宏,經(jīng)過了慎重的考慮。我希望可以有一個代碼掃描器去判斷我是否漏掉了模塊初始化(可能我使用了一個模塊,但忘記初始化它)。宏可以幫助代碼掃描分析器更容易實現(xiàn)。而且,使用宏更像是對語言做的輕微且必要的擴(kuò)展。這樣,我的系統(tǒng)中模塊模塊的實現(xiàn)代碼最后,都有一個init函數(shù),里面只是簡單的調(diào)用了USING來引用別的模塊。例如:#include"module.h"/*我個人偏愛把module.h的引入放在源文件最后,初始化入口之前。它里面之定義了USING宏,以及相關(guān)管理函數(shù)。這樣做是為了避免在代碼的其它地方去引入別的模塊。*/intfoo_init(){USING(memory);//引用內(nèi)存管理模塊USING(log);//引用log模塊return0;}至于模塊的卸載,大部分需求下是不需要的。今天在這里就不論證這一點了。淺談C語言中模塊化設(shè)計的范式今天繼續(xù)談模塊化的問題。這個想慢慢寫成個系列,但是不一定連續(xù)寫?;臼窍肫饋砹耍驼睃c思路出來。主要還是為以后集中整理做點鋪墊。我們都知道,層次分明的代碼最容易維護(hù)。你可以輕易的換掉某個層次上的某個模塊,而不用擔(dān)心對整個系統(tǒng)造成很大的副作用。層次不清的設(shè)計中,最糟糕的一種是模塊循環(huán)依賴。即,分不清兩個模塊誰在上,誰在下。這個時候,最容易牽扯不清,其結(jié)果往往是把兩者看做一體去維護(hù)算了。這里面還涉及一些初始化次序等繁雜的細(xì)節(jié)。其次,就是越層的模塊聯(lián)系。當(dāng)模塊A是模塊B的上層,而模塊B又是模塊C的上層,這個時候,讓模塊C對模塊A可見,在模塊A中有對C導(dǎo)出接口的直接調(diào)用,對于清晰的設(shè)計是很忌諱的一件事。雖然,我們很難完全避免這個問題,去讓A對C的調(diào)用完全通過B。但通常應(yīng)盡力為之。(注:以后寫書的話,我爭取補(bǔ)充一些實際的例子來說明)不過,對語言不原生支持的數(shù)據(jù)類型,以及基礎(chǔ)設(shè)施,但卻有必要創(chuàng)造出來給系統(tǒng)用的??梢杂行├狻1热鐑?nèi)存管理,log管理,字符串(C語言用原始庫函數(shù)管理比較麻煩)等等,我們可能以基礎(chǔ)模塊的形式提供。但卻可能被不同層次的模塊直接使用。但,上到一定層次后,還是需要去隱藏它們的。下面來一點更實際的分析。以C語言為例,由于C語言缺乏namespace的原生支持,我們通常給api加上統(tǒng)一前綴來區(qū)分。這倒也不麻煩。那么模塊A看起來就是一堆'A_xxxxx'為名字的方法。我個人主張單個模塊不宜過大,在實現(xiàn)時適合放在同一個.c文件里即可。通常,一個模塊會圍繞一類對象處理。這些對象可以用整數(shù)handle來表示,也可以用一個特定類型的對象指針。兩種方案各有千秋。先來談對象指針的方案。一個模塊A的接口描述文件很可以是這樣的(希望以后能補(bǔ)上更現(xiàn)實的代碼):#ifndef_A_h#define_A_hstructA;structB;structA*A_create(void);voidA_release(structA*self);voidA_bind(structA*self,structB*b);voidA_commit(structA*self);voidA_update(void);intA_init(void);#endif這里,我們定義了A這種數(shù)據(jù)類型。我個人反對用typedef或宏來減少代碼輸入。除非有特別的理由,都寫上struct前綴,而不是定義出新類型。尤其是在較底層的模塊設(shè)計時更是如此。在接口描述時,structA的細(xì)節(jié)是絕對不應(yīng)該暴露出來的,它的數(shù)據(jù)結(jié)構(gòu)應(yīng)該僅存在于實現(xiàn)的文件a.c中。關(guān)于A的接口通常分兩類,一類是對structA*做一些處理的,那么就讓第一個參數(shù)傳入self指針。這相當(dāng)于C++的this指針。比如上例中的A_commit;另一類接近于C++類的靜態(tài)成員函數(shù),通常用于對這一類對象全部做一個處理,如A_update。注:我無意用C去模擬C++,但基于一類數(shù)據(jù)類型做一些處理的方法,對于C,這樣的寫法也是一個常規(guī)的范式而已。至于面向?qū)ο蟮仍跇?gòu)建復(fù)雜系統(tǒng)時常用到的方法,以后我會談?wù)勎易约撼S玫牧硪恍┓妒健;蛟S像C++,也可以不像。怎么寫更好,是個見任見智的問題。不用過于拘泥。這里的例子中,我們還提到了另一個數(shù)據(jù)類型B。顯然,它是放在B模塊中的。我們通常不會在a.h中去includeb.h,而只是聲明一下structB。(對于C語言來說,這并不必要,但寫上是個好習(xí)慣)。這是因為,如果B是位于A之下的模塊,既在A模塊的實現(xiàn)中,會用到B的方法,我們通常不會讓用到A模塊的人,可以看見B的接口。包含a.h的同時隱式包含b.h就是不必要的了。從范例代碼中,我們可以猜想,structA是對structB的某種封裝,可以通過對A的操作,間接操作到其中的B類型。在A的模塊初始化A_init中一定就會初始化B了。如果是這樣,B的層次就位于A之下。往往structB中還會保留一個structA類型的引用。首先,我們應(yīng)該盡力避免這種情況。即:位于下層的B應(yīng)該對上層的A一無所知是最好的。如果在B模塊中必須出現(xiàn)structA,那么我們應(yīng)該至少保證,僅僅是structA*,一個引用,而絕對不能出現(xiàn)任何對A模塊內(nèi)接口的調(diào)用。不要認(rèn)為使用巧妙的方法,繞過循環(huán)依賴初始化問題就夠了。這應(yīng)該是一個設(shè)計原則,不要去違反。btw,草率的接口設(shè)計往往是日后系統(tǒng)脆弱的根源。圖一時之快,隨意暴露一些接口,或是自以為聰明的用一些“巧妙”的方法,甚至是語法糖來繞過設(shè)計原則,都是很危險的。一個常見的難處理的問題是:如果structA和structB相互有雙向引用。怎樣建立這個引用關(guān)系?這個建立的過程,到底是A的方法,還是B的方法?我的答案是,誰在上層,就是誰的方法。但是A和B相互都看不見內(nèi)部數(shù)據(jù)布局的細(xì)節(jié),讓B的內(nèi)部對A類型做一個引用,比如也需要從B模塊中暴露一個接口出來。這個接口,可能僅供A使用。在這個例子里,就是僅供A_bind這個方法去使用。如果是C++,我們或許會采用friend。也可能使用其它一些技巧。反正C++里可以挖掘的語法太多了。但C怎么辦?下面給個我自己的方案。原本,我們在B中導(dǎo)出的api是這樣的:voidB_set_A(structB*self,structA*a);現(xiàn)在寫成:structi_A;voidB_set_A(structB*self,structi_A*a);在b.c的實現(xiàn)中,加一個函數(shù)用于structi_A*到structA*的轉(zhuǎn)換。staticinlinestructA*A(structi_A*a){return(structA*)a;}然后在a.c的實現(xiàn)中,加一個類似函數(shù)用于轉(zhuǎn)換structA*到structi_A*。這樣,在a.c之外,其它模塊因為不能得到任何structi_A類型,而不會錯誤的使用B_set_A這個接口了。我所偏愛的C語言面向?qū)ο缶幊谭妒矫嫦驅(qū)ο缶幊滩皇倾y彈。大部分場合,我對面向?qū)ο蟮氖褂梅浅V?jǐn)慎,能不用則不用。相關(guān)的討論就不展開了。但是,某些場合下,采用面向?qū)ο蟮拇_是比較好的方案。比如UI框架,又比如3d渲染引擎中的場景管理。C語言對面向?qū)ο缶幊滩]有原生支持,但沒有原生支持并不等于不適合用C寫面向?qū)ο蟪绦?。反而,我們對具體實現(xiàn)方式有更多的選擇。大部分用C寫面向?qū)ο蟪绦虻某绦騿T受C++影響頗深。企圖用宏模擬出一個常見C++編譯器已經(jīng)實現(xiàn)的對象模型。于我愚見,這并不是一個好的方向。C++的對象模型,本質(zhì)上是為了追求實現(xiàn)層的性能,并直接體現(xiàn)出來。就有如在C++中被濫用的inline,的確有效,卻破壞了分離原則。C++的繼承是過緊的耦合。我所理解的面向?qū)ο?,是讓不同的?shù)據(jù)元有共同的操作方式,適合成組的處理。根據(jù)操作方式的不同,我們會對數(shù)據(jù)元做不同的分組。一個數(shù)據(jù)可能出現(xiàn)在這個組里,也可以出現(xiàn)在那個組里。這取決于你從不同的方面提取的共性。這些可供統(tǒng)一操作的共性稱之為接口(Interface),接口在C語言中,表現(xiàn)為一組函數(shù)指針的集合。放在C++中,即為虛表。我所偏愛的面向?qū)ο髮崿F(xiàn)方式(使用C語言)是這樣的:若有一組數(shù)據(jù),我們需要讓他們看起來都有一種叫作foo的共性。把符合這樣的數(shù)據(jù)都稱為foo_object。通常,我們會有如下api去操控foo_object。structfoo_object;structfoo_object*foo_create();voidfoo_release(structfoo_object*);voidfoo_dosomething(structfoo_object*);在具體實現(xiàn)時,會在一個叫foo.c的實現(xiàn)文件中,定義出foo_object結(jié)構(gòu),里面有一些foo_dosomething所需的數(shù)據(jù)成員。但是,以上還不能滿足要求。因為,我們會有不同的數(shù)據(jù),他們只是表現(xiàn)出foo_object某些方面的特性。對于不同的數(shù)據(jù),它們在dosomething時,實際所做的操作也有所區(qū)別。這時,我們需要定義出一個接口,供foo.c內(nèi)部使用。那么,以上的頭文件就需要做一些修改,把接口i_foo的定義加進(jìn)去,并修改create函數(shù)。structi_foo{void(*foobar)(void*);};structfoo_object*foo_create(structi_foo*iface,void*data);這里稍做解釋。i_foo是供foo_dosomething內(nèi)部使用的一組接口。構(gòu)造foo_object時,我們把一個外部數(shù)據(jù)data和為foo_object相關(guān)特性定義出的i_foo接口捆綁在一起,傳入構(gòu)造函數(shù)foo_create。一般,我還會會每個符合foo_object特性的對象實現(xiàn)一個方法來得到對應(yīng)的i_foo,如:structfoobar;structi_foo*foobar_foo(void);structfoobar*foobar_create(void);voidfoobar_release(structfoobar*);創(chuàng)建一個foo_object對象的代碼看起來是這樣:structfoobar*foobar=foobar_create();structfoo_object*fobj=foo_create(foobar_foo(),foobar);structfoo_object的定義中,必然要記錄i_foo的接口指針和data數(shù)據(jù)指針。從C++的觀點看,foo_object是基類,它也會有一些基類成員和非虛的成員函數(shù)。具體的派生類在實現(xiàn)時,改寫了虛表i_foo的內(nèi)容(重載了虛函數(shù))。data數(shù)據(jù)是在對基類foo_object繼承時擴(kuò)展的數(shù)據(jù)成員。但,在這里,我們使用了組合的方式來擴(kuò)展成員。這增加了一層間接性,但提供了更低的耦合。其中的優(yōu)劣暫且不討論了。通??雌饋頃沁@樣:structfoo_object{structi_foo*vtbl;void*data;void*others;};voidfoo_dosomething(structfoo_object*fobj){fobj->vtbl->foobar(fobj->data);//dosomethingelse}此處還有另一個問題:data的生命期該由誰來負(fù)責(zé)?生命期管理是個很大的課題。也是大多數(shù)使用C/C++開發(fā)的軟件的復(fù)雜度重要來源。我個人傾向于把生命期管理獨立出來解決。所以foo_object模塊一般并不負(fù)責(zé)data的生命期管理。它只負(fù)責(zé)structfoo_object的資源釋放。自己經(jīng)營自己,是我的C語言軟件開發(fā)的觀點之一。我傾向于

溫馨提示

  • 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
  • 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
  • 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁內(nèi)容里面會有圖紙預(yù)覽,若沒有圖紙預(yù)覽就沒有圖紙。
  • 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
  • 5. 人人文庫網(wǎng)僅提供信息存儲空間,僅對用戶上傳內(nèi)容的表現(xiàn)方式做保護(hù)處理,對用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對任何下載內(nèi)容負(fù)責(zé)。
  • 6. 下載文件中如有侵權(quán)或不適當(dāng)內(nèi)容,請與我們聯(lián)系,我們立即糾正。
  • 7. 本站不保證下載資源的準(zhǔn)確性、安全性和完整性, 同時也不承擔(dān)用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。

評論

0/150

提交評論