組合還是繼承,這是一個問題
——由模式談面向對象的原則之多用組合、少用繼承
剛剛接觸模式或者學習模式的人,經常會有這樣的問題,為什么模式是成功的呢?很多人都會說模式是經驗的積累,當然是正確的。可是經驗為什么偏偏就證明了這種模式是正確的呢?這其中起用作的就是面向對象的基本原則。正是因為模式都或多或少的符合了面向對象的基本原則,所以模式才成為我們面向對象的設計和編碼過程中不敗的法則。那么什么是面向對象的基本原則呢?這就是我們將要一一講到的問題。
單純的講到一個個的原則,就是那么的寥寥幾句,非常的簡單,但又是非常抽象的,難以理解。怎么辦?
任何的理論,只要有生動的例子來講解或證明,就能極大的幫助理解。所以我們準備從一個個的生動的例子來闡述我們的面向對象的基本原則。講那些例子呢?上面我們說到,模式都是極大的遵從了這些原則的,那么我們把模式作為例子,來說明這些原則,不是我們信手拈來的嗎?
現在我們說說其中的一個原則:對類的功能的擴展,要多用組合,少用繼承。
對于類的擴展,在面向對象的編程過程中,我們首先想到的是類的繼承,由子類繼承父類,從而完成了對子類功能的擴展。但是,面向對象的原則告訴我們,對類的功能的擴展要多用組合,而少用繼承。其中的原因有以下幾點:
第一、 子類對父類的繼承是全部的公有和受保護的繼承,這使得子類可能繼承了對子類無用甚至有害的父類的方法。換句話說,子類只希望繼承父類的一部分方法,怎么辦?
第二、 實際的對象千變萬化,如果每一類的對象都有他們自己的類,盡管這些類都繼承了他們的父類,但有些時候還是會造成類的無限膨脹。
第三、 繼承的子類,實際上需要編譯期確定下來,這滿足不了需要在運行內才能確定對象的情況。而組合卻可以比繼承靈活得多,可以在運行期才決定某個對象。
嗨!光說這么多一二三有什么用,我們就是想看看實際情況是不是像上面說的那樣呢?還是來看看實際的例子吧!
現在我們需要這樣一個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<map.size();i++)
{
System.out.println(map.getValue(i));
}
測試結果為:
111
190
132
正是我們所需要看到的結果。如此說來,這個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<list.length;i++)
{
System.out.println(list[i]);
}
運行的結果如下:
132
111
190
哎喲,怎么回事啊?與上面的順序不對了。你朋友過來找你,說你寫的代碼怎么不對啊?你很吃驚,說把代碼給我看看。于是你看到了上面的代碼。你大罵道,混蛋,怎么不是用我的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());
通過上面的例子,我們可以看到組合的又一個很優越的好處:能夠在運行期創建新的對象。如上面我們的加冰咖啡,我們沒有這個類,卻能通過組合在運行期創建該對象,這的確大大的增加了我們程序的靈活性。
如果咖啡店的老板再要求你增加加壓咖啡,你就不會再擔心了,只給他增加了一個類就解決了所有的問題。
本文來自CSDN博客,轉載請標明出處:http://blog.csdn.net/hivon/archive/2006/01/19/583558.aspx
posted on 2009-06-14 19:04
Frank_Fang 閱讀(394)
評論(0) 編輯 收藏 所屬分類:
Design Pattern