設計模式系列之迭代器模式(Iterator Pattern)——遍歷聚合對象中的元素
說明:設計模式系列文章是讀劉偉
所著《設計模式的藝術之道(軟件開發人員內功修鍊之道)》
一書的閱讀筆記。個人感覺這本書講的不錯,有興趣推薦讀一讀。詳細內容也可以看看此書作者的博客//blog.csdn.net/LoveLion/article/details/17517213
。
模式概述
模式定義
在軟件開發中,經常需要使用聚合對象來存儲一系列數據。聚合對象有兩個職責:
- 存儲數據
- 遍曆數據
從依賴性來看,前者是聚合對象的基本職責,而後者既是可變化的,又是可分離的。因此,可以將遍曆數據的行為從聚合對象中分離出來,封裝在一個被稱之為迭代器
的對象中,由迭代器
來提供遍歷聚合對象內部數據的行為,這將簡化聚合對象的設計,更符合單一職責的要求。
迭代器模式(
Iterator Pattern
): 提供一種方法來訪問聚合對象,而不用暴露這個對象的內部存儲。迭代器模式是一種對象行為型模式。
模式結構圖
在迭代器模式結構中包含聚合和迭代器兩個層次結構,考慮到系統的靈活性和可擴展性,在迭代器模式中應用了工廠方法
模式,其模式結構如圖所示。
在迭代器模式結構圖中包含如下幾個角色:
Iterator
(抽象迭代器
):它定義了訪問和遍曆元素的接口,聲明了用於遍曆數據元素的方法,例如:用於獲取第一個元素的first()
方法,用於訪問下一個元素的next()
方法,用於判斷是否還有下一個元素的hasNext()
方法,用於獲取當前元素的currentItem()
方法等,在具體迭代器中將實現這些方法。ConcreteIterator
(具體迭代器
):它實現了抽象迭代器接口,完成對聚合對象的遍歷,同時在具體迭代器中通過游標來記錄在聚合對象中所處的當前位置,在具體實現時,游標通常是一個表示位置的非負整數。Aggregate
(抽象聚合類
):它用於存儲和管理元素對象,聲明一個createIterator()
方法用於創建一個迭代器對象,充當抽象迭代器工廠角色。ConcreteAggregate
(具體聚合類
):它實現了在抽象聚合類中聲明的createIterator()
方法,該方法返回一個與該具體聚合類對應的具體迭代器ConcreteIterator
實例。
模式偽代碼
在抽象迭代器
中聲明了用於遍歷聚合對象中所存儲元素的方法
interface Iterator {
public void first(); //將游標指向第一個元素
public void next(); //將游標指向下一個元素
public boolean hasNext(); //判斷是否存在下一個元素
public Object currentItem(); //獲取游標指向的當前元素
}
在具體迭代器
中將實現抽象迭代器
聲明的遍曆數據的方法
class ConcreteIterator implements Iterator {
private ConcreteAggregate objects; //維持一個對具體聚合對象的引用,以便於訪問存儲在聚合對象中的數據
private int cursor; //定義一個游標,用於記錄當前訪問位置
public ConcreteIterator(ConcreteAggregate objects) {
this.objects=objects;
}
public void first() { ...... }
public void next() { ...... }
public boolean hasNext() { ...... }
public Object currentItem() { ...... }
}
聚合類
用於存儲數據並負責創建迭代器對象
,最簡單的抽象聚合類
代碼如下所示
interface Aggregate {
Iterator createIterator();
}
具體聚合類作為抽象聚合類的子類,一方面負責存儲數據,另一方面實現了在抽象聚合類中聲明的工廠方法createIterator()
,用於返回一個與該具體聚合類對應的具體迭代器對象,代碼如下所示
class ConcreteAggregate implements Aggregate {
//......
public Iterator createIterator() {
return new ConcreteIterator(this);
}
//......
}
模式改進
在迭代器模式結構圖中,我們可以看到具體迭代器類
和具體聚合類
之間存在雙重關係,其中一個關係為關聯關係
,在具體迭代器
中需要維持一個對具體聚合對象的引用,該關聯關係的目的是訪問存儲在聚合對象中的數據
,以便迭代器能夠對這些數據進行遍歷操作。
除了使用關聯關係
外,為了能夠讓迭代器可以訪問到聚合對象中的數據,我們還可以將迭代器類設計為聚合類的內部類
,JDK中的迭代器類就是通過這種方法來實現的,如下AbstractList
類代碼片段所示
package java.util;
public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {
//...
public boolean add(E e) {...}
abstract public E get(int index);
public E set(int index, E element) {
throw new UnsupportedOperationException();
}
//...
public Iterator<E> iterator() {
return new Itr();
}
// 這裡用內部類可直接訪問到聚合對象中的數據
private class Itr implements Iterator<E> {
int cursor = 0;
int lastRet = -1;
int expectedModCount = modCount;
public boolean hasNext() {
return cursor != size();
}
public E next() {
//...
}
public void remove() {
//...
}
}
}
模式應用
模式在JDK中的應用
在JDK中,Collection
接口和Iterator
接口充當了迭代器模式的抽象層,分別對應於抽象聚合類
和抽象迭代器
,而Collection
接口的子類充當了具體聚合類
,如圖列出了JDK中部分與List有關的類及它們之間的關係。
在JDK中,實際情況比上圖要複雜很多,List
接口除了繼承Collection
接口的iterator()
方法外,還增加了新的工廠方法listIterator()
,專門用於創建ListIterator
類型的迭代器,在List
的子類LinkedList
中實現了該方法,可用於創建具體的ListIterator
子類ListItr
的對象。
既然有了iterator()
方法,為什麼還要提供一個listIterator()
方法呢?這兩個方法的功能不會存在重複嗎?幹嘛要多此一舉?由於在Iterator
接口中定義的方法太少,只有三個,通過這三個方法只能實現正向遍歷
,而有時候我們需要對一個聚合對象進行逆向遍歷
等操作,因此在JDK的ListIterator
接口中聲明了用於逆向遍歷的hasPrevious()
和previous()
等方法,如果客戶端需要調用這兩個方法來實現逆向遍歷,就不能再使用iterator()
方法來創建迭代器了,因為此時創建的迭代器對象是不具有這兩個方法的。具體可細讀java.util.ListIterator
模式在開源項目中的應用
開源項目中用到遍歷的地方,很多地方都會用到迭代器設計模式。這裡只提一個org.apache.kafka.clients.consumer.ConsumerRecords
。詳情可以找kafka client
源碼閱讀。
public class ConsumerRecords<K, V> implements Iterable<ConsumerRecord<K, V>> {
@Override
public Iterator<ConsumerRecord<K, V>> iterator() {
return new ConcatenatedIterable<>(records.values()).iterator();
}
private static class ConcatenatedIterable<K, V> implements Iterable<ConsumerRecord<K, V>> {
private final Iterable<? extends Iterable<ConsumerRecord<K, V>>> iterables;
public ConcatenatedIterable(Iterable<? extends Iterable<ConsumerRecord<K, V>>> iterables) {
this.iterables = iterables;
}
@Override
public Iterator<ConsumerRecord<K, V>> iterator() {
return new AbstractIterator<ConsumerRecord<K, V>>() {
Iterator<? extends Iterable<ConsumerRecord<K, V>>> iters = iterables.iterator();
Iterator<ConsumerRecord<K, V>> current;
public ConsumerRecord<K, V> makeNext() {
while (current == null || !current.hasNext()) {
if (iters.hasNext())
current = iters.next().iterator();
else
return allDone();
}
return current.next();
}
};
}
}
}
模式總結
迭代器模式是一種使用頻率非常高的設計模式,通過引入迭代器可以將數據的遍歷功能從聚合對象中分離出來,聚合對象只負責存儲數據,而遍曆數據由迭代器來完成。由於很多編程語言的類庫都已經實現了迭代器模式,因此在實際開發中,我們只需要直接使用Java、C#等語言已定義好的迭代器即可,迭代器已經成為我們操作聚合對象的基本工具之一。
- 主要優點
迭代器模式的主要優點如下:
(1) 它支持以不同的方式遍歷一個聚合對象,在同一個聚合對象上可以定義多種遍歷方式。在迭代器模式中只需要用一個不同的迭代器來替換原有迭代器即可改變遍歷算法,我們也可以自己定義迭代器的子類以支持新的遍歷方式。
(2) 迭代器簡化了聚合類。由於引入了迭代器,在原有的聚合對象中不需要再自行提供數據遍歷等方法,這樣可以簡化聚合類的設計。
(3) 在迭代器模式中,由於引入了抽象層,增加新的聚合類和迭代器類都很方便,無須修改原有代碼,滿足開閉原則
的要求。
(4) 不管實現如何變化,都可以使用Iterator
,引入Iterator
後可以將遍歷與實現分離開來。對於遍歷者來說我們可能只會用到hasNext()
和next()
方法,如果底層數據存儲結構變了(舉個例子原來用List
存儲,需求變動後改為用Map
存儲),對於上層調用者來說可能完全是透明的,遍歷者並不關心你具體如何存儲。
- 主要缺點
迭代器模式的主要缺點如下:
(1) 由於迭代器模式將存儲數據和遍曆數據的職責分離,增加新的聚合類需要對應增加新的迭代器類,類的個數成對增加,這在一定程度上增加了系統的複雜性。
(2) 抽象迭代器的設計難度較大,需要充分考慮到系統將來的擴展,例如JDK內置迭代器Iterator
就無法實現逆向遍歷,如果需要實現逆向遍歷,只能通過其子類ListIterator
等來實現,而ListIterator
迭代器無法用於操作Set
類型的聚合對象。在自定義迭代器時,創建一個考慮全面的抽象迭代器並不是件很容易的事情。
- 適用場景
在以下情況下可以考慮使用迭代器模式:
(1) 訪問一個聚合對象的內容而無須暴露它的內部表示。將聚合對象的訪問與內部數據的存儲分離,使得訪問聚合對象時無須了解其內部實現細節。
(2) 需要為一個聚合對象提供多種遍歷方式。
(3) 為遍歷不同的聚合結構提供一個統一的接口,在該接口的實現類中為不同的聚合結構提供不同的遍歷方式,而客戶端可以一致性地操作該接口。