我們都用過電視機遙控器,通過它我們可以進行開機、關機、換臺、改變音量等操作。我們可以將電視機看做一個儲存電視訊道的集合物件,通過遙控器可以對電視機中的頻道集合進行操作,例如返回上一個頻道、跳轉到下一個頻道或者跳轉到指定的頻道等。遙控器的出現,使得使用者不需要知道這些頻道到底如何儲存在電視機中。在軟體開發中也存在類似於電視機一樣的類,他們可以儲存了多個成員物件(元素),這些類通常稱為聚合類(Aggregate Class),對應的物件稱為聚合物件。為了更加方便地操作這些聚合物件,同時可以很靈活地為聚合物件增加不同的遍歷方法,也需要類似於電視機遙控器一樣的角色,可以訪問一個聚合物件中的元素擔憂部需要暴露它的內部結構,這就是我們需要學習的迭代器模式。
迭代器模式(Iterator) | 學習難度:★★★☆☆ | 使用頻率:★★★★★ |
一、銷售管理系統中資料的遍歷
Background : M公司為某商場開發了一套銷售管理系統,在對該系統進行分析和設計時,M公司開發人員發現經常需要對系統中的商品資料、客戶資料等進行遍歷,為了複用這些遍歷程式碼,M公司開發人員設計了一個抽象的資料聚合類AbstractObjectList,而將儲存商品和客戶登記的類作為其子類。AbstractObjectList類結構如下圖所示。
在上圖中,IList型別的物件objects用於儲存資料,AbstractObjectList類的方法說明如下表所示:
AbstractObjectList類的子類ProductList和CustomerList分別用於儲存商品資料和客戶資料。
M公司開發人員通過對AbstractObjectList類結構進行分析,發現該設計方案存在以下問題:
(1)在該類中,AddObject()與RemoveObject()等方法用於管理資料,而GetNextItem()、GetPreviousItem()、IsFirst()等方法又用於遍歷資料,導致了聚合類的職責過重,違反了單一職責原則。
(2)如果將抽象聚合類宣告為一個介面,則在這個介面中充斥著大量方法,不利於子類實現,違反了介面隔離原則。
(3)如果將所有的遍歷操作都交給子類來實現,將導致子類程式碼過於龐大,而且必須暴露AbstractObjectList類的內部儲存細節,向子類公開自己的私有屬性,否則子類無法實施對資料的遍歷,將破壞AbstractObjectList類的封裝性。
如何解決該問題?解決方案之一就是將聚合類中負責遍歷資料的方法提取出來,封裝到專門的類中,實現資料儲存和資料遍歷的分離,無須暴露聚合類的內部屬性即可對其進行操作,這正是迭代器模式的意圖所在。
二、迭代器模式概述
2.1 迭代器模式簡介
在軟體開發中,經常需要使用聚合物件來儲存一系列資料。聚合物件擁有兩個職責:一是儲存資料,二是遍歷資料。從依賴性來看,前者是聚合物件的基本職責,而後者既是可變化的又是可分離的。因此,可以將遍歷資料的行為從聚合物件中分離出來,封裝在一個被稱為“迭代器”的物件中,由迭代器來提供遍歷聚合物件內部資料的行為,這將簡化聚合物件的設計,更加符合單一職責原則。
迭代器(Iterator)模式:提供一種方法來訪問聚合物件,而不用暴露這個物件的內部表示,其別名為遊標(Cursor)。迭代器模式是一種物件行為型模式。
2.2 迭代器模式結構
(1)Iterator(抽象迭代器):定義了訪問和遍歷元素的介面,宣告瞭用於遍歷資料元素的方法。
(2)ConcreteIterator(具體迭代器):它實現了抽象迭代器介面,完成對聚合物件的遍歷。
(3)Aggregate(抽象聚合類):用於儲存和管理元素物件,宣告一個CreateIterator()方法用於建立一個迭代器物件,充當抽象迭代器工廠角色。
(4)ConcreteAggregate(具體聚合類):實現了在抽象聚合類中宣告的CreateIterator()方法,返回一個對應的具體迭代器ConcreteIterator例項。
三、銷售管理系統中資料的遍歷實現
3.1 重構後的設計結構
其中,AbstractObjectList充當抽象聚合類,ProductList充當具體聚合類,AbstractIterator充當抽象迭代器,ProductIterator充當具體迭代器。
3.2 重構後的程式碼實現
(1)抽象聚合類:AbstractObjectList
/// <summary> /// 抽象聚合類:AbstractObjectList /// </summary> public abstract class AbstractObjectList { protected IList<object> objectList = new List<object>(); public AbstractObjectList (IList<object> objectList) { this.objectList = objectList; } public void AddObject(object obj) { this.objectList.Add(obj); } public void RemoveObject(object obj) { this.objectList.Remove(obj); } public IList<Object> GetObjectList() { return this.objectList; } // 宣告建立迭代器物件的抽象工廠方法 public abstract AbstractIterator CreateIterator(); }
(2)具體聚合類 - ProductList 與 具體迭代器 - ProductIterator => 這裡採用了內部類的方式
/// <summary> /// 具體聚合類:ProductList /// </summary> public class ProductList : AbstractObjectList { public ProductList(IList<object> objectList) : base(objectList) { } public override AbstractIterator CreateIterator() { return new ProductIterator(this); } /// <summary> /// 內部類=>具體迭代器:ProductIterator /// </summary> private class ProductIterator : AbstractIterator { private ProductList productList; private IList<object> products; private int cursor1; // 定義一個遊標,用於記錄正向遍歷的位置 private int cursor2; // 定義一個遊標,用於記錄逆向遍歷的位置 public ProductIterator(ProductList productList) { this.productList = productList; this.products = productList.GetObjectList(); // 獲取集合物件 this.cursor1 = 0; // 設定正向遍歷遊標的初始值 this.cursor2 = this.products.Count - 1; // 設定逆向遍歷遊標的初始值 } public object GetNextItem() { return products[cursor1]; } public object GetPreviousItem() { return products[cursor2]; } public bool IsFirst() { return cursor2 == -1; } public bool IsLast() { return cursor1 == products.Count; } public void Next() { if (cursor1 < products.Count) { cursor1++; } } public void Previous() { if (cursor2 > -1) { cursor2--; } } } }
(3)抽象迭代器:AbstractIterator
/// <summary> /// 抽象迭代器:AbstractIterator /// </summary> public interface AbstractIterator { void Next(); // 移動至下一個元素 bool IsLast(); // 判斷是否為最後一個元素 void Previous(); // 移動至上一個元素 bool IsFirst(); // 判斷是否為第一個元素 object GetNextItem(); // 獲取下一個元素 object GetPreviousItem(); // 獲取上一個元素 }
(4)客戶端測試
public class Program { public static void Main(string[] args) { IList<object> products = new List<object>(); products.Add("倚天劍"); products.Add("屠龍刀"); products.Add("斷腸草"); products.Add("葵花寶典"); products.Add("四十二章經"); AbstractObjectList objectList = new ProductList(products); // 建立聚合物件 AbstractIterator iterator = objectList.CreateIterator(); // 建立迭代器物件 Console.WriteLine("正向遍歷"); while (!iterator.IsLast()) { Console.Write(iterator.GetNextItem() + ","); iterator.Next(); } Console.WriteLine(); Console.WriteLine("-------------------------------------------------------"); Console.WriteLine("逆向遍歷"); while (!iterator.IsFirst()) { Console.Write(iterator.GetPreviousItem() + ","); iterator.Previous(); } Console.ReadKey(); } }
F5編譯執行後的結果如下圖所示:
四、迭代器模式小結
4.1 主要優點
(1)支援以不同方式遍歷一個聚合物件,在同一個聚合物件上可以定義多種便利方式。
(2)增加新的聚合類和迭代器類都很方便 => 無須修改原有程式碼,符合開閉原則。
4.2 主要缺點
增加新的聚合類需要對應增加新的迭代器類 => 類的個數會成對增加!
4.3 應用場景
(1)訪問一個聚合物件的內容而無須暴露它的內部表示。
(2)需要為一個聚合物件提供多種遍歷方式。
(3)重點 => 該模式在.Net中,可以通過實現IEnumberable介面即可,不再需要單獨實現! (在.NET下,迭代器模式中的聚集介面和迭代器介面都已經存在了,其中IEnumerator介面扮演的就是迭代器角色,IEnumberable介面則扮演的就是抽象聚集的角色,其中定義了GetEnumerator()方法。)
參考資料
(1)劉偉,《設計模式的藝術—軟體開發人員內功修煉之道》
(2)聖傑,《C#設計模式之迭代器模式》