邊城愚人

          如果我不在邊城,我一定是在前往邊城的路上。

            BlogJava :: 首頁 :: 新隨筆 :: 聯系 :: 聚合  :: 管理 ::
            31 隨筆 :: 0 文章 :: 96 評論 :: 0 Trackbacks

          ??? ??? 在匆忙之際理清消除實現繼承和面向接口編程這樣兩個大問題可不是一件容易的事情,尤其考慮到自身的認識水平。坦白的說,這又是一篇“炒冷飯”的文章,但這“冷飯”又確實不好炒。因此,在閱讀了這篇文章之后,你可要批判地接受(拒絕)我的觀點,盡管我的觀點也是來自于別人的觀點。

          ??? ??? 繼承是面向對象中很重要的概念。如果考慮到Java語言特性,繼承分為兩種:接口繼承和實現繼承。這只是技術層面的問題,即便C++中不存在接口的概念,但它的虛基類實際上也相當于接口。對于OO的初學者來說,他們很希望自己的程序中出現大量的繼承,因為這樣看起來很OO。但濫用繼承會帶來很多問題,盡管有時候我們又不得不使用繼承解決問題。

          ??? ??? 相比于接口繼承,實現繼承的問題要更多,它會帶來更多的耦合問題。但接口繼承也是有問題的,這是繼承本身的問題。實現繼承的很多問題出于其自身實現上,因此這里重點討論實現繼承的問題。

          ??? ??? 舉個例子(這個例子實在太老套了)。我要實現一個Stack類,我想當然地選擇Stack類繼承于ArrayList類(你也可以認為我很想OO些或者出于本性的懶惰);現在又有了新的需求,需要實現一個線程安全的Stack,我又定義了一個ConcurrentStack類繼承于Stack并覆蓋了Stack中的部分代碼。

          ??? ??? 因為Stack繼承于ArrayListStack不得不對外暴露出ArrayList所有的public方法,即便其中的某些方法對它可能是不需要的;甚至更糟的是,可能其中的某些方法能改變Stack的狀態,而Stack對這些改變并不知情,這就會造成Stack的邏輯錯誤。

          ??? ??? 如果我要在ArrayList中添加新的方法,這個方法就有可能在邏輯上破壞它的派生類StackConcurrentStack。因此在基類(父類)添加方法(修改代碼)時,必須檢查這些修改是否會對派生類產生影響;如果產生影響的話,就不得不對派生類做進一步的修改。如果類的繼承體系不是一個人完成的,或者是修改別人的代碼的情況下,很可能因為繼承產生難以覺察的BUG

          ??? ??? 問題還是有的。我們有時會見到這樣的基類,它的一些方法只是拋出異常,這意味著如果派生類支持這個方法就重寫它,否則就如父類一樣拋出異常表明其不支持這個方法的調用。我們也能見到它的一個變種,父類的方法是抽象的,但不是所有的子類都支持這個方法,不支持的方法就以拋出異常的方式表明立場。這種做法是很不友好和很不安全的,它們只能在運行時被“僥幸捕捉”,而很多漏網的異常方法可能會在某一天突然出現,讓人不知所措。


          ??? ??? 引起上面問題的很重要的原因便是基類和派生類之間的耦合。往往只是對基類做了小小的改動,卻不得不重構它們的所有的派生類,這就是臭名昭著的“脆弱的基類”問題。由于類之間的關系是存在的,因此耦合是不可避免的甚至是必要的。但在做OO設計時,當遇到如基類和派生類之間的強耦合關系,我們就要思量思量,是否一定需要繼承呢?是否會有其他的更優雅的替代方案呢?如果一定要學究的話,你會在很多書中會看到這樣的原則:如果兩個類之間是ISA關系,那么就使用繼承;如果兩個類之間是HasA的關系,那么就使用委派。很多時候這條原則是適用的,但ISA并不能做為使用繼承的絕對理由。有時為了消除耦合帶來的問題,使用委派等方法會更好地封裝實現細節。繼承有時會對外及向下暴露太多的信息,在GOF的設計模式中,有很多模式的目的就是為了消除繼承。

          ??? ??? 關于何時采用繼承,一個重要的原則是確定方法是否能夠共享。比如DAO ,可以將通用的CRUD 方法定在一個抽象DAO 中,具體的DAO 都派生自這個抽象類。嚴格的說,抽象DAO 和派生的DAO 實現并不具有IS A 關系,我們只是為了避免重復的方法定義和實現而作出了這一技術上的選擇。可以說,使用接口還是抽象類的原則是,如果多個派生類的方法內容沒有共同的地方,就用接口作為抽象;如果 多個派生類 的方法含有共同的地方,就用抽象類作為抽象。當這一原則不適用于接口繼承,如果出現接口繼承,就會相應地有實現繼承(基類更多的是抽象類)。

          ??? ??? 現在說說面向接口編程。在眾多的敏捷方法中,面向接口編程總是被大師們反復的強調。面向接口編程,實際上是面向抽象編程,將抽象概念和具體實現相隔離。這一原則使得我們擁有了更高層次的抽象模型,在面對不斷變更的需求時,只要抽象模型做的好,修改代碼就要容易的多。但面向接口編程不意味著非得一個接口對應一個類,過多的不必要的接口也可能帶來更多的工作量和維護上的困難。

          ??? ??? 相比于繼承,OO中多態的概念要更重要。一個接口可以對應多個實現類,對于聲明為接口類型的方法參數、類的字段,它們要比實現類更易于擴展、穩定,這也是多態的優點。假如我以實現類作為方法參數定義了一個方法void doSomething(ArrayList list)但如果領導哪天覺得 ArrayList不如LinkedList更好用,我將不得不將方法重構為void doSomething(LinkedList list),相應地要在所有調用此方法的地方修改參數類型(很遺憾地,我連對象創建也是采用ArrayList list = new ArrayList()方式,這將大大增加我的修改工作量)。如果領導又覺得用list存儲數據不如set好的話,我將再一次重構方法,但這一次我變聰明了,我將方法定義為void doSomething(Set set)創建對象的方式改為Set set = new HashSet()。但這樣仍不夠,如果領導又要求將set改回list怎么辦?所以我應該將方法重構為void doSomething(Collection collection)Collection的抽象程度最高,更易于替換具體的實現類。即便需要List或者Set固有的特性,我也可以做向下類型轉換解決問題,盡管這樣做并不安全。

          ??? ??? 面向接口編程最重要的價值在于隱藏實現,將抽象的實現細節封裝起來而不對外開放,封裝這對于Java EE 中的分層設計和框架設計尤其重要。但即便在編程時使用了接口,我們也需要將接口和實現對應起來,這就引出如何創建對象的問題。在創建型設計模式中,單例、工廠方法(模板方法)、抽象工廠等模式都是很好的解決辦法。現在流行的控制反轉(也叫依賴注入)模式是以聲明的方式將抽象與實現連接起來,這既減少了單調的工廠類也更易于單元測試。

          ??? ??? 做個總結吧。盡管我竭力批駁繼承的不好鼓吹接口的好,但這并不是絕對的。濫用繼承、濫用接口都會帶來問題。做Java EE開發的很多朋友抱怨DAOService中一個接口一個類的實現方式,盡管它們似乎看起來已成為業界的最佳實踐之一。也許排除掉接口會使程序更“瘦”一些,但“瘦”并一定就“好”,需要根據項目的具體情況而定。關于繼承和接口的最佳實踐,各位看官還是需要自身的經驗積累和總結了。

          posted on 2007-09-03 10:08 kafka0102 閱讀(2715) 評論(5)  編輯  收藏 所屬分類: OO

          評論

          # re: 消除實現繼承和面向接口編程 2007-09-03 11:19 dennis
          傳統的OO教育中一直很強調繼承、多態,其實OO中最重要的是封裝的概念,封裝不僅僅是數據的封裝(最初級的封裝),如果將封裝推廣,抽象類或者說基類是對派生類的封裝(或者說隱藏),組合也是一種封裝,Adapter、state、Facade等模式更是封裝,在多線程編程中,封裝更是異常重要。  回復  更多評論
            

          # re: 消除實現繼承和面向接口編程 2007-09-03 21:35 Matthew Chen
          個人也覺得oo中會出現一些問題,這些問題隨著技術和思想的進步日漸凸現.

          但就本文的觀點,其實談不上是oo的封裝和繼承帶來的,很多往往是我們設計人員的疏忽.

          比如第一個stack的例子,ArrayList是明顯的jfc,繼承自它是完全沒有必要的,除非你的類在抽象空間中能夠真正代表ArrayList類的一個特殊子類集合,面向對象優先考慮抽象空間的完美而不是效率和實現空間的簡便,這是規則,也是出于ood的要求,在這個實例中,明顯應該用聚合而不是繼承.

          拋出異常的問題,對不支持的方法拋出異常過于拙劣了,完全可以在繼承樹的層次上加上抽象類,很好地分叉,而不是依賴有限的異常處理機制,異常處理在面向對象構架中算得上相當游離的一個功能了,盡管看似也有類的繼承關系,但獨立的捕獲和處理,并不適合作為業務邏輯的相關環節實現.

          oo的問題個人覺得很多在繼承層次的復雜性,jfc庫繼承層次過多,導致無法查知當前方法的具體調用棧位置——也就是具體方法的實現方式往往被復雜和繁冗的繼承和封裝掩蓋,讓人摸不找頭腦,不知道過程邏輯究竟在何處實現,也就無法最大限度地學習已有的類庫(當然,只是學習和修改,因為使用是不需要知道了,畢竟封裝就是為了屏蔽實現細節。)  回復  更多評論
            

          # re: 消除實現繼承和面向接口編程 2007-09-03 22:17 kafka0102
          @Matthew Chen
          是的,對于繁雜的對象模型,如果繼承層次太多就會很難把握具體的調用,也更難于修改。對于我舉的例子,也許并不漂亮,這里只是為了說明一下問題。實際上,我看到很多代碼有在基類拋出異常或子類拋出異常的情況,因為作者希望抽象出的公共操作只是對其某個或某幾個子類不適用,而作者因為實際需求又希望將方法作為公共方法。  回復  更多評論
            

          # re: 消除實現繼承和面向接口編程 2007-09-08 10:56 InPractice
          我就很反感一個接口一個實現的方式。當然這是以我的項目經驗為前提的(不排除目光偏狹的可能性)。
          這種預留的擴展余地,即將來可以加入其他的實現。在我的項目中事實上成為了“過度工程”的例子。因為到現在為止還是一個接口一個實現。倒是每次讀代碼是多了一個間接級別,麻煩了很多,效率降低。有人可能爭辯說,代碼讀起來麻煩點沒有關系,設計上還是優美的。我不同意這種觀點,現實中的代碼讀的次數遠遠超過寫的次數,讀代碼效率降低的影響不是一個所謂的優美的(在我看是華而不實)設計可以彌補的。
          在我看來,一個接口一個實現, 恰恰是當前流行的反模式。盡管可能在某些情況下有其合理性,但多數是對“面向接口編程”的濫用。  回復  更多評論
            

          # re: 消除實現繼承和面向接口編程 2008-01-26 22:40 nabie
          繼承還是應該以 IS-A 為原則。樓主的例子要實現一個 Stack 卻用 ArrayList 作為基類就明顯是一個濫用 OO 特性的例子,因為 Stack 并非是一種 ArrayList。正因為如此,才會有“可能其中的某些方法能改變Stack的狀態,而Stack對這些改變并不知情”的情況。并非使用了繼承就是 OO,程序語音中的繼承只是為了支持 OO 的繼承概念而提供的。但如果你在使用語音的繼承特性時并不按照 OO 的概念來使用,則即使你整個程序都布滿了各種 OO 特有的東西,你的程序也不是一個 OO 的程序。類的體系應該是合乎分類的邏輯的。如計算機包括個人電腦、服務器和大型機等,所以個人電腦、服務器和大型機等是個人電腦的子類,而個人電腦有分為臺式機和筆記本,所以臺式機和筆記本是個人電腦的子類,但他們并非是大型機,所以不是大型機的子類。你不能因為現在已經有大型機這個類,而臺式機、筆記本和大型機一樣都能計算,所以把它們作為大型機的子類,這樣一些對大型機的操作當然不能運用在臺式機和筆記本上。而你可以使他們并列,而提取出計算功能形成一個叫計算機的父類里,然后臺式機、筆記本和大型機都是計算機的子類。  回復  更多評論
            


          只有注冊用戶登錄后才能發表評論。


          網站導航:
           
          主站蜘蛛池模板: 大厂| 额济纳旗| 上高县| 承德市| 东丽区| 绵竹市| 饶阳县| 通许县| 光山县| 巴彦县| 阿巴嘎旗| 凉城县| 保定市| 离岛区| 梁平县| 三门峡市| 内黄县| 江源县| 浪卡子县| 太谷县| 屯昌县| 穆棱市| 朝阳区| 兴国县| 八宿县| 霍林郭勒市| 白朗县| 卢氏县| 筠连县| 乌拉特后旗| 千阳县| 康平县| 宁国市| 墨脱县| 宁明县| 曲松县| 陆良县| 蒙自县| 黎川县| 南京市| 绥芬河市|