冒號和他的學(xué)生們(連載25)——軟件應(yīng)變
冒號和他的學(xué)生們
25.軟件應(yīng)變
潛其心能觀天下之理,定其心能應(yīng)天下之變 ——《呂坤·呻吟語》
第七課剛一開堂,冒號就提了一個問題:“如果把一個Java程序中所有的private關(guān)鍵字換成public,請問該程序還能工作嗎?”
“應(yīng)該還能工作,除非——此前不能工作。”問號小心翼翼地回答。
冒號接著問:“既然如此,何必費(fèi)事區(qū)分它們呢?”
嘆號嘴一撇:“當(dāng)然是為了信息隱藏啰。”
冒號步步緊逼:“隱藏什么信息呢?又為什么要隱藏?”
嘆號應(yīng)對:“對象的狀態(tài)需要隱藏。如果一個對象的狀態(tài)直接暴露在外,讓客戶隨意修改,可能會破壞對象的內(nèi)在邏輯。”
冒號依舊窮追不舍:“那為什么對象的方法有些也需要隱藏?”
“以前我也有此疑問,看別人代碼時最感興趣的就是那些私有方法。”引號不打自招。
逗號逗他:“看來你患有偷窺癖哦。”
引號暗暗踢了逗號一腳:“現(xiàn)在我明白了,這是為了實現(xiàn)數(shù)據(jù)抽象,將接口與實現(xiàn)分離開來。”
冒號仍不罷休:“這種抽象究竟有何實際好處?”
句號搶答:“一方面,抽象接口描述了一個類最本質(zhì)的行為特征;另一方面,具體實現(xiàn)隨時可能變動,隱藏它們可以保證這種變動不會波及客戶代碼。”
“說到點子上了!”冒號終于停止了追問,“軟件與硬件之別,不僅是無形與有形之別,更是變化與固化之別。所謂變化,指源代碼隨時可能因需而變。一個軟件修改維護(hù)的時間通常會超過編寫時間,越復(fù)雜越成熟的程序越是如此。軟件的難點有二:其一是邏輯的復(fù)雜,其二是需求的變化。許多程序員看重前者而看輕后者,大部分時間花在尋求解決方案上,而不是在選擇解決方案上。他們目眩于奇技淫巧卻不解大巧若拙之妙,殊不知充滿技巧的代碼不僅難于理解而易于出錯,且因其普適性低而受變化的沖擊更大。眾所周知,比武時最忌招式用老,老即難以變化,一旦為對手看破則后果不堪設(shè)想。同樣,動不動凌空躍起只是影視作品中招徠眼球的花哨場面,實戰(zhàn)中很少出現(xiàn),蓋因空中不易變招。當(dāng)然凡事皆有度,無一招用老,便無一招用實,難以完成致命一擊。反映在軟件上,那就是過度設(shè)計會帶來不必要的復(fù)雜和效率損失。”
眾人均想,又上起久違的武術(shù)課了。
冒號滔滔不絕:“一言以蔽之,軟件之軟,體現(xiàn)在適應(yīng)變化的能力。許多編程設(shè)計思想包括OOP的思想都是以此為主題的,抽象與封裝便是典型代表。抽象一個對象模型即是將一類對象最本質(zhì)因而最不易變化的部分提煉出來,而封裝——準(zhǔn)確地說是信息隱藏——則是將非本質(zhì)、容易變化的部分隱藏起來,從而將一個類劃分為陰陽兩面。由于變化多發(fā)生在陰面,對外是屏蔽的,因此修改該面毫無累及客戶之憂,由此提高了軟件的抗變能力。有些人誤認(rèn)為信息隱藏是出于軟件安全(security)的考慮,實乃是似是而非的皮相之見。”
問號提問:“軟件的變化主要有哪些?”
“軟件的變化大致分兩種:一種是出于內(nèi)在需求而作的結(jié)構(gòu)性變化,通常以改善軟件質(zhì)量為目的,即所謂的重構(gòu)(refactoring);一種是出于外在需求而作的功能性變化,通常以滿足客戶需要為目的。理想的抽象與封裝,應(yīng)能完全避免第一類變化對于客戶代碼的影響,也能最大限度地降低第二類變化的副作用。只是知易行難,為細(xì)微的變化而付出巨大代價的例子比比皆是。‘千年蟲’就是一個最典型的例子,而當(dāng)32 位的IPv4 全部換成128位的IPv6 ,其代價也不遑多讓。從中可以看出,信息隱藏,尤其是結(jié)構(gòu)性信息隱藏是多么的重要!下面看一個簡單的例子。”冒號打開幻燈片——
// 用直角坐標(biāo)實現(xiàn)的復(fù)數(shù)類













































“這是一個用直角坐標(biāo)實現(xiàn)的復(fù)數(shù)Java類,為簡明起見,僅僅實現(xiàn)了實部、虛部、模、輻角、加法和乘法等運(yùn)算。同樣地,我們也可以用極坐標(biāo)來實現(xiàn)。”冒號投影出另一段代碼——
// 用極坐標(biāo)實現(xiàn)的復(fù)數(shù)類





















































句號似已深明其意:“這兩個類的接口相同而實現(xiàn)方式不同,它們的區(qū)別是結(jié)構(gòu)性的,而不是功能性的。就實現(xiàn)效率而論,直角坐標(biāo)便于加減運(yùn)算,而極坐標(biāo)便于乘除、乘方開方等運(yùn)算。實現(xiàn)者可能會為采用何種方案而舉棋不定,好在由于隱藏了結(jié)構(gòu)性信息,即使以后修改了實現(xiàn)方案,也不會影響客戶。”
冒號補(bǔ)充道:“如果將代碼移植到C++,修改了實現(xiàn)方案,還是可能在一定程度上影響客戶的。”
嘆號有些驚訝:“為什么?C++不也是OOP語言嗎?”
冒號解釋:“由于C++需要頭文件,即使私有成員也必須在頭文件中聲明。這意味著改動任何私有數(shù)據(jù)結(jié)構(gòu)甚至私有方法的簽名,所有包含該頭文件的源代碼雖不必改寫,卻需要重新編譯鏈接。這對大型程序來說通常是難以忍受的,同時也說明設(shè)計與語言息息相關(guān)的。如果一個設(shè)計者只是高高在上,完全不考慮語言細(xì)節(jié),難免流于紙上談兵。”
逗號問道:“為什么Java不需要頭文件呢?”
“因為Java、C#包括D語言中類似頭文件的信息,已經(jīng)在編譯時自動提取并保存了。”冒號道出緣由,“出于歷史原因和效率上的考慮,C++仍沿用C的頭文件用法,成為除指針和內(nèi)存管理之外最令人頭痛的問題。因此在C++中應(yīng)盡可能地使用前置聲明(forward declaration),減少包含的(included)頭文件。另外,可以將一些私有靜態(tài)(private static)成員從頭文件轉(zhuǎn)移到實現(xiàn)代碼中,以匿名命名空間(anonymous namespace)的方式來實現(xiàn)完全隱藏。此外還有一個非常有用的技巧——柄/體(handle/body)模式或稱橋梁模式(bridge pattern),可以將接口與實現(xiàn)完全分開。這種模式不僅可以解決C++中的頭文件問題,對Java等不需要頭文件的語言也是有用的。下面我們用這種模式重新實現(xiàn)Complex類。”
幻燈一閃,新的源碼出現(xiàn)在眾人眼前——

















































































































冒號進(jìn)而指出:“這是橋梁模式的簡化版。稍加改進(jìn),我們不僅可以在編譯期間決定具體實現(xiàn)方式,甚至可以讓客戶在運(yùn)行期間選擇實現(xiàn)方式。你們課后不妨試試。”
引號一拍大腿:“妙!如此既免除了實現(xiàn)者抉擇的煩惱,也給賦予使用者更大的自由,可謂一舉兩得啊。”
句號也道:“信息隱藏雖能將抽象接口與具體實現(xiàn)分離,但仍然封裝在同一類中。橋梁模式則讓二者徹底解耦(decouple),增強(qiáng)了對變化的適應(yīng)力,具有更大的靈活性和可擴(kuò)展性。”
“當(dāng)然這也增加了一定的復(fù)雜性和效率上的損失,具體運(yùn)用時應(yīng)酌情考量,避免過度設(shè)計。”冒號提醒道,“最后,如果Complex類需要功能上的變化,比如增加乘方、開方等運(yùn)算,只要不修改現(xiàn)有運(yùn)算的簽名,是不會傷及客戶代碼的。”
posted on 2008-07-29 00:20 鄭暉 閱讀(1970) 評論(3) 編輯 收藏 所屬分類: 冒號和他的學(xué)生們