由模式談面向對象的原則之多用組合、少用繼承

 

組合還是繼承,這是一個問題

                                              ——由模式談面向對象的原則之多用組合、少用繼承
        剛剛接觸模式或者學習模式的人,經常會有這樣的問題,爲什麼模式是成功的呢?很多人都會說模式是經驗的積累,當然是正確的。可是經驗爲什麼偏偏就證明了這種模式是正確的呢?這其中起用作的就是面向對象的基本原則。正是因爲模式都或多或少的符合了面向對象的基本原則,所以模式才成爲我們面向對象的設計和編碼過程中不敗的法則。那麼什麼是面向對象的基本原則呢?這就是我們將要一一講到的問題。
 
       單純的講到一個個的原則,就是那麼的寥寥幾句,非常的簡單,但又是非常抽象的,難以理解。怎麼辦?
任何的理論,只要有生動的例子來講解或證明,就能極大的幫助理解。所以我們準備從一個個的生動的例子來闡述我們的面向對象的基本原則。講那些例子呢?上面我們說到,模式都是極大的遵從了這些原則的,那麼我們把模式作爲例子,來說明這些原則,不是我們信手拈來的嗎?
現在我們說說其中的一個原則:對類的功能的擴展,要多用組合,少用繼承。

      對於類的擴展,在面向對象的編程過程中,我們首先想到的是類的繼承,由子類繼承父類,從而完成了對子類功能的擴展。但是,面向對象的原則告訴我們,對類的功能的擴展要多用組合,而少用繼承。其中的原因有以下幾點:
第一、            子類對父類的繼承是全部的公有和受保護的繼承,這使得子類可能繼承了對子類無用甚至有害的父類的方法。換句話說,子類只希望繼承父類的一部分方法,怎麼辦?
 
第二、            實際的對象千變萬化,如果每一類的對象都有他們自己的類,儘管這些類都繼承了他們的父類,但有些時候還是會造成類的無限膨脹。

第三、            繼承的子類,實際上需要編譯期確定下來,這滿足不了需要在運行內才能確定對象的情況。而組合卻可以比繼承靈活得多,可以在運行期才決定某個對象。
嗨!光說這麼多一二三有什麼用,我們就是想看看實際情況是不是像上面說的那樣呢?還是來看看實際的例子吧!
現在我們需要這樣一個HashMap,它除了能按常規的Map那樣取值,如get(Object obj)。還能按位取值,像ArrayList那樣,按存入對象對的先後順序取值。
對於這樣一個問題,我們首先想到的是做一個類,它繼承了HashMap類,然後用一個ArrayList屬性來保存存入的key,我們按key的位來取值,代碼如下:

 
這個ListMap類對HashMap作了一定的擴展,很簡單就實現了上面我們所要求的功能。然後我們對該類做一下測試:
 
測試結果爲:
111
190
132
正是我們所需要看到的結果。如此說來,這個ListMap類就可以放心的使用了嗎?有實現了這樣功能的類,你的同事或朋友也可能把這個類拿來使用一下,他可能寫出來如下的代碼:
 
運行的結果如下:
132
111
190
      哎喲,怎麼回事啊?與上面的順序不對了。你朋友過來找你,說你寫的代碼怎麼不對啊?你很吃驚,說把代碼給我看看。於是你看到了上面的代碼。你大罵道,混蛋,怎麼不是用我的getValue方法啊?你朋友搔搔頭道,values方法不是一樣的嗎?你也沒告訴我不能用啊?
通過上面的例子,我們看到了繼承的第一個危害:繼承不分青紅皁白的把父類的公有和受保護的方法統統繼承下來。如果你的子類沒有對一些方法重寫,就會對你的子類產生危害。上面的ListMap類,你沒有重寫繼承自HashMap類的values方法,而該方法仍然是按HashMap的方式取值,沒有先後順序。這時候,如果在ListMap類的對象裏使用該方法取得的值,就沒有實現我們上面的要求。
接上面的那個例子,你聽了朋友的抱怨,搖搖頭,想想也是,不能怪他。你只得把values方法在ListMap類重寫一遍,然後又嘀咕着,我是不是該把HashMap類的公有方法在ListMap類裏全部重寫?很多方法根本沒有必要用到啊?……
對了,很多方法在ListMap里根本不必用到,但是你用繼承的話,還不得不在ListMap裏重寫它們。如果用組合的話,就沒有上面的煩惱了:
 
        這樣,你的朋友就只能使用你的getKey和getValue方法了。如果他向你抱怨沒有values方法,你儘可以滿足他的要求,給他添加上那個方法,而不必擔心可能還有方法沒有被重寫了。
我們來看Adapter模式,該模式的目的十分簡單:我手裏握有一些實現了WhatIHave接口的實現,可我覺得這些實現的功能不夠用,我還需要從Resource類裏取一些功能來爲我所用。Adapter模式的解決方法如下:
 
上面是兩個基礎類,很明顯,我們所要的類既要有g()方法,也要有f()和h()方法。
 
上面就是一個Adapter模式最簡單的解決問題的思路。我們主要到,對於Resource類,該模式使用的是組合,而不是繼承。這樣使用是有多個原因:第一,Java不支持多重繼承,如果需要使用好幾個不同的Resource類,則繼承解決不了問題。第二,如果Resource類還有一個方法:k(),我們在WhatIWant類裏使用不上的話,繼承就給我們造成多餘方法的問題了。
如果說Adapter模式對組合的應用的目的十分簡單明確,那麼Decorator模式對組合的應用簡直就是令人叫絕。
讓我們還是從Decorator模式的最佳例子說起,咖啡店需要售賣各種各樣的咖啡:黑咖啡、加糖、加冰、加奶、加巧克力等等。顧客要買咖啡,他可以往咖啡任意的一種或幾種產品。
這個問題一提出來,我們最容易想到的是繼承。比如說加糖咖啡是一種咖啡,滿足ia a的句式,很明顯,加糖咖啡是咖啡的一個子類。於是,我們馬上可以賦之行動。對於咖啡我們做一個咖啡類:Coffee,咖啡加糖:SugarCoffee,咖啡加冰:IceCoffee,咖啡加奶:MilkCoffee,咖啡加巧克力:ChocolateCoffee,咖啡加糖加冰:SugarIceCoffee……
哎喲,我們發現問題了:這樣下去我們的類好多啊。可是咖啡店的老闆還不放過我們,他又逼着我們增加蒸汽咖啡、加壓咖啡,結果我們發現,每增加一種新的類型,我們的類好像是成幾何級數增加,我們都要瘋了。
這個例子向我們展示了繼承的第二個缺點,會使得我們的子類快速的膨脹下去,達到驚人的數量。
怎麼辦?我們的Decorator模式找到了組合來爲我們解決問題。下面我們來看看Decorator模式是怎麼來解決這個問題的。
首先是它們的共同接口:
 
我們來看客戶端的調用。
如果顧客想要黑咖啡,調用如下:
Product prod = new Coffee();
System.out.println(prod.money());
如果顧客需要加冰咖啡,調用如下:
Product prod = new Ice(new Coffee());
System.out.println(prod.money());
如果顧客想要加糖加冰加奶加巧克力咖啡,調用如下:
Product prod = new Chocolate(new Milk(new Ice(new Sugar())));
System.out.println(prod.money());
通過上面的例子,我們可以看到組合的又一個很優越的好處:能夠在運行期創建新的對象。如上面我們的加冰咖啡,我們沒有這個類,卻能通過組合在運行期創建該對象,這的確大大的增加了我們程序的靈活性。
如果咖啡店的老闆再要求你增加加壓咖啡,你就不會再擔心了,只給他增加了一個類就解決了所有的問題。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章