思考并回答以下问题:
本章导学
迭代器模式是一种使用频率非常高的设计模式,迭代器用于对一个聚合对象进行遍历。通过引入迭代器可以将数据的遍历功能从聚合对象中分离出来,聚合对象只负责存储数据,而遍历数据由迭代器来完成,简化了聚合对象的设计,更符合单一职责原则的要求。
本章将学习迭代器模式的定义与结构,结合实例学习迭代器模式的实现和应用,并学习如何使用.NET Framework内置的迭代器。
本章知识点
- 迭代器模式的定义
- 迭代器模式的结构
- 迭代器模式的实现
- 迭代器模式的应用
- 迭代器模式的优缺点
- 迭代器模式的适用环境
- 使用内部类实现迭代器
- .NET内置迭代器
迭代器模式概述
在现实生活中,人们有两种方式来操作一台电视机实现开机、关机、换台、改变音量等功能,一种方式是直接通过电视机控制面板上的按键来实现,另一种方式是通过电视机遥控器来间接实现。遥控器为操作电视机带来很大的方便,用户并不需要知道电视频道到底怎样存储在电视机中。在此,可以将电视机看成一个存储电视频道的集合对象,通过遥控器可以对电视机中的电视频道集合进行操作,例如返回上一个频道、跳转到下一个频道或者跳转到指定的频道。电视机遥控器和电视机示意图如图1所示。
图1 电视机遥控器与电视机示意图
在软件开发中,也存在着大量类似电视机一样的类,它们可以存储多个成员对象(元素),这些类通常称为聚合类(Aggregate Classes),对应的对象称为聚合对象。为了更加方便地操作这些聚合对象,同时可以很灵活地为聚合对象增加不同的遍历方法,也需要类似电视机遥控器一样的角色,可以访问一个聚合对象中的元素但又不需要暴露它的内部结构。本章所要学习的迭代器模式将为聚合对象提供一个“遥控器”,通过引入迭代器,客户端无须了解聚合对象的内部结构即可实现对聚合对象中成员的遍历,还可以根据需要很方便地增加新的遍历方式。
在软件系统中,聚合对象拥有两个职责:一是存储数据;二是遍历数据。从依赖性来看,前者是聚合对象的基本职责;后者既是可变化的,又是可分离的。因此,可以将遍历数据的行为从聚合对象中分离出来,封装在迭代器对象中,由迭代器来提供遍历聚合对象内部数据的行为,这将简化聚合对象的设计,更符合单一职责原则的要求。
迭代器模式的定义如下:1
提供一种方法顺序访问一个聚合对象中的各个元素,且不用暴露该对象的内部表示。
迭代器模式又称为游标(Cursor)模式,它是一种对象行为型模式。
迭代器模式的结构与实现
迭代器模式的结构
在迭代器模式结构中包含聚合和迭代器两个层次结构,考虑到系统的灵活性和可扩展性,在迭代器模式中应用了工厂方法模式,其模式结构如图2所示。
图2 迭代器模式结构图
由图2可知,迭代器模式包含以下4个角色。
(1)Iterator(抽象迭代器):它定义了访问和遍历元素的接口,声明了用于遍历数据元素的方法。例如,用于获取第一个元素的First()方法,用于访问下一个元素的Next()方法,用于判断是否还有下一个元素的HasNext()方法,用于获取当前元素的CurrentItem()方法等,在具体迭代器中将实现这些方法。
(2)ConcreteIterator(具体迭代器):它实现了抽象迭代器接口,完成对聚合对象的遍历,同时在具体迭代器中通过游标来记录在聚合对象中所处的当前位置,在具体实现时,游标通常是一个表示位置的非负整数。
(3)Aggregate(抽象聚合类):它用于存储和管理元素对象,声明一个CreateIterator()方法用于创建一个迭代器对象,充当抽象迭代器工厂角色。
(4)ConcreteAggregate(具体聚合类):它是抽象聚合类的子类,实现了在抽象聚合类中声明的CreateIterator()方法,该方法返回一个与该具体聚合类对应的具体迭代器Concretelterator实例。
迭代器模式的实现
在迭代器模式中,提供了一个外部的迭代器对聚合对象进行访问和遍历,迭代器定义了一个访问该聚合元素的接口,并且可以跟踪当前遍历的元素,了解哪些元素已经遍历过而哪些没有。迭代器的引入,将使对一个复杂聚合对象的操作变得简单。
下面结合代码对迭代器模式的实现作进一步分析。在迭代器模式中应用了工厂方法模式,抽象迭代器对应于抽象产品角色,具体迭代器对应于具体产品角色,抽象聚合类对应于抽象工厂角色,具体聚合类对应于具体工厂角色。
在抽象迭代器中声明了用于遍历聚合对象中所存储元素的方法,其典型代码如下:1
2
3
4
5
6
7interface Iterator
{
void First(); // 将游标指向第一个元素
void Next(); // 将游标指向下一个元素
bool HasNext(); // 判断是否存在下一个元素
object CurrentItem(); // 获取游标指向的当前元素
}
在具体迭代器中将实现抽象迭代器声明的遍历数据的方法,其典型代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32class ConcreteIterator : Iterator
{
private ConcreteAggregate objects; // 维持一个对具体聚合对象的引用,以便于访问存储在聚合对象中的数据
private int cursor; // 定义一个游标,用于记录当前访问位置
public ConcreteIterator(ConcreteAggregate objects)
{
this.objects = objects;
}
public void First()
{
// 实现代码
}
public void Next()
{
// 实现代码
}
public bool HasNext()
{
// 实现代码
return false;
}
public object CurrentItem()
{
// 实现代码
return null;
}
}
需要注意的是,抽象迭代器接口的设计非常重要,一方面需要充分满足各种遍历操作的要求,尽量为各种遍历方法提供声明;另一方面又不能包含太多方法,如果接口中的方法太多将给子类的实现带来麻烦。因此,可以考虑使用抽象类来设计抽象迭代器,在抽象类中为每一个方法提供一个空的默认实现。如果需要在具体迭代器中为聚合对象增加全新的遍历操作,则必须修改抽象迭代器和具体迭代器的源代码,这将违反开闭原则,因此在设计时要考虑全面,避免之后修改接口。
聚合类用于存储数据并负责创建迭代器对象,最简单的抽象聚合类代码如下:1
2
3
4interface Aggregate
{
Iterator CreateIterator();
}
具体聚合类作为抽象聚合类的子类,一方面负责存储数据,另一方面实现了在抽象聚合类中声明的工厂方法CreateIterator(),用于返回一个与该具体聚合类对应的具体迭代器对象。其典型代码如下:1
2
3
4
5
6
7
8
9class ConcreteAggregate : Aggregate
{
//......
public Iterator CreateIterator()
{
return new ConcreteIterator(this);
}
//......
}
迭代器模式的应用实例
下面通过一个应用实例来进一步学习和理解迭代器模式。
1.实例说明
某软件公司为某商场开发了一套销售管理系统,在对该系统进行分析和设计时,开发人员发现经常需要对系统中的商品数据、客户数据等进行遍历,为了复用这些遍历代码,开发人员设计了一个抽象的数据集合类AbstractObjectList,将存储商品和客户等数据的类作为其子类,AbstractObjectList类结构如图3所示。
图3 AbstractObjectList类结构图
在图3中,List类型的对象objects用于存储数据,其方法与说明如表1所示。
表1 AbstractObjectList类的方法与说明
AbstractObjectList() | 构造方法,用于给objects对象赋值 |
AddObject() | 增加元素 |
RemoveObject() | 删除元素 |
GetObjects() | 获取所有元素 |
Next() | 移至下一个元素 |
IsLast() | 判断当前元素是否是最后一个元素 |
Previous() | 移至上一个元素 |
IsFirst() | 判断当前元素是否是第一个元素 |
GetNextItem() | 获取下一个元素 |
GetPreviousItem() | 获取上一个元素 |
AbstractObjectList类的子类ProductList和CustomerList分别用于存储商品数据和客户数据。
通过分析,发现AbstractObjectList类的职责非常重,它既负责存储和管理数据,又负责遍历数据,违背了单一职责原则,实现代码将非常复杂。因此,开发人员决定使用迭代器模式对AbstractObjectList类进行重构,将负责遍历数据的方法提取出来,封装到专门的类中,实现数据存储和数据遍历分离,还可以给不同的具体数据集合类提供不同的遍历方式。
现给出使用迭代器模式重构后的解决方案。
2.实例类图
通过分析,本实例的结构如图4所示。
图4 销售管理系统数据遍历结构图
为了简化类图和代码,图4只提供了一个具体聚合类和一个具体迭代器类,AbstractObjectList充当抽象聚合类,ProductList充当具体聚合类,Abstractlterator充当抽象迭代器,Productlterator充当具体迭代器。
3.实例代码
(1)AbstractObjectList:抽象聚合类。
1 | using System.Collections.Generic; |
(2)ProductList:商品数据类,充当具体聚合类。
1 | using System.Collections.Generic; |
(3)AbstractIterator:抽象迭代器。
1 | namespace IteratorSample |
(4)ProductIterator:商品迭代器,充当具体迭代器。
1 | using System.Collections.Generic; |
(5)Program:客户端测试类。
1 | using System; |
4.结果及分析
编译并运行程序,输出结果如下:1
2
3
4
5正向遍历:
倚天剑,屠龙刀,断肠草,葵花宝典,四十二章经,
-----------------------------
逆向遍历:
四十二章经,葵花宝典,断肠草,屠龙刀,倚天剑,
如果需要增加一个新的具体聚合类,例如客户数据集合类,并且需要为客户数据集合类提供不同于商品数据集合类的正向遍历和逆向遍历操作,只需增加一个新的聚合子类和一个新的具体迭代器类即可,原有类库代码无须修改,符合开闭原则;如果需要为ProductList类更换一个迭代器,只需增加一个新的具体迭代器类作为抽象迭代器类的子类,重新实现遍历方法即可,原有迭代器代码无须修改,也符合开闭原则;如果要在迭代器中增加新的方法,则需要修改抽象迭代器的源代码,这将违背开闭原则。
使用内部类实现迭代器
在图2所示的迭代器模式结构图中,可以看到具体迭代器类和具体聚合类之间存在着双重关系,其中一个关系为关联关系,在具体迭代器中需要维持一个对具体聚合对象的引用,该关联关系的目的是访问存储在聚合对象中的数据,以便迭代器能够对这些数据进行遍历操作。
除了使用关联关系外,为了能够让迭代器可以访问到聚合对象中的数据,还可以将迭代器类设计为聚合类的内部类。例如可以对19.3节中的ProductList类进行修改,将Productlterator类作为ProductList类的内部类,代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65using System.Collections.Generic;
namespace IteratorSample
{
class ProductListNew : AbstractObjectList
{
public ProductListNew(List<object> products) : base(products)
{
}
public override AbstractIterator CreateIterator()
{
return new ProductIterator();
}
// 商品迭代器:具体迭代器,内部类实现
private class ProductIterator : AbstractIterator
{
private int cursor1;
private int cursor2;
public ProductIterator()
{
cursor1 = 0;
cursor2 = objects.Count - 1;
}
public void Next()
{
if(cursor1 < objects.Count)
{
cursor1++;
}
}
public bool IsLast()
{
return (cursor1 == objects.Count);
}
public void Previous()
{
if(cursor2 > -1)
{
cursor2--;
}
}
public bool IsFirst()
{
return (cursor2 == -1);
}
public object GetNextItem()
{
return objects[cursor1];
}
public object GetPreviousItem()
{
return objects[cursor2];
}
}
}
}
需要注意的是,根据C#语言的语法规定,内部类只能访问到外部类中定义的静态(static)成员变量,因此需要将AbstractObjectList中的objects声明为静态变量,代码如下:1
2
3...
protected static List<object> objects = new List<object>();
...
同时还需要将引用该变量的方法中的this关键字去掉。
无论使用哪种实现机制,客户端代码都是一样的,也就是说客户端无须关心具体迭代器对象的创建细节,只需通过调用工厂方法Createlterator()即可得到一个可用的迭代器对象,这也是使用工厂方法模式的好处,通过工厂来封装对象的创建过程,简化了客户端的调用。
.NET内置迭代器
在.NET Framework中内置了对迭代器模式的支持,提供了抽象的迭代器接口System.Collections.IEnumerator和枚举接口System.Collections.IEnumerable,C#语言中的foreach循环就是利用IEnumerable提供的IEnumerator来实现的。
在.Net中,IEnumerable接口的定义如下:1
2
3
4public interface IEnumerable
{
IEnumerator GetEnumerator(); // 工厂方法,获取迭代器对象
}
IEnumerator接口的定义如下:1
2
3
4
5
6
7
8
9public interface IEnumerator
{
object Current{ // 返回当前集合中的元素
get;
}
bool MoveNext(); // 遍历集合,移至下一个元素
void Reset(); // 恢复初始位置
}
通过扩展IEnumerable接口和IEnumerator接口可以定义自己的聚合类和迭代器类,实现了IEnumerable的子类都可以对应定义一个迭代器,用于对其中的元素进行遍历。在NET中,ICollection继承自IEnumerable,IList继承自ICollection,而ArrayList是IList的子类,实现了GetEnumerator()方法,并返回一个ArrayListEnumeratorSimple类型的对象。
ArrayList类的代码片段如下:1
2
3
4
5
6
7
8
9public class ArrayList : IList, ICloneable
{
...
public virtual IEnumerator GetEnumerator()
{
return new ArrayListEnumeratorSimple(this);
}
...
}
ArrayList中,GetEnumerator()方法是一个虚方法,表示可以自定义一个集合类型继承ArrayList,然后重写这个方法,创建不同的IEnumerator对象,从而实现不同的遍历方式。
在.NET中,ArrayListEnumeratorSimple类的代码片段如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23[ ]
private class ArrayListEnumeratorSimple : IEnumerator, ICloneable
{
// Methods
internal ArrayListEnumeratorSimple(ArrayList list)
{
this.list = list;
this.index = -1;
this.version = list._version;
this.currentElement = list;
}
public object Clone(){};
public virtual bool MoveNext(){};
public virtual void Reset(){};
public virtual bool Current(){};
// Fields
private object currentElement;
private int index;
private ArrayList list;
private int version;
}
ArrayListEnumeratorSimple实现了IEnumerator接口,且实现了MoveNext()、Reset()、Current等方法和属性,该类是一个私有类,其构造函数被internal修饰符限制,在构造函数中,传入的参数类型是ArrayList,MoveNext()、Current、Reset()等操作通过构造函数所传递的ArrayList对象实现。
在此,IEnumerable是抽象聚合类,IEnumerator是抽象迭代器,ArrayList是具体聚合类,ArrayListEnumeratorSimple是具体迭代器。
在实际的C#应用开发中通常很少自定义迭代器,一般使用.NET Framework内置的迭代器即可,下面的代码演示了如何使用.NET Framework内置迭代器。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34using System;
using System.Collections;
using System.Collections.Generic;
namespace IteratorSample
{
class Program
{
static void Process(IEnumerable e)
{
IEnumerator i = e.GetEnumerator(); // 创建迭代器对象
while (i.MoveNext())
{
Console.WriteLine(i.Current.ToString());
}
}
static void Main(string[] args)
{
IList persons;
persons = new ArrayList(); // 创建一个ArrayList类型的聚合对象
persons.Add("张无忌");
persons.Add("小龙女");
persons.Add("令狐冲");
persons.Add("韦小宝");
persons.Add("袁紫衣");
persons.Add("小龙女");
Process(persons);
Console.Read();
}
}
}
如上加粗代码所示,在静态方法Process()中使用迭代器1Enumerator对IEnumerable对象进行处理。该代码的运行结果如下:1
2
3
4
5
6张无忌
小龙女
令狐冲
韦小宝
袁紫衣
小龙女
迭代器模式的优缺点与适用环境
迭代器模式是一种使用频率非常高的设计模式,通过引入迭代器可以将数据的遍历功能从聚合对象中分离出来,聚合对象只负责存储数据,而遍历数据由迭代器来完成。由于很多编程语言的类库都已经实现了迭代器模式,因此在实际开发中,直接使用C#、Java等语言已经定义好的迭代器即可,迭代器已经成为操作聚合对象的基本工具之一。
迭代器模式的优点
迭代器模式是一种使用频率非常高的设计模式,通过引入迭代器可以将数据的遍历功能从聚合对象中分离出来,聚合对象只负责存储数据,而遍历数据由迭代器来完成。由于很多编程语言的类库都已经实现了迭代器模式,因此在实际开发中,直接使用C#,Java等语言已经定义好的迭代器即可,迭代器已经成为操作聚合对象的基本工具之一。
迭代器模式的优点
迭代器模式的主要优点如下:
- (1)迭代器模式支持以不同的方式遍历一个聚合对象,在同一个聚合对象上可以定义多种遍历方式。在迭代器模式中,只需用一个不同的迭代器来替换原有迭代器即可改变遍历算法,也可以自己定义迭代器的子类以支持新的遍历方式。
- (2)迭代器模式简化了聚合类。由于引入了迭代器,在原有的聚合对象中不需要再自行提供数据遍历等方法,这样可以简化聚合类的设计。
- (3)在迭代器模式中,由于引入了抽象层,增加新的聚合类和迭代器类都很方便,无须修改原有代码,符合开闭原则。
迭代器模式的缺点
迭代器模式的主要缺点如下:
- (1)由于迭代器模式将存储数据和遍历数据的职责分离,在增加新的聚合类时需要对应地增加新的迭代器类,类的个数成对增加,这在一定程度上增加了系统的复杂性。
- (2)抽象迭代器的设计难度较大,需要充分考虑系统将来的扩展。在自定义迭代器时,创建一个考虑全面的抽象迭代器并不是一件很容易的事情。
迭代器模式的适用环境
在以下情况下可以考虑使用迭代器模式:
- (1)访问一个聚合对象的内容而无须暴露它的内部表示。将聚合对象的访问与内部数据的存储分离,使得访问聚合对象时无须了解其内部实现细节。
- (2)需要为一个聚合对象提供多种遍历方式。
- (3)为遍历不同的聚合结构提供一个统一的接口,在该接口的实现类中为不同的聚合结构提供不同的遍历方式,而客户端可以一致性地操作该接口。
本章小结
(1)迭代器模式提供了一种方法顺序访问一个聚合对象中的各个元素,且不用暴露该对象的内部表示。迭代器模式是一种对象行为型模式。
(2)迭代器模式包含抽象迭代器、具体迭代器、抽象聚合类和具体聚合类4个角色。其中,抽象迭代器定义了访问和遍历元素的接口,声明了用于遍历数据元素的方法;具体迭代器实现了抽象迭代器接口,完成对聚合对象的遍历;抽象聚合类用于存储和管理元素对象;具体聚合类是抽象聚合类的子类,实现了在抽象聚合类中声明的方法。
(3)迭代器模式的主要优点包括支持以不同的方式遍历一个聚合对象,在同一个聚合对象上可以定义多种遍历方式;简化了聚合类;增加新的聚合类和迭代器类都很方便,无须修改原有代码,符合开闭原则。其主要缺点是在增加新的聚合类时需要对应增加新的迭代器类,类的个数成对增加,这在一定程度上增加了系统的复杂性;抽象迭代器的设计难度较大,需要充分考虑系统将来的扩展。
(4)迭代器模式适用的环境:访问一个聚合对象的内容而无须暴露它的内部表示;需要为一个聚合对象提供多种遍历方式;为遍历不同的聚合结构提供一个统一的接口,在该接口的实现类中为不同的聚合结构提供不同的遍历方式,而客户端可以一致性地操作该接口。
(5)除了使用关联关系外,为了能够让迭代器可以访问到聚合对象中的数据,还可以将迭代器类设计为聚合类的内部类。
(6)在.NET Framework中内置了对迭代器模式的支持,提供了抽象的迭代器接口IEnumerator和枚举接口IEnumerable,通过使用.NET Framework内置的迭代器可以很方便地实现对常用集合对象的遍历操作。