1. 程式人生 > >迭代器模式 Iterator 行為型 設計模式(二十)

迭代器模式 Iterator 行為型 設計模式(二十)

  在計算機中,Iterator意為迭代器,迭代有重複的含義,在程式中,更有“遍歷”的含義
  
  如果給定一個數組,我們可以通過for迴圈來遍歷這個陣列,這種遍歷就叫做迭代
  
  對於陣列這種資料結構,我們稱為是可迭代的
  
  所以
  
  迭代器就是可以用來對於一個數據集合進行遍歷的物件
  
  原文地址:迭代器模式 Iterator 行為型 設計模式(二十)
  
  意圖
  
  提供一種方法,順序訪問一個聚合物件中的各個元素,而又不需要暴露該物件的內部表示。
  
  別名:遊標 Cursor
  
  集合與遍歷
  
  由一個或多個確定的元素所構成的整體叫做集合。
  
  多個物件聚集在一起形成的總體稱之為聚集aggregate。
  
  集合和聚集有相似的含義。
  
  容器是指盛物品的器具,Java的Collection框架,就是設計用來儲存物件的容器
  
  容器可以認為是集合、聚集的具體體現形式,三個元素在一起叫做集合(聚集),怎麼在一起?陣列,列表?這具體的體現形式就是容器
  
  容器必須提供內部物件的訪問方式,如果不能獲取物件,容器也失去了存在的意義,一個只能進不能出的儲蓄罐你要它何用?
  
  因為容器的存在就是為了更方便的使用、管理物件。
  
  而且通常需要容器提供對於內部所有元素的遍歷方法。
  
  然而容器其內部有不同的擺放形式,可順序,可無序堆集
  
  簡言之,就是不同型別的容器必然有不同的內部資料結構
  
  那麼,一種解決辦法就是不同的容器各自提供自己的遍歷方法。
  
  這樣的話,對於使用容器管理物件的客戶端程式來說:
  
  如果迭代的邏輯,也就是業務邏輯沒有變化
  
  當需要更換為另外的集合時,就需要同時更換這個迭代方法
  
  考慮這樣一個場景
  
  有一個方法,方法的引數型別為 Collection
  
  他的迭代邏輯,也就是業務邏輯為遍歷所有元素,讀取每個元素的資訊,並且進行列印...
  
  如果不同的容器有不同的遍歷方法,也就是一種實現類有一種不同的遍歷方法
  
  一旦更換實現類,那麼就需要同步更換掉這個迭代方法,否則方法將無法通過編譯
  
  如果集合的實現不變,需要改變業務邏輯,也就是迭代的邏輯
  
  那麼就需要修改容器類的迭代方法,也就是修改原來的遍歷方法
  
  還是上面的場景
  
  有一個方法,方法的引數型別為 Collection
  
  他的迭代邏輯,也就是業務邏輯為遍歷所有元素,讀取每個元素的資訊,並且進行列印...
  
  現在他的實現類無需變化
  
  但是業務邏輯需要變動,比如希望從後往前的方式進行遍歷,而不再是從前往後
  
  就需要修改原來的方法或者重新寫一個方法
  
  出現上述問題的根本原因就在於元素的迭代邏輯與容器本身耦合在一起
  
  當迭代邏輯或者集合實現發生變更時,需要進行修改,不符合開閉原則
  
  容器自身不僅僅需要儲存管理物件,還要負責物件的遍歷訪問,不符合單一職責原則
  
  儲存管理物件是容器的核心職責,雖然經常需要提供遍歷方法,但是他並不是核心職責
  
  但是為了提供遍歷元素的方法,可能不得不在容器類內提供各種全域性變數,比如儲存當前的位置等,這無疑也會導致容器聚集類設計的複雜度
  
  結構
  
  image_5c11cddc_779f
  
  抽象迭代器角色Iterator
  
  定義遍歷元素所需要的介面
  
  具體的迭代器ConcreteIterator
  
  實現了Iterator介面,並且跟蹤當前位置
  
  抽象集合容器角色Aggregate
  
  定義建立相應迭代器的介面(方法)
  
  就是一個容器類,並且定義了一個返回迭代器的方法
  
  具體的容器角色ConcreteAggregate
  
  Aggregate的子類,並且實現了建立Iterator物件的介面,也就是返回一個ConcreteIterator例項
  
  客戶端角色Client
  
  持有容器物件以及迭代器物件的引用,呼叫迭代物件的迭代方法遍歷元素
  
  迭代器模式中,通過一個外部的迭代器來對容器集合物件進行遍歷。
  
  迭代器定義了遍歷訪問元素的協議方式。
  
  容器集合物件提供建立迭代器的方法。
  
  示例程式碼
  
  Aggregate角色
  
  提供了iterator()獲取Iterator
  
  複製程式碼
  
  package iterator;
  
  public abstract class Aggregate {
  
  abstract Iterator iterator();
  
  abstract Object get(int index);
  
  abstract int size();
  
  }
  
  複製程式碼
  
  ConcreateAggregate角色
  
  內部使用一個Object陣列,陣列直接通過構造方法傳遞進去(只是為了演示學習模式,不要糾結這算不上一個容器)
  
  提供了大小的獲取方法以及獲取指定下標元素的方法
  
  尤其是實現了iterator(www.michenggw.com/)方法,建立一個ConcreteIterator例項,將當前Concrete www.mcyllpt.com/  Aggregate作為引數傳遞給他的構造方法
  
  複製程式碼
  
  package iterator;
  
  public class ConcreateAggregate extends Aggregate {
  
  private Object[] objects;
  
  ConcreateAggregate(Object[] objects) {
  
  this.objects = objects;
  
  }
  
  

@Override
  
  Iterator iterator() {
  
  return new ConcreateIterator(this);
  
  }
  
  @Override
  
  Object get(int index) {
  
  return objects[index];
  
  }
  
  @Override
  
  int size() {
  
  return objects.length;
  
  }
  
  }
  
  複製程式碼
  
  迭代器介面
  
  一個是否還有元素的方法,一個獲取下一個元素的方法
  
  複製程式碼
  
  package iterator;
  
  public interface Iterator {
  
  boolean hasNext();
  
  Object next();
  
  }
  
  複製程式碼
  
  具體的迭代器
  
  內部維護了資料的大小和當前位置
  
  如果下標未到最後,那麼就是還有元素
  
  next()方法用於獲取當前元素,獲取後當前位置往後移動一下
  
  複製程式碼
  
  package iterator;
  
  public class ConcreateIterator implements Iterator {
  
  private Aggregate aggregate;
  
  private int index = 0;
  
  private int size = 0;
  
  ConcreateIterator(Aggregate aggregate) {
  
  this.aggregate = aggregate;
  
  size = aggregate.size();
  
  }
  
  
@Override

  
  public boolean hasNext() {
  
  return index < size ? true : false;
  
  }
  
  @Override
  
  public Object next() {
  
  Object value = aggregate.get(index);
  
  index++;
  
  return value;
  
  }
  
  }
  
  複製程式碼
  
  測試類
  
  複製程式碼
  
  package iterator;
  
  public class Client {
  
  public static void main(String[] args) {
  
  Object[] objects = {"1", 2, 3, 4, 5};
  
  Aggregate aggregate = new ConcreateAggregate(objects);
  
  Iterator iterator = aggregate.iterator();
  
  while (iterator.hasNext()) {
  
  System.out.println(iterator.next());
  
  }
  
  }
  
  }
  
  複製程式碼
  
  image_5c11cddc_5524
  
  示例程式碼中ConcreateAggregate本身提供了獲取指定下標元素的方法,可以直接呼叫獲取元素
  
  藉助於Iterator,將迭代邏輯從Aggregate中剝離出來,獨立封裝實現
  
  在客戶端與容器之間,增加了一層Iterator,實現了客戶端程式與容器的解耦
  
  image_5c11cddc_440b
  
  說白了,增加了Iterator,相當於通過Iterator封裝了真實容器物件的獲取元素的方法
  
  不直接呼叫方法,經過Iterator轉換一層
  
  而且仔細品味下,這有點“適配”的韻味,適配的目標就是統一的元素訪問協議,通過Iterator約定
  
  而被適配的角色,則是真實容器物件元素的操作方法
  
  總之“間接”“委託”“代理”的感覺,對吧,好處自己品味
  
  外部迭代與內部迭代
  
  在上面的示例程式中,通過引入Iterator,實現了迭代邏輯的封裝抽象
  
  但是容器聚集物件本身有獲取元素的方法,所以客戶端仍舊可以自行遍歷
  
  Iterator也只不過是容器聚集物件的一個客戶而已
  
  這種迭代器也叫做外部迭代器
  
  對於外部迭代器有一個問題,對於不同的ConcreteAggregate,可能都需要一個不同的ConcreteIterator
  
  也就是很可能會不得不建立了一個與Aggregate等級結構平行的Iterator結構,出現了很多的ConcreteIterator類
  
  這勢必會增加維護成本
  
  而且,雖然迭代器將客戶端的訪問與容器進行解耦,但是迭代器卻是必須依賴容器物件的
  
  也就是迭代器類ConcreteIterator與ConcreteAggregate必須進行通訊,會增加設計的複雜度,而且這也會增加類之間的耦合性
  
  另外的一種方法是使用內部類的形式,也就是將ConcreteIterator的實現,移入到ConcreteAggregate的內部
  
  藉助於內部類的優勢:對外部類有充足的訪問許可權,也就是無需擔心為了通訊要增加複雜度的問題
  
  準確的說,你沒有任何的通訊成本,內部類可以直接讀取外部類的屬性資料資訊
  
  而且,使用內部類的方式不會導致類的爆炸(儘管仍舊是會有另一個class檔案,但是從程式碼維護的角度看算是一個類)
  
  這種形式可以叫做內部迭代器
  
  不過無論哪種方式,你可以看得出來,使用迭代器的客戶端程式碼,都是一樣的
  
  藉助於工廠方法iterator()獲得一個迭代器例項(簡單工廠模式)
  
  然後藉助於迭代器進行元素遍歷
  
  JDK中的迭代
  
  我們看下JDK中的Collection提供給我們的迭代方式
  
  Collection是所有集合的父類,Collection實現了Iterable介面
  
  Iterable介面提供了iterator()方法用於返回一個Iterator類的一個例項物件
  
  Iterator類提供了對元素的遍歷方法
  
  image_5c11cddc_295f
  
  接下來看下ArrayList的實現
  
  ArrayList中iterator()返回了一個Itr物件,而這個物件是ArrayList的內部類,實現了Iterator介面
  
  image_5c11cddc_26d
  
  看得出來,java給集合框架內建了迭代器模式
  
  在ArrayList中使用就是內部類的形式,也就是內部迭代器
  
  boolean hasNext()
  
  是否擁有更多元素,換句話說,如果next()方法不會丟擲異常,就會返回true
  
  next();
  
  返回下一個元素
  
  remove()
  
  刪除元素
  
  有幾點需要注意
  
  1.)初始時,可以認為“當前位置”為第一個元素前面
  
  所以next()獲取第一個元素
  
  image_5c11cddc_3d19
  
  2.)根據第一點,初始的當前位置”為第一個元素前面,所以如果想要刪除第一個元素的話,必須先next,然後remove
  
  複製程式碼
  
  Iterator iterator = list.iterator();
  
  iterator.next();
  
  iterator.remove();
  
  複製程式碼
  
  否則,會丟擲異常
  
  image_5c11cddc_6e70
  
  3.)不僅僅是刪除第一個元素需要先next,然後才能remove,每一個remove,前面必須有一個next,成對出現
  
  所以remove是刪除當前元素
  
  如果下面這樣,會丟擲異常
  
  複製程式碼
  
  iterator.next();
  
  iterator.remove();
  
  iterator.remove();
  
  複製程式碼
  
  image_5c11cddc_424a
  
  4.)迭代器只能遍歷一次,如果需要重新遍歷,可以重新獲取迭代器物件
  
  如果已經遍歷到尾部之後仍舊繼續使用,將會丟擲異常
  
  複製程式碼
  
  Iterator iterator = list.iterator();
  
  while (iterator.hasNext()) {
  
  iterator.next();
  
  }
  
  iterator.next();
  
  複製程式碼
  
  image_5c11cddc_7a48
  
  總結
  
  在java中萬事萬物都是物件
  
  前面的命令模式將請求轉換為命令物件
  
  直譯器模式中,將語法規則轉換為終結符表示式和非終結符表示式
  
  在迭代器模式中,將“遍歷元素”轉換為物件
  
  通過迭代器模式引入迭代器,將遍歷邏輯功能從容器聚集物件中分離出來
  
  聚合物件本身只負責資料儲存,遍歷的職責交給了迭代器
  
  對於同一個容器物件,可以定義多種迭代器,也就是可以定義多種遍歷方式
  
  如果需要使用另外的迭代方式,僅僅需要更改迭代器物件即可
  
  這樣你甚至可以把ConcreteIterator使用配置檔案進行注入,靈活設定
  
  將迭代遍歷的邏輯從容器物件中分離,必然會減少容器類的複雜程度
  
  當增加新的容器類或者迭代器類時,不需要修改原有的程式碼,符合開閉原則
  
  如果你想要將容器聚集物件的遍歷邏輯從容器物件中的分離
  
  或者想要提供多種不同形式的遍歷方式時,或者你想為不同的容器物件提供一致性的遍歷介面邏輯
  
  你就應該考慮迭代器模式了
  
  迭代器模式的應用是如此廣泛,以至於java已經將他內建到集合框架中了
  
  所以對於我們自己來說,多數時候可以認為迭代器模式幾乎用不到了
  
  因為絕大多數時候,使用框架提供的應該就足夠了
  
  在java實現中,迭代器模式的比較好的做法就是Java集合框架使用的這種形式---內部類形式的內部迭代器,如果真的需要自己搞一個迭代器,建議仿照集合框架搞吧
  
  藉助於迭代器模式,如果迭代的邏輯不變,更換另外的集合實現,因為實現了共同的迭代器介面,所以不需要對迭代這塊,無需做任何變動
  
  如果需要改變迭代邏輯,必須增加新的迭代形式,只需要增加一個新的內部類實現迭代器介面即可,其他使用的地方只需要做很小的調整
  
  ArrayList中的ListIterator<E> listIterator() 方法就是如此
  
  有人覺得增加一個類和一個方法這不也是修改麼?個人認為:開閉原則儘管最高境界是完全的對擴充套件開放對修改關閉,但是也不能死摳字眼
  
  增加了一個新的獲取迭代物件的方法以及一個新的類,總比將原有的原始碼中新增新的方法那種修改要強得多,所有的遍歷邏輯都封裝在新的迭代器實現類中,某種程度上可以認為並沒有“修改原始碼”
  
  使用內部類的形式,有人覺得不還是在一個檔案中麼?但是內部類會有單獨的class檔案,而且,內部類就像一道牆,分割了內外,所有的邏輯被封裝在迭代器實現類中
  
  不需要影響容器自身的設計實現,所以也是符合單一職責原則的。