冒號(hào)和他的學(xué)生們
27.接口服務(wù)
律己宜嚴(yán),待人宜寬 ——《洪應(yīng)明·菜根譚》
嘆號(hào)幡然反省:“以前我們做OOP編程時(shí),總是專注于如何利用其他類來解決問題,而較少考慮自己設(shè)計(jì)的類對(duì)其他類的影響。”
引號(hào)翻開以前的筆記:“前面提過,OOP的世界是民主制的,所有對(duì)象都是獨(dú)立而平等的公民,有權(quán)利尋求服務(wù),也有義務(wù)提供服務(wù)。看來我們是光惦著權(quán)利而忘了義務(wù)了。”
冒號(hào)繼而提出:“作為服務(wù)的提供者,最重要的是講誠(chéng)信。首先,服務(wù)要有可靠性,不能陽奉陰違——即接口必須履行它的承諾;其次,服務(wù)要有穩(wěn)定性,不能朝令夕改——即接口一經(jīng)公開,不得隨意變更。”
句號(hào)迅即領(lǐng)悟:“從抽象的角度看,服務(wù)的可靠性保證了規(guī)范抽象,服務(wù)的穩(wěn)定性保證了數(shù)據(jù)抽象。”
“孺子可教也!”冒號(hào)喜贊道,“相比而言,前者更為重要,但遺憾的是,只有后者才有法律保障——如果接口被廢棄或其簽名發(fā)生變化,固然會(huì)牽連客戶,至少還可通過編譯器來發(fā)現(xiàn)和修改;而規(guī)范只是語義上的契約,沒有語法上的約束,不在編譯器的監(jiān)管范圍之內(nèi)。”
引號(hào)插言:“編譯器管不著,那只有靠單元測(cè)試了。”
“這正是單元測(cè)試的主要目的。”冒號(hào)很認(rèn)同,“此外,高質(zhì)量的服務(wù)還要有純粹性和完備性。Unix有一個(gè)哲學(xué):‘一個(gè)程序只做一件事,但要做好’。用在OOP上,則是:‘一個(gè)類只提供一套服務(wù),但要完善’。譬如,同為手機(jī),老式的大哥大提供的服務(wù)是純粹的,現(xiàn)代的智能手機(jī)則不是——除了打電話,還能攝像、聽音樂、打游戲、上網(wǎng)等等,完全是手機(jī)與掌上電腦的結(jié)合體。又如,同為通訊工具,手機(jī)提供的服務(wù)是完備的,而BP機(jī)提供的服務(wù)是不完備的——只能接收信息,不能發(fā)送信息。”
嘆號(hào)搖頭晃腦:“提供的服務(wù)過多則不純粹,過少則不完備。如此設(shè)計(jì)出的類是不是要達(dá)到‘增一分則肥,減一分則瘦’的美人標(biāo)準(zhǔn)啊?”
“編程畢竟是門實(shí)踐活兒,完美無缺的設(shè)計(jì)如夢(mèng)中佳人,可以追求卻難以企及。”冒號(hào)笑了笑,“其實(shí)關(guān)鍵不在于服務(wù)數(shù)量的多寡,而在于服務(wù)的一致性和關(guān)聯(lián)性。連貫一致的服務(wù)是良好的抽象與封裝的結(jié)果,同時(shí)也是‘高聚合、低耦合’的保證。”
“作一個(gè)服務(wù)的提供者真不容易啊。”問號(hào)嘆道,“那么,作為服務(wù)的享受者有什么講究嗎?”
逗號(hào)鼻腔里發(fā)出共鳴聲:“哈,享受服務(wù)還需要講究嗎?”
“當(dāng)然有。”冒號(hào)斷然道,“作為服務(wù)的享受者,最重要的是講規(guī)矩。享受人家的服務(wù),自然得按人家的規(guī)則,否則服務(wù)將得不到保障。比如,你可以在超市的貨架上任意選取商品,但不能偷偷溜進(jìn)貨艙去取。”
逗號(hào)辯道:“可貨艙想進(jìn)也進(jìn)不去啊。正如合適的封裝,是禁止客人進(jìn)入私有接口的。”
冒號(hào)作一引喻:“我們不妨這么假設(shè),貨艙的正門掛著‘非工作人員莫入’的牌子。但是你偶然發(fā)現(xiàn),通往洗手間的過道盡頭正好是貨艙的后門,既沒有上鎖,也沒有掛牌。請(qǐng)問你會(huì)不會(huì)大搖大擺地走進(jìn)去?”
逗號(hào)啞口無言。
冒號(hào)循循善誘:“超市的工作人員也許不該為圖方便而開放后門,但那是管理者的事,作為客戶顯然不應(yīng)乘虛而入。商品從貨艙到貨架之前可能會(huì)有裝箱、拆箱、條碼打印、條碼掃描等操作,客戶直接從貨艙拿貨無疑將破壞這種程序,于人于己皆無益處。同樣地,作為他人代碼的客戶,就應(yīng)按他人所設(shè)計(jì)的方式去重用,如此才能保證預(yù)期的效果。至于他人代碼是否有效杜絕了一切可能的漏洞,那是監(jiān)管軟件質(zhì)量的負(fù)責(zé)人的職責(zé)。”
引號(hào)表示理解:“這就好比客戶購(gòu)買了一款產(chǎn)品,卻不按使用說明書進(jìn)行操作,由此而引起的一切后果,廠家概不負(fù)責(zé)。”
“就是這個(gè)理兒。”冒號(hào)輕錘桌面,“當(dāng)然事物是一分為二的。生活中有一個(gè)司空見慣的現(xiàn)象:許多行人跨越護(hù)欄、橫穿馬路。一方面,行人應(yīng)該遵守交通規(guī)則,不應(yīng)破壞道路的‘封裝性’。另一方面,有些交通設(shè)計(jì)者沒有‘以人為本’的客戶意識(shí),為行人提供的斑馬線、天橋或隧道之間相距過遠(yuǎn)。從客觀上說,不夠完備的服務(wù)是導(dǎo)致行人違規(guī)的一大誘因。”
此言顯見深得人心——幾乎人人都當(dāng)過道路封裝的破壞分子。
冒號(hào)接著問:“提個(gè)問題:當(dāng)你們?cè)谑褂靡粋€(gè)類或其中的某個(gè)方法時(shí),對(duì)其用法存疑,即使閱讀注釋文檔也無濟(jì)于事,怎么辦?”
嘆號(hào)順嘴說道:“看源代碼唄。”
“看源代碼是一種很好的學(xué)習(xí)和借鑒他人的方式,但不宜作為用法參考。”冒號(hào)否定道,“且不說源代碼有可能無法獲取,既便能夠,從中提煉出的用法也不一定可靠,更何況具體實(shí)現(xiàn)隨時(shí)可能變化。再打個(gè)比方,如果你不清楚如何設(shè)置一個(gè)鬧鐘,應(yīng)該去看看說明書。如果說明書仍不解決問題,最好詢問廠家,而不是揭開鬧鐘的后蓋去研究它的運(yùn)行機(jī)制,即使你真是個(gè)鐘表行家。”
“所以應(yīng)該直接咨詢代碼的作者。”逗號(hào)發(fā)現(xiàn),過早搶答往往會(huì)掉入老冒的陷阱。這回學(xué)乖了,等嘆號(hào)落坑后才胸有成竹地應(yīng)招。
“方向正確!”冒號(hào)肯定后再次考問,“對(duì)方應(yīng)以何種方式回答?”
“可以口頭,也可以書面啊。”逗號(hào)答畢,隱隱覺得還是著了道。
果然,冒號(hào)搖搖頭:“正確的做法是,對(duì)方應(yīng)通過改進(jìn)并提交的文檔來解釋。該過程可多次循環(huán),直至問題解決。只有這樣,主客雙方的代碼維護(hù)者——包括當(dāng)前的和將來的——才能真正受益。”
問號(hào)深究:“但假如無法聯(lián)系到原作者呢?比如包括JDK庫(kù)在內(nèi)的軟件?”
冒號(hào)回答:“除了盜版的商業(yè)軟件,都應(yīng)該能聯(lián)系到原作者。當(dāng)然,如果與作者使用的不是同一源碼控制庫(kù),上述做法也是可以變通的。好在無論是JDK庫(kù),還是正規(guī)的第三方軟件,文檔注釋應(yīng)該都足夠清晰,許多還會(huì)提供示例代碼。如果這些還不能讓你明白,要么是該軟件不值信賴,也就沒有重用的價(jià)值;要么是你自身的理解問題,只有求助有識(shí)之士了。”
句號(hào)體會(huì)到:“由此可見,封裝的代碼不僅要屏蔽客戶代碼的訪問,最好還能屏蔽客戶代碼開發(fā)者的訪問。這樣既鼓勵(lì)代碼作者多寫規(guī)范文檔,又鼓勵(lì)代碼用戶多讀規(guī)范文檔。一切以規(guī)范為中心,而不以源碼實(shí)現(xiàn)為中心。”
“非常好的建議!”冒號(hào)豎起拇指,“訪問控制只是個(gè)玻璃罩,能防止亂動(dòng)的雙手,卻防止不了偷窺的雙眼。它至多只能維護(hù)語法上的封裝和信息隱藏,而語義上的封裝只有靠規(guī)范來維護(hù)。對(duì)程序員而言,前者是一種需要學(xué)習(xí)的知識(shí),后者是一種需要培養(yǎng)的素質(zhì)。”
嘆號(hào)覺得腦子里仍是半清半濁:“能舉個(gè)語義上違反封裝的例子嗎?”
冒號(hào)爽快地接受請(qǐng)求:“第一個(gè)例子是上節(jié)課談到對(duì)象封裝時(shí)作為反例的User類,其中getBirthday直接返回了內(nèi)部域birthday的引用。如果你在調(diào)用getBirthday后對(duì)返回值進(jìn)行修改,就是一種違反封裝的行為。”
嘆號(hào)有些愕然:“那不是User類本身首先違反封裝原則的嗎?”
冒號(hào)食指微揚(yáng):“不錯(cuò),User類的作者錯(cuò)在授人以隙,而你錯(cuò)在乘人之隙。”
眾人一陣哄笑,嘆號(hào)面紅耳赤,仿佛真的犯了錯(cuò)似的。
“剛才我們說過,超市開放貨艙后門屬管理不善,而客戶鉆進(jìn)去取貨屬不守規(guī)矩。類似地,行人橫穿馬路的問題也有兩方面的因素。”冒號(hào)重提前例,“說回User類,其設(shè)計(jì)者肯定不希望客戶通過此種方式來修改birthday,否則也不會(huì)提供setBirthday的接口。”
逗號(hào)頗為不服:“可是setBirthday中除了簡(jiǎn)單的賦值什么也沒干哪!”
“哈哈,又忍不住偷看源代碼了吧!”冒號(hào)逮了個(gè)正著,“你怎么能保證User類的作者哪天不心血來潮,在setBirthday中寫一些不同尋常的代碼?不要輕視任何一個(gè)接口,哪怕它暫時(shí)只有一個(gè)空語句的實(shí)現(xiàn)。事實(shí)上,許多空接口就是為將來的功能擴(kuò)展預(yù)留的,隨時(shí)可能被充實(shí),或者被子類覆蓋。”
逗號(hào)心里話:得,又掉溝里了!
冒號(hào)續(xù)道:“第二個(gè)例子涉及Java中的Swing。一般說來,如果一個(gè)組件的可視化性質(zhì)如位置、尺寸等發(fā)生改變,都需要重新布局(layout )。凡是Swing組件(component )都要調(diào)用revalidate 方法。絕大多數(shù)情況下,setText、setFont、setIcon等方法的實(shí)現(xiàn)中會(huì)自動(dòng)調(diào)用revalidate,但仍有少數(shù)例外。規(guī)范文檔中又語焉不詳,令人困惑。為保證不受源碼變動(dòng)的影響,同時(shí)免除記憶之困,最好在一個(gè)組件所有與布局相關(guān)的變化完畢后,專門調(diào)用一次revalidate。以輕微的性能代價(jià)換來長(zhǎng)治久安,無疑是正確的。相反,依賴源代碼而非規(guī)范文檔編程,顯然是危險(xiǎn)的。如果說第一個(gè)例子直接破壞了封裝,有可能馬上被察覺,該例則隱蔽得多——只要在所依賴的源代碼不變,一切都正常。然而一旦有變,后果難以預(yù)料。”
引號(hào)不免有些感慨:“一般人熟悉JDK的API文檔多過熟悉源碼,尚且可能犯依賴源碼編程的錯(cuò)誤。如果重用同一開發(fā)組的代碼,甚至是本人的代碼,對(duì)源碼非常熟悉,偏偏文檔還匱乏,這種錯(cuò)誤更是在所難免。”
“意識(shí)到這一點(diǎn)就是很大的進(jìn)步啊。”冒號(hào)欣慰道,“再舉一例。有時(shí)在使用一個(gè)類時(shí),你很想重用其中一個(gè)protected方法,但當(dāng)前所在的客戶類既不是其子類,所在的package也不同。怎么辦?”
句號(hào)承認(rèn):“以前的確碰到這樣的問題,第一感覺是恨那作者太小氣:為什么不干脆將其設(shè)為public與眾共享?轉(zhuǎn)念一想,大不了寫個(gè)繼承的子類,別的事不做,專門把那些protected方法轉(zhuǎn)化為public。”
“是不是這樣?”冒號(hào)在黑板上飛快地寫下——
class Reserved
{
protected void f(){/**/}
protected int g(){/**/}
…
}
class Open extends Reserved
{
public void f(){super.f();}
public int g(){return super.g();}
…
}
見句號(hào)點(diǎn)頭,冒號(hào)問:“你不覺得有何不妥嗎?”
“很俗很暴力。”句號(hào)的自評(píng)令眾人噴飯。
冒號(hào)分析道:“你既然那么希望調(diào)用某個(gè)protected方法,說明它一定不平凡,但為何作者遮遮掩掩、不愿公開呢?假若他的設(shè)計(jì)是合理的,那么只有一個(gè)解釋:它是為內(nèi)部或子類服務(wù)的,本就不打算對(duì)外開放。你所需要的服務(wù)要么是設(shè)計(jì)者刻意回避的,要么接口另有所在,說不定還恰好調(diào)用了你所需要的方法呢!”
一束光芒從眾人腦際劃過。
冒號(hào)又補(bǔ)充道:“不輕易公開他人的protected成員還有一個(gè)理由。正因?yàn)?/span>protected的接口比public使用的范圍狹窄得多,接口變動(dòng)的可能性往往也更大,客戶應(yīng)該慎用。總之,道法自然,不自然的另一面通常是不正確,請(qǐng)注重培養(yǎng)這種編程嗅覺。”
逗號(hào)使勁吸了吸鼻子。
冒號(hào)遂作結(jié)語:“我們提倡針對(duì)接口編程(programming to an interface),避免針對(duì)實(shí)現(xiàn)編程(programming to an implementation)。以上三例則是通過接口深入實(shí)現(xiàn)(programming through an interface to an implementation——《code complete》),本質(zhì)上正是針對(duì)實(shí)現(xiàn)編程。以違背服務(wù)初衷的方式享受的服務(wù),如同盛夏的豆腐——即使沒有變質(zhì),也是不能持久的。”