Vincent

          Vicent's blog
          隨筆 - 74, 文章 - 0, 評(píng)論 - 5, 引用 - 0
          數(shù)據(jù)加載中……

          Java 理論與實(shí)踐: 偽 typedef 反模式

          將泛型添加到 Java? 語(yǔ)言中增加了類(lèi)型系統(tǒng)的復(fù)雜性,提高了許多變量和方法聲明的冗長(zhǎng)程度。因?yàn)闆](méi)有提供 “typedef” 工具來(lái)定義類(lèi)型的簡(jiǎn)短名稱(chēng),所以有些開(kāi)發(fā)人員轉(zhuǎn)而把擴(kuò)展當(dāng)作 “窮人的 typedef”,但是收到的決不是好的結(jié)果。在這個(gè)月的 Java 理論與實(shí)踐 中,Java 專(zhuān)家 Brian Goetz 解釋了這個(gè) “反模式” 的限制。

          對(duì)于 Java 5.0 中新增的泛型工具,一個(gè)常見(jiàn)的抱怨就是,它使代碼變得太冗長(zhǎng)。原來(lái)用一行就夠的變量聲明不再存在了,與聲明參數(shù)化類(lèi)型有關(guān)的重復(fù)非常討厭,特別是還沒(méi)有良好地支持自動(dòng)補(bǔ)足的 IDE。例如,如果想聲明一個(gè) Map,它的鍵是 Socket,值是 Future<String>,那么老方法就是:

          												
          														Map socketOwner = new HashMap();
          
          												
          										

          比新方法緊湊得多:
          Map<Socket, Future<String>> socketOwner 
            = new HashMap<Socket, Future<String>>();  
          

          當(dāng)然,新方法內(nèi)置了更多類(lèi)型信息,減少了編程錯(cuò)誤,提高了程序的可讀性,但是確實(shí)帶來(lái)了更多聲明變量和方法簽名方面的前期工作。類(lèi)型參數(shù)在聲明和初始化中的重復(fù)看起來(lái)尤其沒(méi)有必要;SocketFuture<String> 需要輸入兩次,這迫使我們違犯了 “DRY” 原則(不要重復(fù)自己)。

          合成類(lèi)似于 typedef 的東西

          添加泛型給類(lèi)型系統(tǒng)增加了一些復(fù)雜性。在 Java 5.0 之前,“type” 和 “class” 幾乎是同義的,而參數(shù)化類(lèi)型,特別是那些綁定的通配類(lèi)型,使子類(lèi)型和子類(lèi)的概念有了顯著區(qū)別。類(lèi)型 ArrayList<?>ArrayList<? extends Number>ArrayList<Integer> 是不同的類(lèi)型,雖然它們是由同一個(gè)類(lèi) ArrayList 實(shí)現(xiàn)的。這些類(lèi)型構(gòu)成了一個(gè)層次結(jié)構(gòu);ArrayList<?>ArrayList<? extends Number> 的超類(lèi)型,而 ArrayList<? extends Number>ArrayList<Integer> 的超類(lèi)型。

          對(duì)于原來(lái)的簡(jiǎn)單類(lèi)型系統(tǒng),像 C 的 typedef 這樣的特性沒(méi)有意義。但是對(duì)于更復(fù)雜的類(lèi)型系統(tǒng),typedef 工具可能會(huì)提供一些好處。不知是好還是壞,總之在泛型加入的時(shí)候,typedef 沒(méi)有加入 Java 語(yǔ)言。

          有些人用作 “窮人的 typedef” 的一個(gè)(壞的)做法是一個(gè)小小的擴(kuò)展:創(chuàng)建一個(gè)類(lèi),擴(kuò)展泛型類(lèi)型,但是不添加功能,例如 SocketUserMap 類(lèi)型,如清單 1 所示:


          清單 1. 偽 typedef 反模式 —— 不要這么做
          public class SocketUserMap extends HashMap<Socket<Future<String>> { }
          SocketUserMap socketOwner = new SocketUserMap();
          

          我將這個(gè)技巧稱(chēng)為偽 typedef 反模式,它實(shí)現(xiàn)了將 socketOwner 定義簡(jiǎn)化為一行的這一(有問(wèn)題的)目標(biāo),但是有些副作用,最終成為重用和維護(hù)的障礙。(對(duì)于有明確的構(gòu)造函數(shù)而不是無(wú)參構(gòu)造函數(shù)的類(lèi)來(lái)說(shuō),派生類(lèi)也需要聲明每個(gè)構(gòu)造函數(shù),因?yàn)闃?gòu)造函數(shù)沒(méi)有被繼承。)





          回頁(yè)首


          偽類(lèi)型的問(wèn)題

          在 C 中,用 typedef 定義一個(gè)新類(lèi)型更像是宏,而不是類(lèi)型聲明。定義等價(jià)類(lèi)型的 typedef,可以與原始類(lèi)型自由地互換。清單 2 顯示了一個(gè)定義回調(diào)函數(shù)的示例,其中在簽名中使用了一個(gè) typedef,但是調(diào)用者提供給回調(diào)的是一個(gè)等價(jià)類(lèi)型,而編譯器和運(yùn)行時(shí)都可以接受它:


          清單 2. C 語(yǔ)言的 typedef 示例
          // Define a type called "callback" that is a function pointer
          typedef void (*Callback)(int);
          
          void doSomething(Callback callback) { }
          
          // This function conforms to the type defined by Callback
          void callbackFunction(int arg) { }
          
          // So a caller can pass the address of callbackFunction to doSomething
          void useCallback() {
            doSomething(&callbackFunction); 
          }
          

          擴(kuò)展不是類(lèi)型定義

          用 Java 語(yǔ)言編寫(xiě)的試圖使用偽 typedef 的等價(jià)程序就會(huì)出現(xiàn)麻煩。清單 3 的 StringListUserList 類(lèi)型都擴(kuò)展了一個(gè)公共超類(lèi),但是它們不是等價(jià)的類(lèi)型。這意味著任何想調(diào)用 lookupAll 的代碼都必須傳遞一個(gè) StringList,而不能是 List<String>UserList


          清單 3. 偽類(lèi)型如何把客戶(hù)限定在只能使用偽類(lèi)型
          class StringList extends ArrayList<String> { }
          class UserList extends ArrayList<String> { }
          ...
          class SomeClass {
              public void validateUsers(UserList users) { ... }
              public UserList lookupAll(StringList names) { ... }
          }
          

          這個(gè)限制要比初看上去嚴(yán)格得多。在小程序中,可能不會(huì)有太大差異,但是當(dāng)程序變大的時(shí)候,使用偽類(lèi)型的需求就會(huì)不斷地造成問(wèn)題。如果變量類(lèi)型是 StringList,就不能給它分配普通的 List<String>,因?yàn)?List<String>StringList 的超類(lèi)型,所以不是 StringList。就像不能把 Object 分配給類(lèi)型為 String 的變量一樣,也不能把 List<String> 分配給類(lèi)型為 StringList 的變量(但是,可以反過(guò)來(lái),例如,可以把 StringList 分配給類(lèi)型為 List<String> 的變量,因?yàn)?List<String>StringList 的超類(lèi)型。)

          同樣的情況也適用于方法的參數(shù);如果一個(gè)方法參數(shù)是 StringList 類(lèi)型,那么就不能把普通的 List<String> 傳遞給它。這意味著,如果不要求這個(gè)方法的每次使用都使用偽類(lèi)型,那么根本不能用偽類(lèi)型作為方法參數(shù),而這在實(shí)踐當(dāng)中就意味著在庫(kù) API 中根本就不能使用偽類(lèi)型。而且大多數(shù)庫(kù) API 都源自本來(lái)沒(méi)想成為庫(kù)代碼的那些代碼,所以 “這個(gè)代碼只是給我自己的,沒(méi)有其他人會(huì)用它” 可不是個(gè)好借口(只要您的代碼有一點(diǎn)兒用處,別人就有可能會(huì)使用它;如果您的代碼臭得很,那您可能是對(duì)的)。

          偽類(lèi)型會(huì)傳染

          這種 “病毒” 性質(zhì)是讓 C 代碼的重用有困難的因素之一。差不多每個(gè) C 包都有頭文件,定義工具宏和類(lèi)型,像 int32booleantruefalse,諸如此類(lèi)。如果想在一個(gè)應(yīng)用程序內(nèi)使用幾個(gè)包,而它們對(duì)于這些公共條目沒(méi)有使用相同的定義,那么即使要編譯一個(gè)只包含所有頭文件的空程序,之前也要在 “頭文件地獄” 問(wèn)題上花好長(zhǎng)時(shí)間。如果編寫(xiě)的 C 應(yīng)用程序要使用許多來(lái)自不同作者的不同的包,那么幾乎肯定要涉及一些這類(lèi)痛苦。另一方面,對(duì)于 Java 應(yīng)用程序來(lái)說(shuō),在沒(méi)有這類(lèi)痛苦的情況下使用許多甚至更多的包,是非常常見(jiàn)的事。如果包要在它們的 API 中使用偽類(lèi)型,那么我們可能就要重新經(jīng)歷早已留在痛苦回憶中的問(wèn)題。

          作為示例,假設(shè)有兩個(gè)不同的包,每個(gè)包都用偽類(lèi)型反模式定義了 StringList,如清單 4 所示,而且每個(gè)包都定義了操作 StringList 的工具方法。兩個(gè)包都定義了同樣的標(biāo)識(shí)符,這一事實(shí)已經(jīng)是不方便的一個(gè)小源頭了;客戶(hù)程序必須選擇導(dǎo)入一個(gè)定義,而另一個(gè)定義則要使用完全限定的名稱(chēng)。但是更大的問(wèn)題是現(xiàn)在這些包的客戶(hù)無(wú)法創(chuàng)建既能傳遞給 sortList 又能傳遞給 reverseList 的對(duì)象,因?yàn)閮蓚€(gè)不同的 StringList 類(lèi)型是不同的類(lèi)型,彼此互不兼容。客戶(hù)現(xiàn)在必須在使用一個(gè)包還是使用另一個(gè)包之間進(jìn)行選擇,否則他們就必須做許多工作,在不同類(lèi)型的 StringList 之間進(jìn)行轉(zhuǎn)換。對(duì)包的作者來(lái)說(shuō)以為方便的東西,成為在所有地方使用這個(gè)包的突出障礙,除非在最受限的環(huán)境中。


          清單 4. 偽類(lèi)型的使用如何妨礙重用
          package a;
          
          class StringList extends ArrayList<String> { }
          class ListUtilities {
              public static void sortList(StringList list) { }
          }
          
          package b;
          
          class StringList extends ArrayList<String> { }
          class SomeOtherUtilityClass {
              public static void reverseList(StringList list) { }
          }
           
          ...
          
          class Client {
              public void someMethod() {
                  StringList list = ...;
                  // Can't do this
                  ListUtilities.sortList(list);
                  SomeOtherUtilityClass.reverseList(list);
              }
          }
          

          偽類(lèi)型通常太具體

          偽類(lèi)型反模式的進(jìn)一步問(wèn)題是,它會(huì)喪失使用接口定義變量類(lèi)型和方法參數(shù)的好處。雖然可以把 StringList 定義成擴(kuò)展 List<String> 的接口,再定義一個(gè)具體類(lèi)型 StringArrayList 來(lái)擴(kuò)展 ArrayList<String> 并實(shí)現(xiàn) StringList,但多數(shù)偽 typedef 反模式的用戶(hù)通常達(dá)不到這種水平,因?yàn)檫@項(xiàng)技術(shù)的目的主要是為了簡(jiǎn)化和縮短類(lèi)型的名稱(chēng)。但結(jié)果是,API 的用處減少了并變得更脆弱,因?yàn)樗鼈兪褂?ArrayList 這樣的具體類(lèi)型,而不是 List 這樣的抽象類(lèi)型。

          更安全的技巧

          一個(gè)更安全的減少聲明泛型集合所需打字量的技巧是使用類(lèi)型推導(dǎo)(type inference)。編譯器可以非常聰明地使用程序中內(nèi)嵌的類(lèi)型信息來(lái)分配類(lèi)型參數(shù)。如果定義了下面這樣一個(gè)工具方法:

          public static <K,V> Map<K,V> newHashMap() {
              return new HashMap<K,V>(); 
          }
          

          那么可以安全地用它來(lái)避免錄入兩次參數(shù):
          Map<Socket, Future<String>> socketOwner = Util.newHashMap();
          

          這種方法之所以能夠奏效,在于編譯器可以根據(jù)泛型方法 newHashMap() 被調(diào)用的位置推導(dǎo)出 KV 的值。



          回頁(yè)首


          結(jié)束語(yǔ)

          偽 typedef 反模式的動(dòng)機(jī)很簡(jiǎn)單 —— 開(kāi)發(fā)人員想要一種方法可以定義更緊湊的類(lèi)型標(biāo)識(shí)符,特別是在泛型把類(lèi)型標(biāo)識(shí)符變得更冗長(zhǎng)的時(shí)候。問(wèn)題在于這個(gè)做法在使用它的代碼和代碼的客戶(hù)之間形成了緊密的耦合,從而妨礙了重用。不喜歡泛型類(lèi)型標(biāo)識(shí)符的冗長(zhǎng)是可以理解的,但這不是解決問(wèn)題的辦法。

          posted on 2006-08-24 17:34 Binary 閱讀(157) 評(píng)論(0)  編輯  收藏


          只有注冊(cè)用戶(hù)登錄后才能發(fā)表評(píng)論。


          網(wǎng)站導(dǎo)航:
           
          主站蜘蛛池模板: 铁岭县| 保靖县| 东阳市| 舟山市| 富民县| 宁晋县| 鄂尔多斯市| 鱼台县| 怀集县| 中牟县| 阿拉善右旗| 怀宁县| 南和县| 珠海市| 永春县| 育儿| 广东省| 灵山县| 北京市| 陆丰市| 天津市| 禹城市| 马龙县| 佛学| 临江市| 林甸县| 弋阳县| 犍为县| 泾阳县| 庆阳市| 和田县| 河东区| 正镶白旗| 阜新市| 乌审旗| 麻栗坡县| 康马县| 万年县| 宁武县| 龙州县| 利川市|