设计模式之迭代器模式
迭代器模式提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露其内部的表示。
解析这个定义:
- “聚合对象”,表示对象中有数组、集合、字典等数据类型属性。
- “顺序访问”,表示模式需提供一个遍历的方式。
- “不暴露其内部的表示”,表明不想让客户端知道太多内部细节。
综合来看,此模式的设计意图非常明确,将聚合对象的遍历访问职责封装抽离出来。
直接抛出 Iterator
接口和 next()
、hasNext()
方法有些“上帝视角”了,让我们抛开预设的实现,从零开始推导如何设计这个“抽离聚合对象访问”的模式,一步步探索最终如何自然演化到经典的迭代器结构。
第一步:明确核心问题
假设我们有一个聚合对象(比如一个自定义集合 MyCollection
),内部用数组存储数据。现在需要让外部能遍历它的元素,但不允许直接暴露内部数组。如何设计?
初始代码(暴露内部实现,不符合封装原则):
class MyCollection {
private String[] data = {"A", "B", "C"};
// 直接返回内部数组(危险!)
public String[] getData() {
return data;
}
}
// 客户端使用时需要知道内部是数组
String[] data = collection.getData();
for (int i = 0; i < data.length; i++) {
System.out.println(data[i]);
}
问题:客户端依赖具体存储结构(数组),一旦 MyCollection
内部改用链表,所有客户端代码都要修改。
第二步:尝试初步封装遍历
为了隐藏内部结构,我们可以在聚合对象内部实现遍历逻辑:
class MyCollection {
private String[] data = {"A", "B", "C"};
public void traverse() {
for (String item : data) {
System.out.println(item); // 或通过回调处理元素
}
}
}
问题:遍历逻辑硬编码在聚合类中(比如只能用顺序遍历)。无法灵活支持多种遍历方式(如逆序、过滤等)。
第三步:分离遍历职责
既然遍历逻辑可能变化,我们需要将其从聚合类中抽离出来。这时自然会想到——用一个独立对象专门负责遍历,创建独立的遍历器。
// 聚合类提供创建遍历器的方法
class MyCollection {
private String[] data = {"A", "B", "C"};
public ??? createTraverser() {
return new ???(data); // 返回某种遍历器
}
}
在返回某种遍历器时,可以看到将data作为参数传入,也就是将这个要遍历的内部数据交给了遍历器,剩下的遍历动作,直接找遍历器就可以了,遍历器要做的就是规范会访问的方式,所谓规范化,就是要统一,各种有遍历诉求的对象及不同的聚合类型,都要有统一的方法,客户端在调用过程中,也会更加清晰。
既然要规范化,自然利用接口定义好方法。
关键问题:遍历器的接口应该是什么?如何保证遍历器能访问聚合对象的内部数据(但又不暴露给客户端)?
第四步:设计遍历器接口
遍历器需要提供一种通用的元素访问方式,但不依赖具体存储结构。可能的操作包括:
- 判断是否还有元素(避免越界)。
- 获取当前元素并移动到下一个位置。
尝试定义接口,注意接口名称及其具体的方法,根据实际需求去定义即可,后续可能还会有逆序要求,则需要扩展接口。
interface Traverser {
boolean isDone(); // 是否遍历结束
String current(); // 获取当前元素
void next(); // 移动到下一个元素
}
(注:这里故意不用 Iterator
和 hasNext()
/next()
命名,避免先入为主。)
第五步:实现具体遍历器
针对数组实现一个具体的遍历器:
class ArrayTraverser implements Traverser {
private String[] array;
private int index = 0;
public ArrayTraverser(String[] array) {
this.array = array;
}
@Override
public boolean isDone() {
return index >= array.length;
}
@Override
public String current() {
return array[index];
}
@Override
public void next() {
index++;
}
}
修改聚合类:
class MyCollection {
private String[] data = {"A", "B", "C"};
public Traverser createTraverser() {
return new ArrayTraverser(data);
}
}
第六步:客户端使用
MyCollection collection = new MyCollection();
Traverser traverser = collection.createTraverser();
while (!traverser.isDone()) {
System.out.println(traverser.current());
traverser.next();
}
客户端完全不知道内部是数组。若要改为链表存储,只需修改 ArrayTraverser
为 LinkedListTraverser
,客户端代码不变。
第七步:发现模式共性
观察上述设计,可以发现:
Traverser
接口的本质是抽象遍历行为。- 不同数据结构的遍历器实现不同,但接口一致。
- 聚合对象返回遍历器。
此时,将 Traverser
改名为 Iterator
,isDone()
改为 hasNext()
,current()
+ next()
合并为 next()
,就是经典的迭代器模式!
