邊城愚人

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

            BlogJava :: 首頁(yè) :: 新隨筆 :: 聯(lián)系 :: 聚合  :: 管理 ::
            31 隨筆 :: 0 文章 :: 96 評(píng)論 :: 0 Trackbacks

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

          ??? ??? 繼承是面向?qū)ο笾泻苤匾母拍睢H绻紤]到Java語(yǔ)言特性,繼承分為兩種:接口繼承和實(shí)現(xiàn)繼承。這只是技術(shù)層面的問(wèn)題,即便C++中不存在接口的概念,但它的虛基類實(shí)際上也相當(dāng)于接口。對(duì)于OO的初學(xué)者來(lái)說(shuō),他們很希望自己的程序中出現(xiàn)大量的繼承,因?yàn)檫@樣看起來(lái)很OO。但濫用繼承會(huì)帶來(lái)很多問(wèn)題,盡管有時(shí)候我們又不得不使用繼承解決問(wèn)題。

          ??? ??? 相比于接口繼承,實(shí)現(xiàn)繼承的問(wèn)題要更多,它會(huì)帶來(lái)更多的耦合問(wèn)題。但接口繼承也是有問(wèn)題的,這是繼承本身的問(wèn)題。實(shí)現(xiàn)繼承的很多問(wèn)題出于其自身實(shí)現(xiàn)上,因此這里重點(diǎn)討論實(shí)現(xiàn)繼承的問(wèn)題。

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

          ??? ??? 因?yàn)?font face="文鼎PL細(xì)上海宋Uni, serif">Stack繼承于ArrayListStack不得不對(duì)外暴露出ArrayList所有的public方法,即便其中的某些方法對(duì)它可能是不需要的;甚至更糟的是,可能其中的某些方法能改變Stack的狀態(tài),而Stack對(duì)這些改變并不知情,這就會(huì)造成Stack的邏輯錯(cuò)誤。

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

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


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

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

          ??? ??? 現(xiàn)在說(shuō)說(shuō)面向接口編程。在眾多的敏捷方法中,面向接口編程總是被大師們反復(fù)的強(qiáng)調(diào)。面向接口編程,實(shí)際上是面向抽象編程,將抽象概念和具體實(shí)現(xiàn)相隔離。這一原則使得我們擁有了更高層次的抽象模型,在面對(duì)不斷變更的需求時(shí),只要抽象模型做的好,修改代碼就要容易的多。但面向接口編程不意味著非得一個(gè)接口對(duì)應(yīng)一個(gè)類,過(guò)多的不必要的接口也可能帶來(lái)更多的工作量和維護(hù)上的困難。

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

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

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

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

          評(píng)論

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

          # re: 消除實(shí)現(xiàn)繼承和面向接口編程 2007-09-03 21:35 Matthew Chen
          個(gè)人也覺(jué)得oo中會(huì)出現(xiàn)一些問(wèn)題,這些問(wèn)題隨著技術(shù)和思想的進(jìn)步日漸凸現(xiàn).

          但就本文的觀點(diǎn),其實(shí)談不上是oo的封裝和繼承帶來(lái)的,很多往往是我們?cè)O(shè)計(jì)人員的疏忽.

          比如第一個(gè)stack的例子,ArrayList是明顯的jfc,繼承自它是完全沒(méi)有必要的,除非你的類在抽象空間中能夠真正代表ArrayList類的一個(gè)特殊子類集合,面向?qū)ο髢?yōu)先考慮抽象空間的完美而不是效率和實(shí)現(xiàn)空間的簡(jiǎn)便,這是規(guī)則,也是出于ood的要求,在這個(gè)實(shí)例中,明顯應(yīng)該用聚合而不是繼承.

          拋出異常的問(wèn)題,對(duì)不支持的方法拋出異常過(guò)于拙劣了,完全可以在繼承樹(shù)的層次上加上抽象類,很好地分叉,而不是依賴有限的異常處理機(jī)制,異常處理在面向?qū)ο髽?gòu)架中算得上相當(dāng)游離的一個(gè)功能了,盡管看似也有類的繼承關(guān)系,但獨(dú)立的捕獲和處理,并不適合作為業(yè)務(wù)邏輯的相關(guān)環(huán)節(jié)實(shí)現(xiàn).

          oo的問(wèn)題個(gè)人覺(jué)得很多在繼承層次的復(fù)雜性,jfc庫(kù)繼承層次過(guò)多,導(dǎo)致無(wú)法查知當(dāng)前方法的具體調(diào)用棧位置——也就是具體方法的實(shí)現(xiàn)方式往往被復(fù)雜和繁冗的繼承和封裝掩蓋,讓人摸不找頭腦,不知道過(guò)程邏輯究竟在何處實(shí)現(xiàn),也就無(wú)法最大限度地學(xué)習(xí)已有的類庫(kù)(當(dāng)然,只是學(xué)習(xí)和修改,因?yàn)槭褂檬遣恍枰懒耍吘狗庋b就是為了屏蔽實(shí)現(xiàn)細(xì)節(jié)。)  回復(fù)  更多評(píng)論
            

          # re: 消除實(shí)現(xiàn)繼承和面向接口編程 2007-09-03 22:17 kafka0102
          @Matthew Chen
          是的,對(duì)于繁雜的對(duì)象模型,如果繼承層次太多就會(huì)很難把握具體的調(diào)用,也更難于修改。對(duì)于我舉的例子,也許并不漂亮,這里只是為了說(shuō)明一下問(wèn)題。實(shí)際上,我看到很多代碼有在基類拋出異常或子類拋出異常的情況,因?yàn)樽髡呦M橄蟪龅墓膊僮髦皇菍?duì)其某個(gè)或某幾個(gè)子類不適用,而作者因?yàn)閷?shí)際需求又希望將方法作為公共方法。  回復(fù)  更多評(píng)論
            

          # re: 消除實(shí)現(xiàn)繼承和面向接口編程 2007-09-08 10:56 InPractice
          我就很反感一個(gè)接口一個(gè)實(shí)現(xiàn)的方式。當(dāng)然這是以我的項(xiàng)目經(jīng)驗(yàn)為前提的(不排除目光偏狹的可能性)。
          這種預(yù)留的擴(kuò)展余地,即將來(lái)可以加入其他的實(shí)現(xiàn)。在我的項(xiàng)目中事實(shí)上成為了“過(guò)度工程”的例子。因?yàn)榈浆F(xiàn)在為止還是一個(gè)接口一個(gè)實(shí)現(xiàn)。倒是每次讀代碼是多了一個(gè)間接級(jí)別,麻煩了很多,效率降低。有人可能爭(zhēng)辯說(shuō),代碼讀起來(lái)麻煩點(diǎn)沒(méi)有關(guān)系,設(shè)計(jì)上還是優(yōu)美的。我不同意這種觀點(diǎn),現(xiàn)實(shí)中的代碼讀的次數(shù)遠(yuǎn)遠(yuǎn)超過(guò)寫的次數(shù),讀代碼效率降低的影響不是一個(gè)所謂的優(yōu)美的(在我看是華而不實(shí))設(shè)計(jì)可以彌補(bǔ)的。
          在我看來(lái),一個(gè)接口一個(gè)實(shí)現(xiàn), 恰恰是當(dāng)前流行的反模式。盡管可能在某些情況下有其合理性,但多數(shù)是對(duì)“面向接口編程”的濫用。  回復(fù)  更多評(píng)論
            

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


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


          網(wǎng)站導(dǎo)航:
           
          主站蜘蛛池模板: 星子县| 额济纳旗| 叙永县| 阳西县| 新泰市| 阳泉市| 托里县| 濮阳县| 六枝特区| 宁武县| 寿宁县| 嘉禾县| 资源县| 上林县| 东兴市| 堆龙德庆县| 成都市| 商洛市| 怀来县| 洛浦县| 兴业县| 赤水市| 竹溪县| 延津县| 凤阳县| 景宁| 博乐市| 惠州市| 东山县| 南京市| 涪陵区| 乌拉特后旗| 类乌齐县| 辽阳县| 黎川县| 遂宁市| 西和县| 湛江市| 银川市| 兰考县| 蓬溪县|