1. 程式人生 > >組合還是繼承,這是一個問題?——由模式談面向物件的原則之多用組合、少用繼承

組合還是繼承,這是一個問題?——由模式談面向物件的原則之多用組合、少用繼承

                                                      組合還是繼承,這是一個問題                                              ——由模式談面向物件的原則之多用組合、少用繼承剛剛接觸模式或者學習模式的人,經常會有這樣的問題,為什麼模式是成功的呢?很多人都會說模式是經驗的積累,當然是正確的。可是經驗為什麼偏偏就證明了這種模式是正確的呢?這其中起用作的就是面向物件的基本原則。正是因為模式都或多或少的符合了面向物件的基本原則,所以模式才成為我們面向物件的設計和編碼過程中不敗的法則。那麼什麼是面向物件的基本原則呢?這就是我們將要一一講到的問題。單純的講到一個個的原則,就是那麼的寥寥幾句,非常的簡單,但又是非常抽象的,難以理解。怎麼辦?任何的理論,只要有生動的例子來講解或證明,就能極大的幫助理解。所以我們準備從一個個的生動的例子來闡述我們的面向物件的基本原則。講那些例子呢?上面我們說到,模式都是極大的遵從了這些原則的,那麼我們把模式作為例子,來說明這些原則,不是我們信手拈來的嗎?現在我們說說其中的一個原則:對類的功能的擴充套件,要多用組合,少用繼承。對於類的擴充套件,在面向物件的程式設計過程中,我們首先想到的是類的繼承,由子類繼承父類,從而完成了對子類功能的擴充套件。但是,面向物件的原則告訴我們,對類的功能的擴充套件要多用組合,而少用繼承。其中的原因有以下幾點:第一、子類對父類的繼承是全部的公有和受保護的繼承,這使得子類可能繼承了對子類無用甚至有害的父類的方法。換句話說,子類只希望繼承父類的一部分方法,怎麼辦?第二、實際的物件千變萬化,如果每一類的物件都有他們自己的類,儘管這些類都繼承了他們的父類,但有些時候還是會造成類的無限膨脹。第三、繼承的子類,實際上需要編譯期確定下來,這滿足不了需要在執行內才能確定物件的情況。而組合卻可以比繼承靈活得多,可以在執行期才決定某個物件。嗨!光說這麼多一二三有什麼用,我們就是想看看實際情況是不是像上面說的那樣呢?還是來看看實際的例子吧!現在我們需要這樣一個HashMap,它除了能按常規的Map那樣取值,如get(Object obj)。還能按位取值,像ArrayList那樣,按存入物件對的先後順序取值。對於這樣一個問題,我們首先想到的是做一個類,它繼承了HashMap類,然後用一個ArrayList屬性來儲存存入的key,我們按key的位來取值,程式碼如下:public class ListMap extends HashMap {private List list;public ListMap() {       super();        this.list = new ArrayList();}public Object put(Object key,Object value){       if(list.contains(key))        {               list.remove(key);        }       this.list.add(key);        return super.put(key,value);}public Object getKey(int i){        return this.list.get(i);}public Object getValue(int i){        return this.get(getKey(i));
}public int size(){        return this.list.size();}}這個ListMap類對HashMap作了一定的擴充套件,很簡單就實現了上面我們所要求的功能。然後我們對該類做一下測試:ListMap map = new ListMap();       map.put("a","111");       map.put("v","190");       map.put("d","132");        for(int i=0;i        {               System.out.println(map.getValue(i));        }
測試結果為:111190132正是我們所需要看到的結果。如此說來,這個ListMap類就可以放心的使用了嗎?有實現了這樣功能的類,你的同事或朋友也可能把這個類拿來使用一下,他可能寫出來如下的程式碼:ListMap map = new ListMap();       map.put("a","111");       map.put("v","190");       map.put("d","132");       String[] list = (String[])map.values().toArray(new String[0]);        for(int i=0;i        {
               System.out.println(list[i]);        }執行的結果如下:132111190哎喲,怎麼回事啊?與上面的順序不對了。你朋友過來找你,說你寫的程式碼怎麼不對啊?你很吃驚,說把程式碼給我看看。於是你看到了上面的程式碼。你大罵道,混蛋,怎麼不是用我的getValue方法啊?你朋友搔搔頭道,values方法不是一樣的嗎?你也沒告訴我不能用啊?通過上面的例子,我們看到了繼承的第一個危害:繼承不分青紅皁白的把父類的公有和受保護的方法統統繼承下來。如果你的子類沒有對一些方法重寫,就會對你的子類產生危害。上面的ListMap類,你沒有重寫繼承自HashMap類的values方法,而該方法仍然是按HashMap的方式取值,沒有先後順序。這時候,如果在ListMap類的物件裡使用該方法取得的值,就沒有實現我們上面的要求。接上面的那個例子,你聽了朋友的抱怨,搖搖頭,想想也是,不能怪他。你只得把values方法在ListMap類重寫一遍,然後又嘀咕著,我是不是該把HashMap類的公有方法在ListMap類裡全部重寫?很多方法根本沒有必要用到啊?……對了,很多方法在ListMap里根本不必用到,但是你用繼承的話,還不得不在ListMap裡重寫它們。如果用組合的話,就沒有上面的煩惱了:public class MyListMap {private HashMap map;private List list;public MyListMap(){       this.map = new HashMap();        this.list = new ArrayList();}public Object put(Object key,Object value){       if(list.contains(key))        {               list.remove(key);        }       this.list.add(key);        return this.map.put(key,value);}public Object getKey(int i){        return this.list.get(i);}public Object getValue(int i){        return this.map.get(getKey(i));}public int size(){        return this.list.size();}}這樣,你的朋友就只能使用你的getKey和getValue方法了。如果他向你抱怨沒有values方法,你儘可以滿足他的要求,給他新增上那個方法,而不必擔心可能還有方法沒有被重寫了。我們來看Adapter模式,該模式的目的十分簡單:我手裡握有一些實現了WhatIHave介面的實現,可我覺得這些實現的功能不夠用,我還需要從Resource類裡取一些功能來為我所用。Adapter模式的解決方法如下:public interface WhatIHave{        public void g();}public class Resource{        public void f()        {               ……        }        public void h()        {               ……        }}上面是兩個基礎類,很明顯,我們所要的類既要有g()方法,也要有f()和h()方法。Public class WhatIWant implements WhatIHave{        private Resource res;        public WhatIWant()        {               res = new Resource();}public void g(){       ……}public void f(){       this.res.f();}public void h(){       this.res.h();}}上面就是一個Adapter模式最簡單的解決問題的思路。我們主要到,對於Resource類,該模式使用的是組合,而不是繼承。這樣使用是有多個原因:第一,Java不支援多重繼承,如果需要使用好幾個不同的Resource類,則繼承解決不了問題。第二,如果Resource類還有一個方法:k(),我們在WhatIWant類裡使用不上的話,繼承就給我們造成多餘方法的問題了。如果說Adapter模式對組合的應用的目的十分簡單明確,那麼Decorator模式對組合的應用簡直就是令人叫絕。讓我們還是從Decorator模式的最佳例子說起,咖啡店需要售賣各種各樣的咖啡:黑咖啡、加糖、加冰、加奶、加巧克力等等。顧客要買咖啡,他可以往咖啡任意的一種或幾種產品。這個問題一提出來,我們最容易想到的是繼承。比如說加糖咖啡是一種咖啡,滿足ia a的句式,很明顯,加糖咖啡是咖啡的一個子類。於是,我們馬上可以賦之行動。對於咖啡我們做一個咖啡類:Coffee,咖啡加糖:SugarCoffee,咖啡加冰:IceCoffee,咖啡加奶:MilkCoffee,咖啡加巧克力:ChocolateCoffee,咖啡加糖加冰:SugarIceCoffee……哎喲,我們發現問題了:這樣下去我們的類好多啊。可是咖啡店的老闆還不放過我們,他又逼著我們增加蒸汽咖啡、加壓咖啡,結果我們發現,每增加一種新的型別,我們的類好像是成幾何級數增加,我們都要瘋了。這個例子向我們展示了繼承的第二個缺點,會使得我們的子類快速的膨脹下去,達到驚人的數量。怎麼辦?我們的Decorator模式找到了組合來為我們解決問題。下面我們來看看Decorator模式是怎麼來解決這個問題的。首先是它們的共同介面:public interface Product{        public double money();}咖啡類:public class Coffee implements Product{        public double money()        {               return 12;}}加糖:public class Sugar implements Product{        private Product product;        public Sugar(Product product)        {               this.product = product;}public double money(){       return product.money+2;}}加冰:public class Ice implements Product{        private Product product;        public Ice(Product product)        {               this.product = product;}public double money(){       return product.money+1.5;}}加奶:public class Milk implements Product{        private Product product;        public Milk(Product product)        {               this.product = product;}public double money(){       return product.money+4.0;}}加巧克力:public class Chocolate implements Product{        private Product product;        public Chocolate(Product product)        {               this.product = product;}public double money(){       return product.money+5.5;}}我們來看客戶端的呼叫。如果顧客想要黑咖啡,呼叫如下: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());通過上面的例子,我們可以看到組合的又一個很優越的好處:能夠在執行期建立新的物件。如上面我們的加冰咖啡,我們沒有這個類,卻能通過組合在執行期建立該物件,這的確大大的增加了我們程式的靈活性。如果咖啡店的老闆再要求你增加加壓咖啡,你就不會再擔心了,只給他增加了一個類就解決了所有的問題。