程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> 冒號和他的學生們(連載27)——接口服務

冒號和他的學生們(連載27)——接口服務

編輯:關於JAVA

27.接口服務

律己宜嚴,待人宜寬                     ——《洪應明·菜根譚》

歎號幡然反省:“以前我們做OOP編程時,總是專注於如何利用其他類來解決問題,而較少考慮自己設計的類對其他類的影響。”

引號翻開以前的筆記:“前面提過,OOP的世界是民主制的,所有對象都是獨立而平等的公民,有權利尋求服務,也有義務提供服務。看來我們是光惦著權利而忘了義務了。”

冒號繼而提出:“作為服務的提供者,最重要的是講誠信。首先,服務要有可靠性,不能陽奉陰違——即接口必須履行它的承諾;其次,服務要有穩定性,不能朝令夕改——即接口一經公開,不得隨意變更。”

句號迅即領悟:“從抽象的角度看,服務的可靠性保證了規范抽象,服務的穩定性保證了數據抽象。”

“孺子可教也!”冒號喜贊道,“相比而言,前者更為重要,但遺憾的是,只有後者才有法律保障——如果接口被廢棄或其簽名發生變化,固然會牽連客戶,至少還可通過編譯器來發現和修改;而規范只是語義上的契約,沒有語法上的約束,不在編譯器的監管范圍之內。”

引號插言:“編譯器管不著,那只有靠單元測試了。”

“這正是單元測試的主要目的。”冒號很認同,“此外,高質量的服務還要有純粹性和完備性。Unix有一個哲學:‘一個程序只做一件事,但要做好’。用在OOP上,則是:‘一個類只提供一套服務,但要完善’。譬如,同為手機,老式的大哥大提供的服務是純粹的,現代的智能手機則不是——除了打電話,還能攝像、聽音樂、打游戲、上網等等,完全是手機與掌上電腦的結合體。又如,同為通訊工具,手機提供的服務是完備的,而BP機提供的服務是不完備的——只能接收信息,不能發送信息。”

歎號搖頭晃腦:“提供的服務過多則不純粹,過少則不完備。如此設計出的類是不是要達到‘增一分則肥,減一分則瘦’的美人標准啊?”

“編程畢竟是門實踐活兒,完美無缺的設計如夢中佳人,可以追求卻難以企及。”冒號笑了笑,“其實關鍵不在於服務數量的多寡,而在於服務的一致性和關聯性。連貫一致的服務是良好的抽象與封裝的結果,同時也是‘高聚合、低耦合’的保證。”

“作一個服務的提供者真不容易啊。”問號歎道,“那麼,作為服務的享受者有什麼講究嗎?”

逗號鼻腔裡發出共鳴聲:“哈,享受服務還需要講究嗎?”

“當然有。”冒號斷然道,“作為服務的享受者,最重要的是講規矩。享受人家的服務,自然得按人家的規則,否則服務將得不到保障。比如,你可以在超市的貨架上任意選取商品,但不能偷偷溜進貨艙去取。”

逗號辯道:“可貨艙想進也進不去啊。正如合適的封裝,是禁止客人進入私有接口的。”

冒號作一引喻:“我們不妨這麼假設,貨艙的正門掛著‘非工作人員莫入’的牌子。但是你偶然發現,通往洗手間的過道盡頭正好是貨艙的後門,既沒有上鎖,也沒有掛牌。請問你會不會大搖大擺地走進去?”

逗號啞口無言。

冒號循循善誘:“超市的工作人員也許不該為圖方便而開放後門,但那是管理者的事,作為客戶顯然不應乘虛而入。商品從貨艙到貨架之前可能會有裝箱、拆箱、條碼打印、條碼掃描等操作,客戶直接從貨艙拿貨無疑將破壞這種程序,於人於己皆無益處。同樣地,作為他人代碼的客戶,就應按他人所設計的方式去重用,如此才能保證預期的效果。至於他人代碼是否有效杜絕了一切可能的漏洞,那是監管軟件質量的負責人的職責。”

引號表示理解:“這就好比客戶購買了一款產品,卻不按使用說明書進行操作,由此而引起的一切後果,廠家概不負責。”

“就是這個理兒。”冒號輕錘桌面,“當然事物是一分為二的。生活中有一個司空見慣的現象:許多行人跨越護欄、橫穿馬路。一方面,行人應該遵守交通規則,不應破壞道路的‘封裝性’。另一方面,有些交通設計者沒有‘以人為本’的客戶意識,為行人提供的斑馬線、天橋或隧道之間相距過遠。從客觀上說,不夠完備的服務是導致行人違規的一大誘因。”

此言顯見深得人心——幾乎人人都當過道路封裝的破壞分子。

冒號接著問:“提個問題:當你們在使用一個類或其中的某個方法時,對其用法存疑,即使閱讀注釋文檔也無濟於事,怎麼辦?”

歎號順嘴說道:“看源代碼呗。”

“看源代碼是一種很好的學習和借鑒他人的方式,但不宜作為用法參考。”冒號否定道,“且不說源代碼有可能無法獲取,既便能夠,從中提煉出的用法也不一定可靠,更何況具體實現隨時可能變化。再打個比方,如果你不清楚如何設置一個鬧鐘,應該去看看說明書。如果說明書仍不解決問題,最好詢問廠家,而不是揭開鬧鐘的後蓋去研究它的運行機制,即使你真是個鐘表行家。”

“所以應該直接咨詢代碼的作者。”逗號發現,過早搶答往往會掉入老冒的陷阱。這回學乖了,等歎號落坑後才胸有成竹地應招。

“方向正確!”冒號肯定後再次考問,“對方應以何種方式回答?”

“可以口頭,也可以書面啊。”逗號答畢,隱隱覺得還是著了道。

果然,冒號搖搖頭:“正確的做法是,對方應通過改進並提交的文檔來解釋。該過程可多次循環,直至問題解決。只有這樣,主客雙方的代碼維護者——包括當前的和將來的——才能真正受益。”

問號深究:“但假如無法聯系到原作者呢?比如包括JDK庫在內的軟件?”

冒號回答:“除了盜版的商業軟件,都應該能聯系到原作者。當然,如果與作者使用的不是同一源碼控制庫,上述做法也是可以變通的。好在無論是JDK庫,還是正規的第三方軟件,文檔注釋應該都足夠清晰,許多還會提供示例代碼。如果這些還不能讓你明白,要麼是該軟件不值信賴,也就沒有重用的價值;要麼是你自身的理解問題,只有求助有識之士了。”

句號體會到:“由此可見,封裝的代碼不僅要屏蔽客戶代碼的訪問,最好還能屏蔽客戶代碼開發者的訪問。這樣既鼓勵代碼作者多寫規范文檔,又鼓勵代碼用戶多讀規范文檔。一切以規范為中心,而不以源碼實現為中心。”

“非常好的建議!”冒號豎起拇指,“訪問控制只是個玻璃罩,能防止亂動的雙手,卻防止不了偷窺的雙眼。它至多只能維護語法上的封裝和信息隱藏,而語義上的封裝只有靠規范來維護。對程序員而言,前者是一種需要學習的知識,後者是一種需要培養的素質。”

歎號覺得腦子裡仍是半清半濁:“能舉個語義上違反封裝的例子嗎?”

冒號爽快地接受請求:“第一個例子是上節課談到對象封裝時作為反例的User類,其中getBirthday直接返回了內部域birthday的引用。如果你在調用getBirthday後對返回值進行修改,就是一種違反封裝的行為。”

歎號有些愕然:“那不是User類本身首先違反封裝原則的嗎?”

冒號食指微揚:“不錯,User類的作者錯在授人以隙,而你錯在乘人之隙。”

眾人一陣哄笑,歎號面紅耳赤,仿佛真的犯了錯似的。

“剛才我們說過,超市開放貨艙後門屬管理不善,而客戶鑽進去取貨屬不守規矩。類似地,行人橫穿馬路的問題也有兩方面的因素。”冒號重提前例,“說回User類,其設計者肯定不希望客戶通過此種方式來修改birthday,否則也不會提供setBirthday的接口。”

逗號頗為不服:“可是setBirthday中除了簡單的賦值什麼也沒干哪!”

“哈哈,又忍不住偷看源代碼了吧!”冒號逮了個正著,“你怎麼能保證User類的作者哪天不心血來潮,在setBirthday中寫一些不同尋常的代碼?不要輕視任何一個接口,哪怕它暫時只有一個空語句的實現。事實上,許多空接口就是為將來的功能擴展預留的,隨時可能被充實,或者被子類覆蓋。”

逗號心裡話:得,又掉溝裡了!

冒號續道:“第二個例子涉及Java中的Swing。一般說來,如果一個組件的可視化性質如位置、尺寸等發生改變,都需要重新布局(layout )。凡是Swing組件(component )都要調用revalidate 方法。絕大多數情況下,setText、setFont、setIcon等方法的實現中會自動調用revalidate,但仍有少數例外。規范文檔中又語焉不詳,令人困惑。為保證不受源碼變動的影響,同時免除記憶之困,最好在一個組件所有與布局相關的變化完畢後,專門調用一次revalidate。以輕微的性能代價換來長治久安,無疑是正確的。相反,依賴源代碼而非規范文檔編程,顯然是危險的。如果說第一個例子直接破壞了封裝,有可能馬上被察覺,該例則隱蔽得多——只要在所依賴的源代碼不變,一切都正常。然而一旦有變,後果難以預料。”

引號不免有些感慨:“一般人熟悉JDK的API文檔多過熟悉源碼,尚且可能犯依賴源碼編程的錯誤。如果重用同一開發組的代碼,甚至是本人的代碼,對源碼非常熟悉,偏偏文檔還匮乏,這種錯誤更是在所難免。”

“意識到這一點就是很大的進步啊。”冒號欣慰道,“再舉一例。有時在使用一個類時,你很想重用其中一個protected方法,但當前所在的客戶類既不是其子類,所在的package也不同。怎麼辦?”

句號承認:“以前的確碰到這樣的問題,第一感覺是恨那作者太小氣:為什麼不干脆將其設為public與眾共享?轉念一想,大不了寫個繼承的子類,別的事不做,專門把那些protected方法轉化為public。”

“是不是這樣?”冒號在黑板上飛快地寫下——

class Reserved
{
   protected void f(){/**/}
   protected int g(){/**/}
   …
}
class Open extends Reserved
{
   public void f(){super.f();}
   public int g(){return super.g();}
   …
}

見句號點頭,冒號問:“你不覺得有何不妥嗎?”

“很俗很暴力。”句號的自評令眾人噴飯。

冒號分析道:“你既然那麼希望調用某個protected方法,說明它一定不平凡,但為何作者遮遮掩掩、不願公開呢?假若他的設計是合理的,那麼只有一個解釋:它是為內部或子類服務的,本就不打算對外開放。你所需要的服務要麼是設計者刻意回避的,要麼接口另有所在,說不定還恰好調用了你所需要的方法呢!”

一束光芒從眾人腦際劃過。

冒號又補充道:“不輕易公開他人的protected成員還有一個理由。正因為protected的接口比public使用的范圍狹窄得多,接口變動的可能性往往也更大,客戶應該慎用。總之,道法自然,不自然的另一面通常是不正確,請注重培養這種編程嗅覺。”

逗號使勁吸了吸鼻子。

冒號遂作結語:“我們提倡針對接口編程(programming to an interface),避免針對實現編程(programming to an implementation)。以上三例則是通過接口深入實現(programming through an interface to an implementation——《code complete》),本質上正是針對實現編程。以違背服務初衷的方式享受的服務,如同盛夏的豆腐——即使沒有變質,也是不能持久的。”

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved