設計模式系列之迭代器模式(Iterator Pattern)——遍歷聚合物件中的元素

行無際發表於2020-04-19

說明:設計模式系列文章是讀劉偉所著《設計模式的藝術之道(軟體開發人員內功修煉之道)》一書的閱讀筆記。個人感覺這本書講的不錯,有興趣推薦讀一讀。詳細內容也可以看看此書作者的部落格https://blog.csdn.net/LoveLion/article/details/17517213

模式概述

模式定義

在軟體開發中,經常需要使用聚合物件來儲存一系列資料。聚合物件有兩個職責:

  1. 儲存資料
  2. 遍歷資料

從依賴性來看,前者是聚合物件的基本職責,而後者既是可變化的,又是可分離的。因此,可以將遍歷資料的行為從聚合物件中分離出來,封裝在一個被稱之為迭代器的物件中,由迭代器來提供遍歷聚合物件內部資料的行為,這將簡化聚合物件的設計,更符合單一職責的要求。

迭代器模式(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有關的類及它們之間的關係。

Java集合框架中部分類結構圖

在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. 主要優點

迭代器模式的主要優點如下:

(1) 它支援以不同的方式遍歷一個聚合物件,在同一個聚合物件上可以定義多種遍歷方式。在迭代器模式中只需要用一個不同的迭代器來替換原有迭代器即可改變遍歷演算法,我們也可以自己定義迭代器的子類以支援新的遍歷方式。

(2) 迭代器簡化了聚合類。由於引入了迭代器,在原有的聚合物件中不需要再自行提供資料遍歷等方法,這樣可以簡化聚合類的設計。

(3) 在迭代器模式中,由於引入了抽象層,增加新的聚合類和迭代器類都很方便,無須修改原有程式碼,滿足開閉原則的要求。

(4) 不管實現如何變化,都可以使用Iterator,引入Iterator後可以將遍歷與實現分離開來。對於遍歷者來說我們可能只會用到hasNext()next()方法,如果底層資料儲存結構變了(舉個例子原來用List儲存,需求變動後改為用Map儲存),對於上層呼叫者來說可能完全是透明的,遍歷者並不關心你具體如何儲存。

  1. 主要缺點

迭代器模式的主要缺點如下:

(1) 由於迭代器模式將儲存資料和遍歷資料的職責分離,增加新的聚合類需要對應增加新的迭代器類,類的個數成對增加,這在一定程度上增加了系統的複雜性。

(2) 抽象迭代器的設計難度較大,需要充分考慮到系統將來的擴充套件,例如JDK內建迭代器Iterator就無法實現逆向遍歷,如果需要實現逆向遍歷,只能通過其子類ListIterator等來實現,而ListIterator迭代器無法用於操作Set型別的聚合物件。在自定義迭代器時,建立一個考慮全面的抽象迭代器並不是件很容易的事情。

  1. 適用場景

在以下情況下可以考慮使用迭代器模式:

(1) 訪問一個聚合物件的內容而無須暴露它的內部表示。將聚合物件的訪問與內部資料的儲存分離,使得訪問聚合物件時無須瞭解其內部實現細節。

(2) 需要為一個聚合物件提供多種遍歷方式。

(3) 為遍歷不同的聚合結構提供一個統一的介面,在該介面的實現類中為不同的聚合結構提供不同的遍歷方式,而客戶端可以一致性地操作該介面。

相關文章