設計模式之迭代器模式(Iterator Pattern)
這篇部落格,我們要詳細講解的是迭代器模式(Iterator Pattern),將要講解的內容有:迭代器模式
的定義,作用,詳細設計分析等方面。
一、Pattern name
迭代器模式(Iterator Pattern) : 提供一種方法順序訪問一個聚合物件中各個元素,而又不需暴露該物件的內部表示。——《設計模式 可複用面向物件軟體的基礎》
二、Problem
在我們實際程式設計的過程中,經常會遇到下面這種情況:
當我們需要遍歷某個集合的時候,常常呼叫某個類的一個方法,返回一個集合型別的資料,如下:
private ArrayList<Book> bookList = new ArrayList<Book>();
public ArrayList<Book> getBookList(){
return bookList;
}
然後,再依據呼叫方法返回的ArrayList型別的bookList進行遍歷操作。但是,這種方式將底層資料的實現方式和資料結構暴露出來,如果未來有一天想要改變底層資料結構,換為陣列或者其他集合類進行實現,那麼原來呼叫這個方法以實現遍歷的那個地方就會報錯。
那麼,我們如何設計,才能消除以後可能發生的底層資料結構變化對整個程式的影響呢?如何設計,才能不把底層實現的資料結構暴露出來呢?這就要用到迭代器模式(Iterator Pattern)。
三、Solution
首先,我們來看一下,迭代器模式的UML類圖結構:
上圖是最基本,最簡單情況的類圖,從圖中右下角處的Client Code我們可以看出,Client在需要遍歷目標集合的時候,無需知道具體是用哪種資料結構實現的,它只需要通過介面Aggregate,獲取一個實現了這個介面的類的物件,並呼叫createIterator()方法,就會獲得目標集合的一個迭代器(Iterator),而這個迭代器提供了first(),next(),isDone(),currentItem()等方法,Client通過這些方法就可以成功完成對目標集合的遍歷操作。
此時,如果我們想改變集合的底層實現的資料結構型別:
那麼直接替換掉ConcreteAggregate就可以了,當然了,從圖中我們可以看出ConcreteAggregate和ConcreteIterator耦合程度很嚴重,所以在更換ConcreteAggregate的時候,很可能也需要適當調整ConcreteIterator的實現程式碼,甚至是需要重新編寫ConcreteIterator的程式碼。
如果我們想要增加一種底層實現的資料結構型別:
那麼可以增加一個ConcreteAggregate2,然後同樣實現Aggregate抽象介面方法,然後同樣由於ConcreteAggregate和ConcreteIterator耦合度很嚴重,很有可能需要增加一個全新的ConcreteIterator的實現類,但是實現的遍歷方式依舊和原來相同。所以很有可能會出現如下所示的變化:
你可能會想,我只是增加了一個底層資料結構型別,但是遍歷策略沒有發生改變,可不可以通過某種巧妙的設計,不用改變右邊的Iterator實現類,就可以實現資料結構型別的增加呢?
其實,我在學習的時候,也產生了這樣的疑問,經過我查閱資料以及分析討論,我覺得答案是:不一定。因為不同的底層資料結構型別之間,有的差異很小,有的差異很大。對於差異很小的,我們只要提出一個抽象父類或介面,比如圖中的List,我們把它改為一個抽象類或介面,其中定義了一些ListIterator實現需要用到的方法,然後再建立兩個不同的實現List介面的實現類或List抽象類的繼承子類ArrayList和LinkedList(或者是MusicList和MovieList等),這樣雖然將底層資料結構型別在原來的ArrayList的基礎上增加了一種新的實現方式:LinkedList,但是此時的遍歷策略並不需要做出任何改變,只要新增加的資料結構型別實現了List中定義的方法,那就可以很容易做到和ListIterator完美結合。
但是我們要注意,如果新增加的資料結構型別與原有的資料結構型別相差太多,無法實現父類或介面(List)定義的方法,那麼就必須增加一個Iterator的實現類,雖然實現的遍歷策略是相同的,沒有發生變化。
如果我們想要改變現有的遍歷策略
那麼可以改變原有的ConcreteIterator的實現類,來實現新的遍歷方式,當然了這個實現類是在具體制定的ConcreteAggregate的基礎上完成的,受到ConcreteAggregate的影響。
如果我們想要增加一種遍歷策略:
那麼可以增加一個ConcreteIterator的實現類,來實現新的遍歷方式,當然了這個實現類是在具體制定的ConcreteAggregate的基礎上完成的,受到ConcreteAggregate的影響。
四、Consequences
當你需要訪問一個聚集物件,而且不管這些物件是什麼都需要遍歷的時候,你就應該考慮用迭代器模式。當你需要對聚集有多種方式遍歷時,可以考慮用迭代器模式。
迭代器模式作用:
訪問一個聚合物件的內容而無需暴露它的內部表示。
支援對聚合物件的多種遍歷。
為遍歷不同的聚合結構提供一個統一的介面。
它支援以不同的方式遍歷一個聚合。
迭代器簡化了聚合的介面。
在同一個聚合上可以有多個遍歷。
五、迭代器模式的典型應用
其實,迭代器模式在很多高階語言中都有體現,甚至有完整全套的包裝實現。在Java中,就有很多地方應用了迭代器模式。我們來舉一個例子(對Java原始碼進行了適當的修改):
迭代介面:Iterator介面
public interface Iterator
{
//判斷是否存在下一個元素
public abstract boolean hasNext();
//返回下一個可用的元素
public abstract Object next();
//移除當前元素
public abstract void remove();
}
容器介面:List介面(相當於Aggregate),定義了iterator()方法(相當於createIterator)
public interface List
{
...
//取得對所有元素的遍歷。可以通過Iterator提供的方法遍歷集合的元素
public abstract Iterator iterator();
...
}
容器介面List的實現類ArrayList(相當於ConcreteAggregate)
public class ArrayList implements List {
...
//負責建立具體迭代器角色的工廠方法
public Iterator iterator() {
//把遍歷委讓給Iterator的實現類Itr。
return new Itr();
}
...
}
迭代介面Iterator的實現類
private class Itr implements Iterator {
...
}
六、常見疑問解答及其他
疑問1:可能有的人會有這樣的疑問,為什麼不設計成這樣:
在Client中:
Aggregate a = new ConcreteAggregate();
Iterator i = new ConcreteIterator(a);
通過這種方式,就可以愉快地在Client中任意組合不同的集合實現類和不同的遍歷策略類,如果有三種集合實現類,有兩種遍歷策略類,就可以實現6種巧妙組合。多完美,為什麼不這樣做呢?
答案是這樣的:如果這樣做呢,一,Client需要知道Aggregate,ConcreteAggregate,Iterator,ConcreteIterator,對這四個都會產生相應的關聯關係,增加了整個系統的關係複雜度。二,也是最重要的,如果把集合類和遍歷策略的匹配放到Client中,那麼Client必須知道,每個遍歷策略具體是什麼?而且,它需要準確地知道哪個集合類可以和哪個遍歷策略類組合,哪個集合類不能和哪個策略類組合。因為我們不得不承認,並不是每個遍歷策略類在所有的集合類上都是可行的。而且,要Client來負責匹配分析,幾個類之間的關係,特別是和Client之間的關係會非常複雜,耦合度會非常高。
疑問2:如何構建一個健壯的迭代器
所謂健壯的迭代器是指:能夠保證插入和刪除操作不會干擾遍歷,且不需拷貝該聚合。
我們知道在遍歷一個聚合的同時更改這個聚合可能是危險的,如果在遍歷聚合的時候增加或刪除該聚合元素,可能會導致兩次訪問同一個元素或者遺漏掉某個元素。一個簡單的解決方法是拷貝該集合,並對該拷貝實施遍歷,但一般來說這樣做代價太高。所以,我們需要努力構造一個健壯的迭代器。
當然了,有許多方法來實現健壯的迭代器,其中大多數需要向這個聚合註冊該迭代器。當插入或刪除元素時,該聚合要麼調整迭代器的內部狀態,要麼在內部的維護額外的資訊以保證正確的遍歷。
七、總結
當然了,迭代器模式的思想博大精深。我們需要學習和分析的絕不是僅僅上面討論的這些,除此之外,還有很多很多。如果你有新的見解,不同的分析,歡迎大家一起討論,一起進步。