在程序设计中,经常要访问一个聚合对象中的各个元素,如链表、集合的遍历。通常的做法是将链表的创建和遍历都放在同一个类中。但这种方式不利于程序的扩展,如果要更换遍历方法就必须修改程序源代码,这违背了面向对象设计原则中的开闭原则。
将遍历方法封装在聚合类中是不可取的,但如果让用户自己实现遍历方法也不太好。首先这增加了用户的负担;其次这会暴露聚合类的内部表示,违背了面向对象编程的封装特性。
上面的两种方法都有不妥之处,为了解决这个问题,“迭代器模式”便应运而生了。
提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。
迭代器模式属于对象行为型模式。
对象行为型模式描述了一组对等的对象之间怎样相互协作共同完成其中任何一个对象都无法单独完成的任务,它涉及算法与对象间职责的分配。
迭代器模式是通过将聚合对象的遍历行为分离出来,抽象成迭代器类来实现的,这符合面向对象设计的单一职责原则。该设计模式的参与者分别是:
Iterator
(抽象迭代器)
定义访问和遍历元素的接口,通常包含 first()
、next()
、isDone()
等方法。
ConcreteIterator
(具体迭代器)
实现抽象迭代器接口中所定义的方法,完成对聚合对象的遍历,并记录遍历的当前位置。
Aggregate
(抽象聚合)
定义存储、添加、删除聚合对象以及创建相应迭代器对象的接口。
ConcreteAggregate
(具体聚合)
实现创建相应迭代器的接口,返回一个具体迭代器的实例。
本小节以外部迭代器为例,具体阐述迭代器模式的实现方式。
Iterator
类的实现如代码片段 1 所示。
【代码片段 1】抽象迭代器 Iterator 的实现
template<class Item>
class Iterator
{
public:
Iterator() {}
virtual ~Iterator() {}
virtual void first() = 0;
virtual void next() = 0;
virtual Item *currentItem() = 0;
virtual bool isDone() = 0;
};
ConcreteIterator
类的实现如代码片段 2 所示。
【代码片段 2】具体迭代器 ConcreteIterator 的实现
template<class Item>
class ConcreteIterator: public Iterator<Item>
{
private:
Aggregate<Item> *aggregate;
int currentIndex;
public:
ConcreteIterator(Aggregate<Item> *aggregate): aggregate(aggregate), currentIndex(0) {}
virtual ~ConcreteIterator() {}
virtual void first()
{
currentIndex = 0;
}
virtual void next()
{
if(currentIndex < aggregate->size())
{
++currentIndex;
}
}
virtual Item *currentItem()
{
if(!isDone())
{
return (aggregate->at(currentIndex));
}
else
{
throw "ConcreteIterator out of range!";
}
}
virtual bool isDone()
{
return currentIndex >= aggregate->size();
}
};
ConcreteIterator
实现了 Iterator
中定义的 4 个方法:
first()
将迭代器指向聚合中的第一个元素。next()
使迭代器向前推进一步。isDone()
检查指向当前元素的索引是否超出了范围。currentItem()
返回当前索引指向的元素。如果迭代已经终止,则抛出一个异常。Aggregate
类的实现如代码片段 3 所示。
【代码片段 3】抽象聚合 Aggregate 的实现
template<class Item>
class Aggregate
{
public:
Aggregate() {}
virtual ~Aggregate() {}
virtual size_t size() = 0;
virtual void append(Item item) = 0;
virtual Item *at(const int index) = 0;
virtual Iterator<Item> *createIterator() = 0;
};
ConcreteAggregate
类的实现如代码片段 4 所示。
【代码片段 4】具体聚合 ConcreteAggregate 的实现
template <class Item>
class ConcreteAggregate: public Aggregate<Item>
{
private:
std::vector<Item> data;
public:
ConcreteAggregate() {}
virtual ~ConcreteAggregate() {}
virtual size_t size()
{
return data.size();
}
virtual void append(Item item)
{
data.push_back(item);
}
virtual Item *at(const int index)
{
return &data[index];
}
virtual Iterator<Item> *createIterator()
{
return new ConcreteIterator<Item>(this);
}
};
ConcreteAggregate
实现了一个线性表,其底层基于 STL 中的 vector
,包含如下方法:
size()
返回线性表中对象的数目。append(item)
在线性表尾部添加元素 item
。at(index)
返回指定下标 index
处的对象,但并不检查指定的下标是否合法。createIterator()
返回可以遍历本线性表的迭代器。用户端的代码如代码片段 5 所示。
【代码片段 5】用户端的代码
int main(int argc, char *argv[])
{
Aggregate<int> *list = new ConcreteAggregate<int>;
for(int i = 1; i <= 10; i++)
{
list->append(i);
}
Iterator<int> *iter = list->createIterator();
for(iter->first(); !iter->isDone(); iter->next())
{
printf("%d ", *(iter->currentItem()));
}
printf("\n");
try
{
iter->next();
printf("%d", *(iter->currentItem()));
}
catch(const char *errorMessage)
{
puts(errorMessage);
}
return 0;
}
程序运行的结果如图 2 所示。在该程序中首先定义了一个线性表,并向其中添加数字 1 到 10。然后用 createIterator()
方法获取迭代器,从头迭代至尾并输出元素的值(见输出第 1 行)。
在迭代已经终止后,程序试图继续往下迭代,最终在 catch
语句块中捕获到了迭代器抛出的异常,并输出提示信息(见输出第 2 行)。
迭代器模式的主要优点如下:
由于迭代器模式将存储数据和遍历数据的职责分离,增加新的聚合类需要对应增加新的迭代器类,类的个数成对增加,这在一定程度上增加了系统的复杂性。
在遍历的过程中,更改迭代器所在的聚合结构可能导致出现异常。所以使用迭代器只能对集合进行遍历,不能在遍历的同时增加或删除集合中的元素。
对该缺点的详细分析见 3.3 节。
在通过迭代器来遍历集合元素的同时,增加或者删除集合中的元素,有可能会导致某个元素被重复遍历或遍历不到。不过,并不是所有情况下都会遍历出错,有的时候也可以正常遍历。
以删除元素为例,在代码片段 5 中,当迭代器遍历到 6 的时候,如果从线性表中将迭代器前面的元素删除掉(例如元素 5),6 到 10 这几个元素会依次往前移一位,这就会导致迭代器本来指向元素 6,现在变成了指向元素 7。迭代器继续向下遍历时只能遍历到元素 8、9、10,元素 7 遍历不到了。
这一过程的示意图如图 3 所示。
不过,如果在遍历时删除的不是迭代器前面的元素(元素 1 到 5)以及迭代器所在位置的元素(元素 6),而是迭代器后面的元素(元素 7 到 10),这样就不会存在某个元素遍历不到的情况了。
这一过程的示意图如图 4 所示。
以上讨论了删除元素的情形。增加元素的情形与之类似,也可能存在某一元素被重复遍历的错误情况,示意图如图 5 所示。
当通过迭代器来遍历聚合对象的时候,增加、删除元素会导致不可预期的遍历结果。为了避免出现这种情况,有两种可能的解决方案:
实际上,第一种解决方法比较难实现,而第二种解决方法更加合理。具体原因请阅读参考文献[5]。
迭代器模式是一种对象行为型模式,它提供了一种在不暴露底层表示的情况下,透明地访问和遍历一个集合中的对象的方式。迭代器模式普遍的应用于访问列表、集合、字典等数据类型。
虽然对于 C++、Java、Python 等现代编程语言,迭代器模式已经内置到语言本身或者类库中了,并不需要我们自己去编写相关的实现代码;但是学习迭代器模式的基本结构,掌握迭代器模式的思想,对我们理解面向对象设计的原则有很大帮助。
【参考文献】
[1] 埃里克·伽玛, 理查德·赫尔姆, 拉尔夫·约翰逊等. 设计模式:可复用面向对象软件的基础[M]. 李英军等译. 北京:机械工业出版社, 2019.
[2] 迭代器模式(详解版)[EB/OL]. C语言中文网, [2020-11-28]. http://c.biancheng.net/view/1395.html.
[3] 董威, 文艳军, 齐治昌. 软件设计与体系结构[M]. 第2版. 北京:高等教育出版社, 2017.
[4] Bing_Lee. 简单易懂23种设计模式——迭代器模式[EB/OL]. CSDN博客, (2020-09-15) [2020-11-28]. https://blog.csdn.net/Bing_Lee/article/details/108608431.
[5] 王争. 迭代器模式(中):遍历集合的同时,为什么不能增删集合元素?[EB/OL]. 极客时间, (2020-04-03) [2020-11-28].
https://time.geekbang.org/column/article/219964.
本文系《软件设计与体系结构》课程作业。