java 為什么說多用組合,少用繼承?


 

對類的功能的擴展,要多用組合,少用繼承

組合:新的類由現有對象所組成。

繼承:按照現有類的類型來創建新類,無需改變現有類的形式,采用現有類的形式並在其中添加新代碼。

當繼承現有類型時,也就創造新的類型,這個新類型不僅包括現有類型的所有成員(盡管private成員被隱藏起來並且不可訪問,),更重要的是它復制了基類的接口,也就是說所有可以發送給基類對象(父類)的消息同時也可以發送給導出(子類)對象。由於通過發送給類的消息類型可知類的類型,所以這也就意味着導出類與基類有相同的類型。

 


對於類的擴展,在面向對象的編程過程中,我們首先想到的是類的繼承,由子類繼承父類,從而完成了對子類功能的擴展。但是,面向對象的原則告訴我們,對類的功能的擴展要多用組合,而少用繼承。其中的原因有以下幾點:
第一、子類對父類的繼承是全部的公有和受保護的繼承,這使得子類可能繼承了對子類無用甚至有害的父類的方法。換句話說,子類只希望繼承父類的一部分方法,怎么辦?
第二、實際的對象千變萬化,如果每一類的對象都有他們自己的類,盡管這些類都繼承了他們的父類,但有些時候還是會造成類的無限膨脹。
第三、 繼承的子類,實際上需要編譯期確定下來,這滿足不了需要在運行內才能確定對象的情況。而組合卻可以比繼承靈活得多,可以在運行期才決定某個對象。
嗨!光說這么多一二三有什么用,我們就是想看看實際情況是不是像上面說的那樣呢?還是來看看實際的例子吧!
現在我們需要這樣一個HashMap,它除了能按常規的Map那樣取值,如get(Object obj)。還能按位取值,像ArrayList那樣,按存入對象對的先后順序取值。
對於這樣一個問題,我們首先想到的是做一個類,它繼承了HashMap類,然后用一個ArrayList屬性來保存存入的key,我們按key的位來取值,代碼如下:

Java代碼  收藏代碼

  1. public class ListMap extends HashMap {  
  2. private List list;  
  3. public ListMap() {  
  4.          super();  
  5.           this.list = new ArrayList();  
  6. }  
  7. public Object put(Object key,Object value)  
  8. {  
  9.          if(list.contains(key))  
  10.           {  
  11.                  list.remove(key);  
  12.           }  
  13.          this.list.add(key);  
  14.           return super.put(key,value);  
  15. }  
  16. public Object getKey(int i)  
  17. {  
  18.           return this.list.get(i);  
  19. }  
  20. public Object getValue(int i)  
  21. {  
  22.           return this.get(getKey(i));  
  23. }  
  24. public int size()  
  25. {  
  26.           return this.list.size();  
  27. }  
  28. }  
  29. 這個ListMap類對HashMap作了一定的擴展,很簡單就實現了上面我們所要求的功能。然后我們對該類做一下測試:  
  30. ListMap map = new ListMap();  
  31.          map.put("a","111");  
  32.          map.put("v","190");  
  33.          map.put("d","132");  
  34.           for(int i=0;i<map.size();i++)  
  35.           {  
  36.                  System.out.println(map.getValue(i));  
  37.           }  

測試結果為:
111
190
132
正是我們所需要看到的結果。如此說來,這個ListMap類就可以放心的使用了嗎?有實現了這樣功能的類,你的同事或朋友也可能把這個類拿來使用一下,他可能寫出來如下的代碼:

 

Java代碼  收藏代碼

  1. ListMap map = new ListMap();  
  2.          map.put("a","111");  
  3.          map.put("v","190");  
  4.          map.put("d","132");  
  5.          String[] list = (String[])map.values().toArray(new String[0]);  
  6.           for(int i=0;i<list.length;i++)  
  7.           {  
  8.                  System.out.println(list[i]);  
  9.           }  

運行的結果如下:
132
111
190
哎喲,怎么回事啊?與上面的順序不對了。你朋友過來找你,說你寫的代碼怎么不對啊?你很吃驚,說把代碼給我看看。於是你看到了上面的代碼。你大罵道,混蛋,怎么不是用我的getValue方法啊?你朋友搔搔頭道,values方法不是一樣的嗎?你也沒告訴我不能用啊?
通過上面的例子,我們看到了繼承的第一個危害:繼承不分青紅皂白的把父類的公有和受保護的方法統統繼承下來。如果你的子類沒有對一些方法重寫,就會對你的子類產生危害。上面的ListMap類,你沒有重寫繼承自HashMap類的values方法,而該方法仍然是按HashMap的方式取值,沒有先后順序。這時候,如果在ListMap類的對象里使用該方法取得的值,就沒有實現我們上面的要求。
接上面的那個例子,你聽了朋友的抱怨,搖搖頭,想想也是,不能怪他。你只得把values方法在ListMap類重寫一遍,然后又嘀咕着,我是不是該把HashMap類的公有方法在ListMap類里全部重寫?很多方法根本沒有必要用到啊?……
對了,很多方法在ListMap里根本不必用到,但是你用繼承的話,還不得不在ListMap里重寫它們。如果用組合的話,就沒有上面的煩惱了:

 

Java代碼  收藏代碼

  1. public class MyListMap {  
  2. private HashMap map;  
  3. private List list;  
  4. public MyListMap()  
  5. {  
  6.          this.map = new HashMap();  
  7.           this.list = new ArrayList();  
  8. }  
  9. public Object put(Object key,Object value)  
  10. {  
  11.          if(list.contains(key))  
  12.           {  
  13.                  list.remove(key);  
  14.           }  
  15.          this.list.add(key);  
  16.           return this.map.put(key,value);  
  17. }  
  18. public Object getKey(int i)  
  19. {  
  20.           return this.list.get(i);  
  21. }  
  22. public Object getValue(int i)  
  23. {  
  24.           return this.map.get(getKey(i));  
  25. }  
  26. public int size()  
  27. {  
  28.           return this.list.size();  
  29. }  
  30. }  

這樣,你的朋友就只能使用你的getKey和getValue方法了。如果他向你抱怨沒有values方法,你盡可以滿足他的要求,給他添加上那個方法,而不必擔心可能還有方法沒有被重寫了。
我們來看Adapter模式,該模式的目的十分簡單:我手里握有一些實現了WhatIHave接口的實現,可我覺得這些實現的功能不夠用,我還需要從Resource類里取一些功能來為我所用。Adapter模式的解決方法如下:

 

Java代碼  收藏代碼

  1. public interface WhatIHave  
  2. {  
  3.           public void g();  
  4. }  
  5. public class Resource  
  6. {  
  7.           public void f()  
  8.           {  
  9.                ……  
  10.           }  
  11.           public void h()  
  12.           {  
  13.                ……  
  14.           }  
  15. }  

上面是兩個基礎類,很明顯,我們所要的類既要有g()方法,也要有f()和h()方法。

 

Java代碼  收藏代碼

  1. Public class WhatIWant implements WhatIHave  
  2. {  
  3.           private Resource res;  
  4.           public WhatIWant()  
  5.           {  
  6.                  res = new Resource();  
  7. }  
  8. public void g()  
  9. {  
  10.        ……  
  11. }  
  12. public void f()  
  13. {  
  14.          this.res.f();  
  15. }  
  16. public void h()  
  17. {  
  18.          this.res.h();  
  19. }  
  20. }  

上面就是一個Adapter模式最簡單的解決問題的思路。我們主要到,對於Resource類,該模式使用的是組合,而不是繼承。這樣使用是有多個原因:第一,Java不支持多重繼承,如果需要使用好幾個不同的Resource類,則繼承解決不了問題。第二,如果Resource類還有一個方法:k(),我們在WhatIWant類里使用不上的話,繼承就給我們造成多余方法的問題了。
如果說Adapter模式對組合的應用的目的十分簡單明確,那么Decorator模式對組合的應用簡直就是令人叫絕。
讓我們還是從Decorator模式的最佳例子說起,咖啡店需要售賣各種各樣的咖啡:黑咖啡、加糖、加冰、加奶、加巧克力等等。顧客要買咖啡,他可以往咖啡任意的一種或幾種產品。
這個問題一提出來,我們最容易想到的是繼承。比如說加糖咖啡是一種咖啡,滿足ia a的句式,很明顯,加糖咖啡是咖啡的一個子類。於是,我們馬上可以賦之行動。對於咖啡我們做一個咖啡類:Coffee,咖啡加糖:SugarCoffee,咖啡加冰:IceCoffee,咖啡加奶:MilkCoffee,咖啡加巧克力:ChocolateCoffee,咖啡加糖加冰:SugarIceCoffee……
哎喲,我們發現問題了:這樣下去我們的類好多啊。可是咖啡店的老板還不放過我們,他又逼着我們增加蒸汽咖啡、加壓咖啡,結果我們發現,每增加一種新的類型,我們的類好像是成幾何級數增加,我們都要瘋了。
這個例子向我們展示了繼承的第二個缺點,會使得我們的子類快速的膨脹下去,達到驚人的數量。
怎么辦?我們的Decorator模式找到了組合來為我們解決問題。下面我們來看看Decorator模式是怎么來解決這個問題的。
首先是它們的共同接口:

 

Java代碼  收藏代碼

  1. package decorator;  
  2.   
  3. interface Product {  
  4. public double money();  
  5. }  
  6.   
  7. //咖啡類:  
  8. class Coffee implements Product {  
  9. public double money() {  
  10.     return 12;  
  11. }  
  12. }  
  13.   
  14. //加糖:  
  15. class Sugar implements Product {  
  16. private Product product;  
  17.   
  18. public Sugar(Product product) {  
  19.     this.product = product;  
  20. }  
  21.   
  22. public double money() {  
  23.     return product.money() + 2;  
  24. }  
  25. }  
  26.   
  27. //加冰:  
  28. class Ice implements Product {  
  29. private Product product;  
  30.   
  31. public Ice(Product product) {  
  32.     this.product = product;  
  33. }  
  34.   
  35. public double money() {  
  36.     return product.money() + 1.5;  
  37. }  
  38. }  
  39.   
  40. //加奶:  
  41. class Milk implements Product {  
  42. private Product product;  
  43.   
  44. public Milk(Product product) {  
  45.     this.product = product;  
  46. }  
  47.   
  48. public double money() {  
  49.     return product.money() + 4.0;  
  50. }  
  51. }  
  52.   
  53. //加巧克力:  
  54. class Chocolate implements Product {  
  55. private Product product;  
  56.   
  57. public Chocolate(Product product) {  
  58.     this.product = product;  
  59. }  
  60.   
  61. public double money() {  
  62.     return product.money() + 5.5;  
  63. }  
  64. }  
  65. public class DecoratorModel{  
  66. public static void main(String [] args){  
  67.     Product coffee = new Coffee();  
  68.     Product sugarCoffee = new Sugar(coffee);  
  69.     Product sugarmilkCoffee = new Milk(sugarCoffee);  
  70.     System.out.println("加糖咖啡:"+sugarCoffee.money());  
  71.     System.out.println("加糖加奶咖啡:"+sugarmilkCoffee.money());  
  72. }  
  73. }  

 


我們來看客戶端的調用。
如果顧客想要黑咖啡,調用如下:
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());
通過上面的例子,我們可以看到組合的又一個很優越的好處:能夠在運行期創建新的對象。如上面我們的加冰咖啡,我們沒有這個類,卻能通過組合在運行期創建該對象,這的確大大的增加了我們程序的靈活性。
如果咖啡店的老板再要求你增加加壓咖啡,你就不會再擔心了,只給他增加了一個類就解決了所有的問題。

 

 

關於組合和繼承的總結:

 

 

組合和繼承都允許在新的類中放置子對象,組合是顯式地這樣做,而繼承則是隱式地這樣做。

繼承是"is-a"關系

 

 

組合是"has-a"關系

到底該使用組合還是繼承:問一問自己是否需要從新類向基類進行向上轉型,若必要則繼承是必要的,反之需要思考。

 


1)組合(has-a)關系可以顯式地獲得被包含類(繼承中稱為父類)的對象,而繼承(is-a)則是隱式地獲得父類的對象,被包含類和父類對應,而組合外部類和子類對應。


2)組合關系在運行期決定,而繼承關系在編譯期就已經決定了。


3)組合是在組合類和被包含類之間的一種松耦合關系,而繼承則是父類和子類之間的一種緊耦合關系。

4)當選擇使用組合關系時,在組合類中包含了外部類的對象,組合類可以調用外部類必須的方法,而使用繼承關系時,父類的所有方法和變量都被子類無條件繼承,子類不能選擇。

5)最重要的一點,使用繼承關系時,可以實現類型的回溯,即用父類變量引用子類對象,這樣便可以實現多態,而組合沒有這個特性。

6)還有一點需要注意,如果你確定復用另外一個類的方法永遠不需要改變時,應該使用組合,因為組合只是簡單地復用被包含類的接口,而繼承除了復用父類的接口外,它甚至還可以覆蓋這些接口,修改父類接口的默認實現,這個特性是組合所不具有的。

7)從邏輯上看,組合最主要地體現的是一種整體和部分的思想,例如在電腦類是由內存類,CPU類,硬盤類等等組成的,而繼承則體現的是一種可以回溯的父子關系,子類也是父類的一個對象。

 

8)這兩者的區別主要體現在類的抽象階段,在分析類之間的關系時就應該確定是采用組合還是采用繼承。

 

9)引用網友的一句很經典的話應該更能讓大家分清繼承和組合的區別:組合可以被說成“我請了個老頭在我家里干活” ,繼承則是“我父親在家里幫我干活"。

 10)組合技術通常是用於在新類中使用現有類的功能而非它的接口的情形。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM