在上一部分中,介紹了兩個結構型的模式:Bridge和Decorator。這一部分的內容,將會接著上面的講解,繼續我們的設計模式之旅。
這一部分,除了還會介紹一個結構型的Composite模式之外,還會有兩個行為模式登場。實際上在前面的內容中,我們已經接觸到行為模式了:Observer和Command就是兩個典型的行為模式。行為模式更多的注重於演算法和對象建間職責的分配,也就是說,它會更多的關注於這個模式系統之類的各對象協作間的語義,以及在對象間進行通訊的流量控制。
Composite模式
毫無疑問的,AWT中的Component-Container體系就是一個很好的Composite模式的例子。Container繼承於Component,而Container中有可以包含有多個Component,因為Container實際上也是Component,因而Container也可以包含Container。這樣通過Component-Container結構的對象組合,形成一個樹狀的階層。這也就是Composite模式所要做的。
Composite模式是為了簡化編程而提出的,一般的在編程的時候,如果嚴格的區分Component和Container的話,有時候會帶來許多不便,而且這些往往是沒有必要的。比如,我要在一個Container中放置一個Component,我並不需要知道這個Component到底是一個Container,或者就是一個一般的Component,在父級容器中所要做的,只是記錄一個Component的引用,在需要的時候調用Component的繪製方法來顯示這個Component。當這個Component確實是一個Container的時候,它可以通過Container重載後的繪製方法,完成對這個容器的顯示,並把繪製訊息傳遞給到它的子物件去。也就是說,對一個父級容器而言,它並不不關心,其子物件到底是一個Component,還是一個Container。它需要將Component和Container統一對待。
圖十一:Composite模式的類圖
Composite模式比較簡單,實現起來也不複雜,但是有一定的局限性。比如,在處理樹的時候,我們往往需要處理三類對象:子樹,頁節點和非頁節點。而在Composite模式中對於子樹和非分葉節點的區分並不明顯,而是把他們合成為一個Composite對象了。而且在GOF給出的Composite的模式中,對於添加,刪除子節點等屬於Composite對象的的方法,是放在了Component對象中的,這雖然在實現的時候可以區分開來,但容易造成一些概念上的誤解。
由上所敘,我們可以提出一個改進了的Composite模式,引入子樹對象,從而將子樹和非分葉節點分開,如所示:
圖十二:Composite模式的一種變體
雖然將Composite從Component類層次中分離出來,但並沒有損害Composite模式的內涵。這樣做不一定就會比上面的那個要好,各有不同的應用,不過有時候用這樣的方法來處理子樹要容易些,概念上也更為清晰。
下面的代碼,給出了一個Composite模式簡單的Java實現:
public abstract class Component{
public abstract void operation();
public void add(Component component){};
public void remove(Component component){};
}
import java.util.*;
public class Composite extends Component{
String name;
ArrayList children = new ArrayList();
public Composite(String name){
this.name = name;
}
public void add(Component component){
children.add(component);
}
public void remove(Component component){
children.remove(component);
}
public void operation(){
System.out.println(name);
Iterator iterator = children.iterator();
while(iterator.hasNext()){
Component child = (Component)iterator.next();
child.operation();
}
}
}
public class Leaf extends Component{
String name;
public Leaf(String name){
this.name = name;
}
public void operation(){
System.out.println(name);
}
}
Strategy模式
Strategy模式主要用來將演算法實現從類中分離出來,並封裝在一個單獨的類中。更簡單的說,對象與其行為(behaviour)這本來緊密聯絡的兩部分被解耦,分別放在了兩個不同的類中。這使得對同一個行為,可以方便的在任何時候切換不同的實現演算法。而通過對策略的封裝,為其提供統一的介面,也可以很容易的引入新的策略。
AWT的LayoutManager,是Strategy模式的一個例子。對於GUI而言,每個組件(Component)在容器中(Container)的排放是需要遵循一定的演算法的。通常的方法是使用絕對座標,就像VB,Delphi之類的工具所作的那樣,記錄每個組件在容器中的位置。這當然會帶來一些問題,比如在表單縮放的時候,就需要手工編碼改變組件的大小和位置,以使得原來的比例得以儲存。而在AWT中,引入了布局管理器(LayoutManager)的概念,使得布局的方法大大豐富,編碼過程也變得簡單。
一個容器,比如Applet,Panel等,僅僅記錄其包含的組件,而布局管理器中封裝了對容器中組件進行布局的演算法,具體地說,就是指明容器中組件的位置和尺寸的大小。通過布局管理器,你只需要確定想放置的組件間的相對位置即可,這一方面簡化編碼,另一方面也有助於實現軟體的平台無關性。
圖十三:AWT中的容器和布局管理器的關係
每一個容器均有一個布局管理器,當容器需要布置它的組件時,它調用布局管理器的方法布置容器內的組件。LayoutManager2繼承於LayoutManager,提供更為細緻的布局功能,它可以讓布局管理器為組件加上約束條件已確定組件如何被布置。例如,為了確定組件被擺放在邊框內的位置,BorderLayout在它的組件上加上方向指示。
特別的,通過實現LayoutManager或者LayoutManager2介面,可以很容易實現自訂的布局策略。
回到模式的話題上來,如果有幾個很相似的類,其區別僅僅是在個別行為上的動作不同,這時候就可以考慮使用Strategy模式。這樣,通過策略組合,將原來的多個類精簡為一個帶有多個策略的類。這很符合OO設計的原則:找到變化的部分,並將其封裝起來!Strategy模式同樣的為子類繼承提供了一個好的替代方案,當使用繼承機制的時候,行為的改變是靜態,你指能夠改變一次--而策略是動態,可以在任何時候,切換任何次數。更為重要的是,策略對象可以在不同的環境中被不同的對象所共用。以布局管理器為例,雖然每一個容器只有一個布局管理器,但是一個布局管理器可以為多個容器工作。
圖十四:Strategy模式的類圖
Strategy模式也有一些缺點,比如,應用程式必須知道所有的策略對象,並從中選者其一。而且在策略對象被使用的時候,它和Context對象之間通常是緊耦合的,Context對象必須為策略對象提供與具體演算法相關的資料或者其它的東西,而這些資料的傳遞可能並不能夠風裝載抽象地策略類中,因為並不是所有的演算法都會需要這些資料的。另外,因為策略對象通常由應用程式所建立,Context對象並不能夠控制Strategy的生命期,而在概念上,這個策略應該從屬於Context對象,其生命期不應該超出Context的範圍對象。
通常的,Strategy很容易和Bridge模式相混淆。確實,他們有著很相近的結構,但是,他們卻是為解決不同的問題而設計的。Strategy模式注重於演算法的封裝,而Bridge模式注重於分離抽象和實現,為一個抽象體系提供不同的實現。
Iterator模式
Iterator模式用來規格化對某一資料結構的遍曆介面。
JDK中在Collection Framework中引入了Iterator介面,提供對一個Collection的遍曆。每一個Collection類中都定義有從Collection介面中繼承而來的iterator()方法,來得到一個Iterator對象,我們稱之為遍曆器,Iterator介面很簡單:
hasNext():用來判斷在遍曆器中是否還有下一個元素。
next():返回遍曆器中的下一個元素。
remove():在被遍曆的Collection類中刪除最後被返回的那個對象。
我們就以最為常用的Vector為例,看看在Collection Framework中,Iterator模式是如何被實現的。在此之前,我們需要先瞭解一些Vector和Collection Framework的結構。
Collection介面作為這個Framework的基礎,被所有其它的集合類所繼承或者實現。對Collection介面,有一個基本的實現是抽象類別AbstractCollection,它實現了大部分與具體資料結構無關的操作。比如判斷一個對象是否存在於這個集合類中的contains()方法:
public boolean contains(Object o) {
Iterator e = iterator();
if (o==null) {
while (e.hasNext())
if (e.next()==null)
return true;
} else {
while (e.hasNext())
if (o.equals(e.next()))
return true;
}
return false;
}
而這其中調用的iterator()方法是一個抽象方法,有賴於具體的資料結構的實現。但是對於這個containers()方法而言,並不需要知道具體的Iterator實現,而只需要知道它所提供的介面,能夠完成某類任務既可,這就是抽象類別中抽象方法的作用。其它的在AbstractCollection中實現的非抽象方法,大部分都是依賴於抽象方法iterator()方法所提供的Iterator介面來實現的。這種設計方法是引入抽象類別的一個關鍵所在,值得仔細領悟。
List介面繼承Collection介面,提供對列表集合類的抽象;對應的AbstractList類繼承AbstractCollection,並實現了List介面,作為List的一個抽象基類。它對其中非抽象方法的實現,也大抵上與AbstractCollection相同,這兒不再贅敘。
而對應於Collection的Iterator,List有其自己的ListIterator,ListIterator繼承於Iterator,並添加了一些專用於List遍曆的方法:
boolean hasPrevious():判斷在列表中當前元素之前是否存在有元素。
Object previous():返回列表中當前元素之前的元素。
int nextIndex():
int previousIndex():
void set(Object o):
void add(Object o):
ListIterator針對List,提供了更為強勁的功能介面。在AbstractList中,實現了具體的iterator()方法和listIterator()方法,我們來看看這兩個方法是如何?的:
public Iterator iterator() {
return new Itr(); //Itr是一個內部類
}
private class Itr implements Iterator {
int cursor = 0;//Iterator的計數器,指示當前調用next()方法時會被返回的元素的位置
int lastRet = -1;//指示剛剛通過next()或者previous()方法被返回的元素的位置,-1
//表示剛剛調用的是remove()方法刪除了一個元素。
//modCount是定義在AbstractList中的欄位,指示列表被修改的次數。Iterator用//這個值來檢查其封裝的列表是否被其他方法所非法修改。
int expectedModCount = modCount;
public boolean hasNext() {
return cursor != size();
}
public Object next() {
try {
//get方法仍然是一個抽象方法,依賴於具體的子類實現
Object next = get(cursor);
//檢查列表是否被不正確的修改
checkForComodification();
lastRet = cursor++;
return next;
} catch(IndexOutOfBoundsException e) {
checkForComodification();
throw new NoSuchElementException();
}
}
public void remove() {
if (lastRet == -1)
throw new IllegalStateException();
checkForComodification();
try {
//同樣remove(int)也依賴於具體的子類實現
AbstractList.this.remove(lastRet);
if (lastRet < cursor)
cursor--;
lastRet = -1;
expectedModCount = modCount;
} catch(IndexOutOfBoundsException e) {
throw new ConcurrentModificationException();
}
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
這兒的設計技巧和上面一樣,都是使用抽象方法來實現一個具體的操作。抽象方法作為最後被實現的內容,依賴於具體的子類。抽象類別看起來很像是一個介於介面和子類之間的一個東西。
從設計上來講,有人建議所有的類都應該定義成介面的形式,這當然有其道理,但多少有些極端。當你需要最大的靈活性的時候,應該使用介面,而抽象類別卻能夠提供一些預設的操作,最大限度的統一子類。抽象類別在許多應用程式框架(Application Framework)中有著很重要的作用。例如,在一個架構中,可以用抽象類別來實現一些預設的服務比如訊息處理等等。這些抽象類別能夠讓你很容易並且自然的把自己的應用嵌入到架構中去。而對於依賴於每個應用具體實現的方法,可以通過定義抽象方法來引入到架構中。
其實在老版本的JDK中也有類似的概念,被稱為Enumeration。Iterator其實與Enmeration功能上很相似,只是多了刪除的功能。用Iterator不過是在名字上變得更為貼切一些。模式的另外一個很重要的功用,就是能夠形成一種交流的語言(或者說文化)。有時候,你說Enumeration大家都不明白,說Iterator就都明白了。
小結:
這部分介紹了三個模式:Composite,Strategy和Iterator。Composite是一個結構性的模式,用來協調整體和局部的關係,使之能夠被統一的安排在一個樹形的結構中,並簡化了編程。Strategy模式與Bridge模式在結構上很相似,但是與Bridge不同在於,它是一個行為模式,更側重於結構的語義以及演算法的實現。它使得程式能夠在不同的演算法之間自由方便的作出選擇,並能夠在運行時切換到其他的演算法,很大程度上增加了程式的靈活性。Iterator模式提供統一的介面操作來實現對一個資料結構的遍曆,使得當資料結構的內部演算法發生改變時,客戶代碼不需要任何的變化,只需要改變相應的Iterator實現,就可以無縫的整合在原來的程式中。
參考資料
- 設計模式:可複用物件導向軟體的基礎 機械工業出版社
- Java2類庫增補版 機械工業出版社
- Java2圖形設計:AWT卷 機械工業出版社
- 可視化物件導向建模技術 北京航天航空工業大學出版社
- DK1.3原始碼
- UML使用者指南 機械工業出版社