26.訪問控制
夫輕諾必寡信,多易必多難 ——《老子·德經》
問號提問:“信息隱藏是否專指用private來控制訪問?”
“這正是我們的下一個焦點。”冒號微颔,“訪問修飾符(access modifier)除了可以應用於類成員外,在Java和C#中還能應用於整個類。public類自然是公開的,而缺省的類在Java 和C#中分別僅對同一package和assembly開放。”
逗號不覺有異:“這有什麼講究嗎?”
“當然。越是基本的語法越講究,也越容易被忽視。”冒號的嘴角漾著一絲微笑,“許多人在寫類時習慣上來就敲‘public class’,或者通過IDE自動生成類的雛形,缺省的一般都是public類。這樣看似無害,但絕非好習慣。事實上大多數類是不需要公開的,這是一種更高層次的信息隱藏。每個package或assembly,如果設計合理,應該是具有相關功能的類和接口的集合。其中不少只是內部使用的,毫無必要公諸於眾。”
引號了然於心:“這與對象封裝類似,既向客戶明確了接口類,又可消弭修改內部類對客戶的影響。”
歎號仍心存疑慮:“OOP是鼓勵和支持重用的。好不容易開發出來的類卻深藏不露,豈不可惜?客戶也許一時用不上,但指不定以後會用上。”
“問得好!這也是大多數人的心理。按照你的邏輯,我同樣可以說:這個類的方法是好不容易開發出來的,藏起來太可惜了,統統public吧!”冒號惟妙惟肖地學著歎號的神態和腔調,把眾人都逗樂了。
歎號心欲辯而口難言。
“重用是令人興奮的,合理的重用既節省了開發時間,又節省了維護時間,代碼也顯得更簡潔優美,可謂一舉多得。但是——”冒號一轉話鋒,“過猶不及,過度追求重用也會造成濫用和誤用。一方面,開發者容易沉溺於局部重用的妙處而忽略整體的設計,淡忘所開發類最核心的職責;另一方面,一旦所重用的類或方法發生改變,所有的重用者均受牽連,先前節省的時間或許會加倍地償還。”
引號深有感觸地說:“難就難在如何把握這個度啊!”
“任何一門技藝到了高級階段,都是‘度’的學問。”冒號一如既往地推而廣之,“初級程序員的理想是為所欲為——能用編程解決一切問題;中級程序員的理想是盡善而為——追求最佳解決方案;高級程序員的理想是有所為有所不為——重在整體設計的選擇,能抵制局部技巧的誘惑;最高理想是無為而無不為——無論宏觀設計還是微觀實現,均非刻意選擇,卻自然合度。”
句號心念一動:“這四個階段可分別用四句廣告詞來代表:從全球通的‘我能’,到奧運會的‘更快、更高、更強’,到安踏的‘我選擇,我喜歡’,最後是馬爹利的‘心意有別,心中有度’。”
“到底是時尚小青年,我的推而廣之到你這兒變成了廣而告之啦。”冒號半誇半谑,“書歸正傳,我們再說說類成員的訪問修飾符。”
冒號在黑板上畫了一張表格——
范圍" 語言
C++
Java
C#
無限制
Public
Public
public
子類或同一包
無
Protected
protected internal
同一類或子類
Protected
private protected(已棄用)
protected
同一包
無
package(缺省)
internal
同一類
private(缺省)
Private
private(缺省)
“其中public和private是兩個極端,一個沒有限制,一個僅限於同一類。Java和C#比C++多了個包(package或assembly)的概念,相應也多了package(缺省)和internal的修飾符。”冒號解說道,“值得注意的是,Java中的protected相當於C#的protected internal,不僅可被同類和子類訪問,還能被同一包內的任何類訪問。而C++和C#中的protected只能被同類和子類訪問,相當於Java中昙花一現的private protected。”
逗號急欲得知:“選擇這些修飾符有什麼訣竅嗎?”
冒號回應:“一個基本原則是盡可能地使用限制性更強的修飾符。即使以後改變主意,再放寬限制也不遲。相反,將一個修飾符收窄就要顧忌對客戶的影響了。尤其是域成員,沒有特殊理由都應該是private的,除非類是一個用作儲存的具體數據類型或內部類(inner class)。在JDK源代碼中有不少package、proctected域成員甚至public域成員,絕不能以之為榜樣。順便說一句,C++和C#中缺省的成員修飾符是private,顯然比Java中缺省package更科學一些。”
問號刨根問底:“為什麼特別強調域成員呢?”
冒號條分縷析:“域成員代表對象的狀態,從運行方面看,若外界隨意讀取和改動,將無法保證內在邏輯的一致性;從設計方面看,屬結構性信息,極易變化;從接口方面看,公開接口都是以方法而非域的形式出現的。這些都要求隱藏域成員。”
引號思路很清晰:“既然域成員一般都用private,那關鍵是如何選擇方法成員的訪問修飾符了。”
“如果將每個類看作一個服務者,它向不同范圍內的客戶承諾不同的服務,或者說提供了層次化的服務。以Java為例,每一個public方法成員即是向所有類提供的服務;protected方法成員對該類的子類和同一package下的類提供服務;默認的package方法成員僅對同一package下的類提供服務;private方法成員則只對該類本身提供服務。那麼如何具體界定一個服務的范圍呢?大多時候這是一個偽問題。”說到這裡,冒號有意停頓了一會,查看大家的反應。
眾人臉上果然滿是狐疑之色。
“因為合理設計的服務,其內容與范圍往往是密不可分的。一個服務如果已經設計好了,甚至已經實現了,而此時尚未決定其使用者的范圍,是否有些荒謬?當然,荒謬其實也是現實中的一種常態。”冒號語帶反諷,“作為一個抽象數據類型的類,其核心是抽象接口,因此首先應該設計公共接口,它們的修飾符自然都是public。如果該類是一個抽象類(abstract class)——此‘抽象’與抽象數據類型的‘抽象’意義有所不同,暫且不表——那麼可能會有一些為其子類提供的服務,它們的修飾符自然該是protected。”
問號聽得仔細:“難道非抽象類就不能有protected的接口嗎?”
冒號回道:“從語法上說當然可以。但一般不建議繼承非抽象類,因而protected的意義就不大了。至於個中緣由,留待下節課再解釋。”
歎號追問:“那package服務和private服務呢?”
冒號應答:“package一般作為library的單位,因此package方法成員存在的唯一理由是僅為該library服務,但這種情況相對少見。說到private方法成員,已談不上是真正的服務,純粹是實現細節。由於它的改動不會影響客戶,因此采用private訪問控制不需要任何理由,不采用它才需要理由。”
眾人聽得頻頻點頭。
冒號總結道:“從軟件應變的角度來看,訪問控制是對修改所帶來的副作用的控制。具體地說,如果修改僅僅涉及到private成員,那只要檢查該類的源代碼即可;如果修改涉及到package成員,只需檢查該類所在的package內的所有類。雖然這些類可能很多,但仍可控制,畢竟package是封閉的;如果修改涉及到protected成員,則不僅要檢查該類所在的package內的所有類,還需檢查該類的子類,如果該類本身是public,涉及的類可以超出該package的范圍,已難以真正掌控;如果修改涉及到public成員,那就意味著任何類都可能調用該接口,也就可能因此而無法編譯、運行和工作。因此訪問控制越松的成員,輻射范圍越廣,軟件重用的效率越高,承擔的責任越大,修改的代價也越大。成熟的程序員對public和protected接口的設計一定是慎之又慎,往往在其上花的功夫更甚於具體代碼的編寫。”
逗號似有些不服:“可是誰又能保證接口永遠不變呢?”
“所以Java和C#才分別提供了deprecated和obsolete以將方法標記為過時啊。”冒號笑言以對,“當然這是不得已而為之的下策,可以理解為設計者對使用者的一種致歉。”
見眾人並無共鳴,冒號心道:響鼓也需重錘,遂言:“看來你們感觸還不深,原因是缺乏一種關鍵的意識——客戶意識。客戶意識對一個程序員的重要性,絲毫不亞於對一個企業的重要性。”
逗號忍不住問:“您一再提到客戶,究竟什麼是客戶?”
“這裡的客戶當然不是一般所指的軟件終端消費者,而是指軟件中間消費者或重用者,即調用該軟件的代碼或代碼編寫者。”冒號作了個名詞解釋,“客戶意識的缺乏有幾種原因。首先,不是每個人都有機會開發大型、關鍵的軟件,許多程序員的客戶主要是他自己或少數幾個開發組成員,修改軟件影響到的代碼不多,影響到的人也不多,沒有真正吃過設計不當的苦頭。其次,開發庫和框架的人畢竟是少數,大多數人開發的代碼主要是自己調用或針對終端消費者的。即使他們寫了一些可重用的代碼,如果代碼質量不被認可,也可能無人采用。當一個人習慣於自彈自唱時,是很難培養客戶意識的。最後,正如我們在對象范式中指出的,過程式編程鼓勵自頂向下,而OOP鼓勵自底向上。顯然,頂是底的客戶。問題是許多OOP程序員仍習慣於過程式編程的風格,所設計的類或接口主要是調用其他的類或接口,而不是被調用。換句話說,他們設計的代碼主要角色是客戶,而不是客戶的服務者。”
一番理論令眾人心服口服。
引號關心地問:“如何培養客戶意識呢?”
“如果認識到客戶意識的重要性,培養起來並不難。”冒號帶著安慰的口吻,“謹記一點:輕諾者,必寡信。每一個public類、每一個非private成員,都是一份承諾。在沒有明確職責、沒有准備承擔變更後果之前,請采用最嚴格的訪問控制。有了客戶意識,才有接口責任感。千萬不要為追求廉價的重用而輕易擴大接口范圍,莫以自身之便而致客戶之不便,莫以一時之便而致長期之不便。另外,單元測試對培養客戶意識很有幫助。它不僅僅能發現程序的邏輯缺陷,還能發現程序的設計缺陷。因為測試代碼就是最典型的客戶代碼,它能讓你站客戶的角度重新審視自己的接口設計。”