冒號(hào)和他的學(xué)生們(連載24)——對(duì)象封裝

           

           

          冒號(hào)和他的學(xué)生們

          ——程序員提高班紀(jì)事

           

          24.對(duì)象封裝

          陰陽(yáng)地理兩分張,隱者為陰顯者陽(yáng)                             ——《玉髓經(jīng).曜星論》

            

          “用廣東話說(shuō),真是有型有料又有性格啊!”嘆號(hào)嘖嘖連聲,“這哪里是在設(shè)計(jì)軟件,分明是在設(shè)計(jì)心儀的對(duì)象嘛。”

          “我們可不就是在談對(duì)象設(shè)計(jì)嗎?”冒號(hào)笑著反問(wèn),“在OOP的世界里,每位程序員都是造物主。保持熱情、專注力和審美情趣,說(shuō)不定哪一天就像希臘神話里的皮格瑪利翁一樣,雕塑的美女變活了。”

          “哇,那可就美了!”逗號(hào)極盡夸張之調(diào)。

          全班哄堂大笑。

          “剛才提到抽象是OOP三大基本特性的基礎(chǔ),下面我們逐個(gè)剖析。”冒號(hào)很快收攏了話題,“首當(dāng)其沖的是封裝性。記得前面談對(duì)象范式時(shí),引號(hào)曾試圖為我們解釋封裝性,可惜被我無(wú)情地打斷了。現(xiàn)在我們請(qǐng)他繼續(xù)講解吧。”

          在眾人逗趣式的掌聲中,引號(hào)竟有些靦腆了:“所謂封裝性,就是將數(shù)據(jù)與相關(guān)行為包裝在一起以實(shí)現(xiàn)信息隱藏。”

          “幾乎無(wú)懈可擊。”冒號(hào)贊揚(yáng)得有些保守,“那么封裝(encapsulation)與信息隱藏information hiding)有區(qū)別嗎?”

          “應(yīng)該是一回事吧。”在冒號(hào)的逼視下,引號(hào)有些猶豫了,“嗯。。。信息隱藏是一種原則,而封裝是實(shí)現(xiàn)這種原則的一種方式。”

          “言之有理!”冒號(hào)這回贊揚(yáng)得很干脆,“盡管大多數(shù)參考書(shū)對(duì)二者不加區(qū)分,我還是要解析一番。其實(shí)廣義的封裝僅僅只是一種打包,即packagebundle,是密封的但可以是透明的。或者說(shuō),封裝就是把一些數(shù)據(jù)和方法裝在一個(gè)封閉的盒子里——可能是黑盒子,也可能是白盒子。從語(yǔ)法上說(shuō),這是OOP與諸如C之類的過(guò)程式語(yǔ)言最大的不同。請(qǐng)問(wèn)這帶來(lái)什么效果?”

          句號(hào)反應(yīng)很快:“這等于引入了一種新的模塊機(jī)制,將相關(guān)的數(shù)據(jù)和作用其上的運(yùn)算捆綁在一起形成被稱為類的模塊。”

          “回答正確!”冒號(hào)很滿意,“剛才我們用C實(shí)現(xiàn)了隊(duì)列,但由于C不支持封裝,只能以文件形式來(lái)劃分模塊,顯然不如劃分那么方便和明晰。此外,封裝還有語(yǔ)法糖(Syntactic sugar)效果。”

          問(wèn)號(hào)好奇地問(wèn):“什么是語(yǔ)法糖?是不是很甜?”

          “所謂語(yǔ)法糖,就是一些語(yǔ)法上的甜頭。它不是核心語(yǔ)法,并沒(méi)有提供任何額外的功能,只是用起來(lái)更簡(jiǎn)潔實(shí)用、更自然方便,看起來(lái)更酷、更炫而已。”冒號(hào)有意用時(shí)髦的詞匯來(lái)填補(bǔ)代溝,“我們知道,過(guò)程式函數(shù)采用謂語(yǔ)(主語(yǔ),賓語(yǔ))的形式,而OOP采用主語(yǔ).謂語(yǔ)(賓語(yǔ))的形式。”

          “哦,就是那個(gè)狗吃屎和吃狗屎啊,那可不甜。”逗號(hào)又來(lái)插科打諢。

          眾人笑得前仰后合。

          冒號(hào)不為所動(dòng):“再拿隊(duì)列為例,如果增加一個(gè)隊(duì)列成員,用剛才的C實(shí)現(xiàn),我們需要寫(xiě)下:queue_add(queue, item)。假如用Java來(lái)實(shí)現(xiàn),只需寫(xiě)queue.add(item)。由于封裝使add綁定在queue上,一方面可以將對(duì)象queue前置,既更符合自然語(yǔ)言,又少敲一個(gè)字符;另一方面,這種綁定使add局限于Queue類中,因此不必加上‘queue_’的前綴以防與其他類的方法函數(shù)名相沖突。這同樣節(jié)省了打字,也使接口更簡(jiǎn)單。”

          句號(hào)提出:“如果C支持函數(shù)重載overload),那么‘queue_’的前綴就可省去。”

          “你說(shuō)的既對(duì)也不對(duì)。”冒號(hào)辯證地評(píng)判,“如果C支持重載,該前綴的確能省去;但從另一角度看,即使JavaC++不支持重載,前綴用樣能省去。因?yàn)楹瘮?shù)add已經(jīng)不再是全局函數(shù),Queue類就是其上下文(context)。換句話說(shuō),分屬不同類的函數(shù)是不可能產(chǎn)生歧義(ambiguity)的,哪怕它們的簽名signature)一模一樣。因此我們要把功勞記在封裝的名下。”

          句號(hào)心悅誠(chéng)服。

          冒號(hào)繼續(xù)講解:“狹義的封裝是在打包的基礎(chǔ)上加上訪問(wèn)控制access control),以實(shí)現(xiàn)信息隱藏。相對(duì)于上述廣義的封裝,不妨認(rèn)為多了一個(gè)將白盒子刷成黑盒子的過(guò)程。這一過(guò)程可以看作對(duì)抽象的一種補(bǔ)充:抽象意味著用戶可以從高層的接口來(lái)看待或使用一類對(duì)象,而不用關(guān)心它底層的實(shí)現(xiàn),而黑盒封裝意味著用戶無(wú)權(quán)訪問(wèn)底層的實(shí)現(xiàn)。”

          逗號(hào)有點(diǎn)茫然:“那談起封裝,究竟指哪一個(gè)?”

          “一般所說(shuō)的封裝大多是狹義的。”冒號(hào)回復(fù)道,“考試中最無(wú)趣的一類試題就是名詞解釋,因?yàn)槟侵荒苡∽C記憶,不能印證理解。軟件編程中也有無(wú)數(shù)的名詞和概念,機(jī)械式的記憶沒(méi)有任何意義——除了面試時(shí)應(yīng)付某些同樣無(wú)趣的考官。我們?cè)谶@里著意詮釋封裝的概念,不是出于學(xué)術(shù)理論的目的,而是為了讓大家深刻體會(huì)封裝的目的和意義,以便在實(shí)踐中靈活運(yùn)用。”

          問(wèn)號(hào)詢問(wèn):“前面提到,代碼既要合法又要合理,那訪問(wèn)控制還重要嗎?”

          “合法合理是對(duì)程序員的要求。對(duì)于語(yǔ)言,我們還是希望它盡可能地提供更多的保障。這就好比社會(huì)和諧不能只靠法律,但法制當(dāng)然越健全越好。”冒號(hào)解答道,“訪問(wèn)控制不僅是一種語(yǔ)法限制,也是一種語(yǔ)義規(guī)范——標(biāo)有public的公用接口對(duì)代碼閱讀者而言,顯然比注釋文檔更正式更直觀。因此,其重要性是不言而喻的。值得一提的是,訪問(wèn)控制也不是滴水不漏的。C++用戶可以通過(guò)指針來(lái)間接訪問(wèn)private成員,Java也可以通過(guò)反射機(jī)制來(lái)訪問(wèn)。”

          見(jiàn)眾人頗有疑義,冒號(hào)便寫(xiě)了一段Java代碼——

          // 通過(guò)反射機(jī)制訪問(wèn)私有變量
          import java.lang.reflect.*;

          class Private 
          {
              
          private String field = "這是私有變量";

              
          private void method() 
              
          {
                  System.out.println(
          "調(diào)用私有方法");
              }

          }


          public class AccessTest
          {
              
          public static void main(String[] args) throws Exception
              
          {
                  Private privateObj 
          = new Private();

                  Field f 
          = Private.class.getDeclaredField("field");
                  f.setAccessible(
          true);
                  System.out.println(f.get(privateObj));

                  Method m 
          = Private.class.getDeclaredMethod("method"new Class[0]);
                  m.setAccessible(
          true);
                  m.invoke(privateObj, 
          new Object[0]);
              }

          }

           

          冒號(hào)講述道:“運(yùn)行這段代碼,可以看到privateObj的域成員和方法成員都被訪問(wèn)了。這是一種hack,僅限于特殊用途,不在我們關(guān)心之列。問(wèn)題是,即使不考慮此類非常規(guī)做法,要實(shí)現(xiàn)信息隱藏也不是件容易的事。”

          嘆號(hào)不解:“信息隱藏困難在哪里呢?加上private不就隱藏了成員嗎?”

          “如果所有信息都隱藏了,這個(gè)對(duì)象還有什么用嗎?”冒號(hào)一語(yǔ)破的。

          逗號(hào)一愣:“可以用getter方法返回信息啊。”

          冒號(hào)更不答話,投影出一段代碼——

          import java.util.Date;
          import java.util.Calendar;

          class User
          {
              
          private Date birthday; /** 生日 */
              
          private boolean sex; /** 性別。true代表男,false代表女 */

              
          public User(Date birthday, boolean sex)
              
          {
                  
          this.birthday = birthday;
                  
          this.sex = sex;
              }


              
          public Date getBirthday()
              
          {
                  
          return birthday;
              }


              
          public void setBirthday(Date birthday)
              
          {
                  
          this.birthday = birthday;
              }


              
          public boolean getSex()
              
          {
                  
          return sex;
              }


              
          public void setSex(boolean sex)
              
          {
                  
          this.sex = sex;
              }


              
          /** 計(jì)算年齡,負(fù)數(shù)表示未知 */
              
          public int computeAge()
              
          {
                  
          if (birthday == nullreturn -1

                  Calendar dob 
          = Calendar.getInstance();
                  dob.setTime(birthday);
                  Calendar now 
          = Calendar.getInstance();
                 
          int age = now.get(Calendar.YEAR) - dob.get(Calendar.YEAR);
                 
          if (now.get(Calendar.DAY_OF_YEAR) < dob.get(Calendar.DAY_OF_YEAR))
                      
          --age;
                 
          return age;
              }

          }

           

          冒號(hào)提問(wèn):“這段代碼簡(jiǎn)單得勿需多言,請(qǐng)問(wèn)它的信息隱藏做得如何?”

          眾人目不轉(zhuǎn)睛地盯了好一陣,無(wú)人應(yīng)答。

          冒號(hào)突發(fā)驚人之語(yǔ):“如果我說(shuō)User所有的方法都違背了信息隱藏原則,你們相信嗎?”

          直直的眼睛全都變圓了。

          引號(hào)忽然明白了:“記得書(shū)上曾說(shuō)不能直接返回類的內(nèi)部對(duì)象。GetBirthday返回Date類型的生日,用戶可以在調(diào)用此方法后直接對(duì)生日進(jìn)行操作。”

          “說(shuō)得對(duì)極了!”冒號(hào)夸贊道,“如果一個(gè)方法返回了一個(gè)可變(mutable域?qū)ο?/u>(field object)的引用,無(wú)異于前門緊閉而后門洞開(kāi)。解決的方法是防御性復(fù)制(defensive copying),即返回一個(gè)clone的對(duì)象,以免授人以柄(handle)。”

          逗號(hào)有些難以置信:“好像這類做法很普通啊。”

          冒號(hào)耐心詳解:“首先,請(qǐng)注意可變引用兩個(gè)條件,所有基本類型的域不是引用,因而是安全的,而JavaString之類非基本類由于是不可變的immutable),也是安全的。同樣,在C++C#中的非基本類的值類型value type)也不在此列。此外C++中申明了const的指針或引用返回值也能防止客戶修改。其次,普通的做法不代表是正確的。事實(shí)上,恕我直言:普通的程序員是不合格的,合格的程序員是不普通的。最后,信息隱藏原則固然極其重要,但也不是金科玉律,在一定條件下也是允許的。比如僅作數(shù)據(jù)儲(chǔ)存之用的類甚至可以開(kāi)放所有的域成員,又比如不同類的對(duì)象共享同一引用。此外在一定范圍之內(nèi)為提高效率也可能采取變通之法,當(dāng)然是在對(duì)用戶曉以利害之后。”

          問(wèn)號(hào)舉一反三:“同樣道理,setBirthday也會(huì)導(dǎo)致信息泄漏。考慮到Date類型如此常用,Java是不是該引入一個(gè)不可變的日期類型呢?”

          嘆號(hào)喃喃自語(yǔ):“getSexsetSex會(huì)有什么問(wèn)題呢?boolean是基本類型啊。”

          冒號(hào)提示:“考慮一下性別的可能性。”

          嘆號(hào)訝然道:“難不成還有不男不女型?”

          眾皆大笑。

          冒號(hào)淡淡一笑:“不排除這種可能。更實(shí)際的情況是,有時(shí)性別是未知的。”

          句號(hào)建議:“可以將小boolean換成大Boolean,多一個(gè)null值。”

          冒號(hào)進(jìn)一步指出:“如果想處理三種以上的可能性,可以采用char類型或String類型。總之這是實(shí)現(xiàn)細(xì)節(jié),最好不要暴露給客戶。因此不妨將getSex換成isMaleisFemale兩個(gè)接口。”

          引號(hào)細(xì)細(xì)玩味:“如果isMaleisFemale均返回false,那么性別不是保密就是中性了。至于性別用booleanBooleanchar還是String來(lái)實(shí)現(xiàn),用戶是懵然不知的,這樣比直接了當(dāng)?shù)?/span>getSex更隱蔽也更靈活。”

          冒號(hào)揭開(kāi)最后的答案:“方法computeAge的問(wèn)題不在其實(shí)現(xiàn),而在其命名。該名暗示年齡是計(jì)算出來(lái)的,這暴露了實(shí)現(xiàn)方式,應(yīng)該改為getAge。請(qǐng)注意,信息隱藏中的信息不僅僅是數(shù)據(jù)結(jié)構(gòu),還包括實(shí)現(xiàn)方式和策略。試想,如果將來(lái)把年齡而不是生日作為User的輸入,用年齡倒推生日,getBirthday是不是要換成computeBirthday呢?”

          嘆號(hào)不禁喟曰:“不想如此簡(jiǎn)單的getset竟如此講究!”

          通,則大處圓融合一而小處各具其妙;不通,則大處千變?nèi)f化而小處無(wú)所分別。”冒號(hào)又打起了禪語(yǔ),“領(lǐng)會(huì)OOP的精髓絕非一年半載之功,但若以抽象與封裝為鑰,必可早日開(kāi)啟通達(dá)之門。封裝的故事遠(yuǎn)未結(jié)束,下節(jié)課繼續(xù)。布置一下課后作業(yè),請(qǐng)將示例中的User類按剛才的提示進(jìn)行改進(jìn)。”

          posted on 2008-07-20 16:27 鄭暉 閱讀(2867) 評(píng)論(3)  編輯  收藏 所屬分類: 冒號(hào)和他的學(xué)生們

          評(píng)論

          # re: 冒號(hào)和他的學(xué)生們(連載24)——對(duì)象封裝 2008-07-21 08:55 隔葉黃鶯

          我讀來(lái)很有味,難道是你的講義,嗯,看起來(lái)不太像。
          如果把這真拿到課堂,整幾個(gè)月的純理論,還不讓你的學(xué)員云里來(lái)霧里去。  回復(fù)  更多評(píng)論   

          # re: 冒號(hào)和他的學(xué)生們(連載24)——對(duì)象封裝 2008-07-29 11:47 Christ Chang

          每次看都會(huì)有收獲~  回復(fù)  更多評(píng)論   

          # re: 冒號(hào)和他的學(xué)生們(連載24)——對(duì)象封裝 2008-07-29 14:00 Mr liang

          好極了!  回復(fù)  更多評(píng)論   

          導(dǎo)航

          統(tǒng)計(jì)

          • 隨筆 - 62
          • 文章 - 0
          • 評(píng)論 - 344
          • 引用 - 0

          公告

          博客搬家:http://blog.zhenghui.org
          《冒號(hào)課堂》一書(shū)于2009年10月上市,詳情請(qǐng)見(jiàn)
          冒號(hào)課堂

          留言簿(17)

          隨筆分類(61)

          隨筆檔案(61)

          文章分類(1)

          文章檔案(1)

          最新隨筆

          積分與排名

          • 積分 - 190565
          • 排名 - 304

          最新評(píng)論

          閱讀排行榜

          評(píng)論排行榜

          主站蜘蛛池模板: 高陵县| 栖霞市| 大悟县| 册亨县| 怀远县| 固原市| 海阳市| 克山县| 崇左市| 西贡区| 三台县| 东宁县| 寿宁县| 新昌县| 佛冈县| 万源市| 合山市| 遂川县| 九龙坡区| 建水县| 孟津县| 抚远县| 乌苏市| 九龙县| 惠安县| 广宗县| 汝南县| 句容市| 天水市| 措美县| 白城市| 鲁甸县| 南通市| 伊金霍洛旗| 张家川| 孙吴县| 闵行区| 澄迈县| 荣昌县| 庆城县| 宜春市|