(這篇文章寫(xiě)于 2008 年底,“去年”指的是 2007 年。)
去年8月入職,培訓(xùn)了4個(gè)月,12月進(jìn)入現(xiàn)在這個(gè)部門(mén),到現(xiàn)在工作正好一年了。工作內(nèi)容是軟件開(kāi)發(fā),具體地說(shuō),用C++開(kāi)發(fā)一個(gè)網(wǎng)絡(luò)應(yīng)用(TCP not Web),這是我們的外匯交易系統(tǒng)的一個(gè)部件。這半年來(lái),和一兩位同事合作把原有的一個(gè)C++程序重寫(xiě)了一遍,并增加了很多新功能,重寫(xiě)后的代碼不長(zhǎng),不到15000行,代碼質(zhì)量與性能大大提高。實(shí)際上,重寫(xiě)只花了三個(gè)月,9月我們交付了第一個(gè)版本,實(shí)現(xiàn)了原來(lái)的主要功能,吞吐量提高4倍。后面這三個(gè)月我們?cè)谠黾有鹿δ埽?zhǔn)備交付第二個(gè)版本。這個(gè)項(xiàng)目讓我對(duì)C++的使用有了新的體會(huì),那就是“實(shí)用當(dāng)頭,樸實(shí)為貴,好用才是王道”。
C++是一門(mén)(最)復(fù)雜的編程語(yǔ)言,語(yǔ)言雖復(fù)雜,不代表一定要用復(fù)雜的方式來(lái)使用它。對(duì)于一個(gè)金融交易系統(tǒng),正確性是首要的,價(jià)格/數(shù)量/交割日期弄錯(cuò)了就會(huì)賠錢(qián)。在編寫(xiě)代碼時(shí),我們特別注意把代碼寫(xiě)得盡量簡(jiǎn)單直白,讓人一看就懂。為了控制代碼的復(fù)雜度,我們采用了基于對(duì)象的風(fēng)格,也就是具體類(lèi)加全局函數(shù),把C++程序?qū)懙萌鏑語(yǔ)言一般清晰,同時(shí)使用一些C++特性和庫(kù)來(lái)減少代碼。
項(xiàng)目中基本沒(méi)有用到面向?qū)ο,或者說(shuō)沒(méi)有用到繼承和多態(tài)的那種面向?qū)ο螅灰欢ǚ堑糜谢?lèi)和派生類(lèi)的設(shè)計(jì)才是好設(shè)計(jì)。引入基類(lèi)和派生類(lèi),或許能帶來(lái)靈活性,但是代碼就不如原來(lái)透徹了。在不需要這種靈活性的場(chǎng)合,干嘛要付出這樣的代價(jià)呢?我寧愿花一天時(shí)間把幾千行 C 代碼弄懂,也不愿在幾十個(gè)類(lèi)組成的繼承體系里繞來(lái)繞去浪費(fèi)腦力。定義并使用清晰一致的接口很重要,但“接口”不一定非得是抽象基類(lèi),一個(gè)類(lèi)的成員函數(shù)就是它的接口。如果看頭文件就能明白這個(gè)類(lèi)在干什么、該怎么用固然很好,如果不明白,打開(kāi)實(shí)現(xiàn)文件,東西都在那兒擺著呢,一望而知。沒(méi)必要非得用個(gè)抽象的接口類(lèi)把使用者和實(shí)現(xiàn)隔開(kāi),再把實(shí)現(xiàn)隱藏起來(lái),這除了讓查找并理解代碼變麻煩之外沒(méi)有任何好處。一個(gè)進(jìn)程內(nèi)部的解耦意義不大,相反,函數(shù)調(diào)用是最直接有效的通信方式;蛟S采用接口類(lèi)/實(shí)現(xiàn)類(lèi)的一個(gè)可能的好處是依賴(lài)注入,便于單元測(cè)試。經(jīng)過(guò)權(quán)衡比較,我們發(fā)現(xiàn)針對(duì)各個(gè)類(lèi)寫(xiě)測(cè)試的意義不大。另外,如果用白盒測(cè)試,那么功能代碼和測(cè)試代碼就得同步更新,會(huì)增加不少工作量,礙手礙腳。
程序里邊有一處用到了繼承,因?yàn)樗芎?jiǎn)化設(shè)計(jì)。這是一個(gè)strategy,涉及一個(gè)基類(lèi)和3、4個(gè)派生類(lèi),所有的類(lèi)都沒(méi)有數(shù)據(jù)成員,只有虛函數(shù)。這幾個(gè)類(lèi)的代碼加起來(lái)不到200行。這個(gè)設(shè)計(jì)不是一開(kāi)始就有的,而是在項(xiàng)目進(jìn)行了一大半的時(shí)候,我們發(fā)現(xiàn)代碼里有若干處針對(duì)請(qǐng)求類(lèi)型的switch/case,于是我們提煉出了一個(gè)strategy,把好幾處switch/case替換為了strategy對(duì)象的虛函數(shù)調(diào)用,從而簡(jiǎn)化了代碼。這里我們純粹把OO當(dāng)做函數(shù)指針表來(lái)用的。
程序里還有幾處用了模板,甚至是type traits,這都是為了簡(jiǎn)化代碼,少敲鍵盤(pán)。這些代碼都藏在一個(gè)角落里,對(duì)外只暴露出一個(gè)全局函數(shù)的接口,使用者不會(huì)被其困擾。
項(xiàng)目里,我們惟一仰賴(lài)的C++特性是確定性析構(gòu),即一個(gè)對(duì)象在離開(kāi)其作用域之后會(huì)保證調(diào)用析構(gòu)函數(shù)。我們利用這點(diǎn)大大簡(jiǎn)化了代碼,并確保資源和內(nèi)存的回收。在我看來(lái),確定性析構(gòu)是C++區(qū)別其他主流開(kāi)發(fā)語(yǔ)言(Java/C#/C/動(dòng)態(tài)腳本語(yǔ)言)的最主要特性。
為了確保正確性,我們另外用Java寫(xiě)了一個(gè)測(cè)試夾具(test harness)來(lái)測(cè)試我們這個(gè)C++程序。這個(gè)測(cè)試夾具模擬了所有與我們這個(gè)C++程序打交道的其他程序,能夠測(cè)試各種正;虍惓5那闆r;旧先魏未a改動(dòng)和bug修復(fù)都在這個(gè)夾具中有體現(xiàn)。如果要新加一個(gè)功能,會(huì)有對(duì)應(yīng)的測(cè)試用例來(lái)驗(yàn)證其行為。如果發(fā)現(xiàn)了一個(gè)bug,先往夾具里加一個(gè)或幾個(gè)能復(fù)現(xiàn)bug的測(cè)試用例,然后修復(fù)代碼,讓測(cè)試通過(guò)。我們積累了幾百個(gè)測(cè)試用例,這些用例表示了我們對(duì)程序行為的預(yù)期,是一份可以運(yùn)行的文檔。每次代碼改動(dòng)提交之前,我們都會(huì)執(zhí)行一遍測(cè)試,以防低級(jí)錯(cuò)誤發(fā)生。
我們讓每個(gè)類(lèi)有明確的職責(zé)范圍,一個(gè)類(lèi)代表一個(gè)概念,不能像個(gè)雜貨鋪一樣什么都裝。在增加或修改功能的時(shí)候,仔細(xì)考慮在哪兒下手才最合理。必要時(shí)可以動(dòng)大手腳,而不是每次都選擇最簡(jiǎn)單的修補(bǔ)方式,那樣只會(huì)使代碼越來(lái)越臭,積重難返,重蹈上一個(gè)版本的覆轍。有時(shí)我們會(huì)提煉出一個(gè)新的類(lèi),把原來(lái)分散在多個(gè)類(lèi)里的代碼集中到一起,從而優(yōu)化結(jié)構(gòu)。我們有測(cè)試夾具保障,并不擔(dān)心修改會(huì)破壞什么。
設(shè)計(jì)不是一開(kāi)始就形成的,而是隨著項(xiàng)目進(jìn)展逐步演化出來(lái)。我們的設(shè)計(jì)是基于類(lèi)的,而不是基于類(lèi)的繼承體系。我們是在寫(xiě)應(yīng)用,不是在寫(xiě)框架,在C++里用那么多繼承對(duì)我們沒(méi)好處。一開(kāi)始我們只有三四個(gè)類(lèi),實(shí)現(xiàn)了基本的報(bào)價(jià)功能,然后增加了一個(gè)類(lèi),實(shí)現(xiàn)了下單功能。這時(shí)我們把報(bào)價(jià)和下單的共同數(shù)據(jù)結(jié)構(gòu)提煉成一個(gè)新的類(lèi),作為原來(lái)兩個(gè)類(lèi)的成員(而不是基類(lèi)。,并把解析客戶輸入的代碼移到這個(gè)類(lèi)里。我們的原則是,可以有特別簡(jiǎn)單的類(lèi),但不宜有特別復(fù)雜的類(lèi),更不能有大怪獸。一個(gè)類(lèi)太大,我們就看看能不能把它拆成兩個(gè),把責(zé)任分開(kāi)。兩個(gè)類(lèi)有共同的代碼邏輯,我們會(huì)考慮提煉出一個(gè)工具類(lèi)來(lái)用,輸入數(shù)據(jù)的驗(yàn)證就是這么提煉出來(lái)的一個(gè)類(lèi)。勿以善小而不為,所以始終能讓代碼保持清晰易懂。
讓代碼保持清晰,給我們帶來(lái)了顯而易見(jiàn)的好處。錯(cuò)誤更容易暴露,在發(fā)布前每多修復(fù)一個(gè)錯(cuò)誤,發(fā)布后就少一次半夜被從被窩里叫醒查錯(cuò)的機(jī)會(huì):)
不要因?yàn)槟硞(gè)技術(shù)流行而去用它,除非它確實(shí)能降低程序的復(fù)雜性。畢竟,軟件開(kāi)發(fā)的首要技術(shù)使命是控制復(fù)雜度,防止腦袋爆掉。對(duì)于繼承要特別小心,這條賊船上去就下不來(lái),除非你是繼承boost::noncopyable 講解面向?qū)ο蟮臅?shū)里,總會(huì)舉一些用繼承的精巧的例子,比如矩形、正方形、圓形繼承自形狀,飛機(jī)和麻雀繼承自“能飛的”,這不意味著繼承處處適用。我認(rèn)為在C++這樣需要自己管理內(nèi)存和對(duì)象生命期的語(yǔ)言里,大規(guī)模使用面向?qū)ο蟆⒗^承、多態(tài)多是自討苦吃。還不如用C語(yǔ)言的思路來(lái)設(shè)計(jì),在局部用一用繼承來(lái)代替函數(shù)指針表。而GoF的《設(shè)計(jì)模式》與其說(shuō)是常見(jiàn)問(wèn)題的解決方案,不如說(shuō)是繞過(guò)(work around)C++語(yǔ)言限制的技巧。當(dāng)然,也是一些人掛在嘴邊用來(lái)忽悠別人或麻痹自己的靈丹妙藥。