初识设计模式——迭代器模式(Iterator Pattern)

cuixiaogang

迭代器模式(Iterator Pattern)是一种行为设计模式,它提供了一种方法顺序访问一个聚合对象中各个元素,而又不暴露该对象的内部表示。借助迭代器模式,你能在不了解集合底层实现的情况下遍历集合中的元素。

迭代器模式的构成

  • 抽象迭代器(Iterator):定义了访问和遍历元素的接口,包含诸如next()、hasNext()等方法。
  • 具体迭代器(Concrete Iterator):实现了抽象迭代器接口,负责具体的遍历操作。
  • 抽象聚合(Aggregate):定义了创建迭代器对象的接口。
  • 具体聚合(Concrete Aggregate):实现了抽象聚合接口,返回一个具体迭代器的实例。

迭器模式结构图

案例

场景

在 WEB 开发中,假设你有一个博客文章列表,需要对文章进行遍历展示。

代码

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
// 抽象迭代器
interface IteratorInterface {
public function hasNext();
public function next();
}

// 具体迭代器
class BlogPostIterator implements IteratorInterface {
private $posts;
private $position = 0;

public function __construct($posts) {
$this->posts = $posts;
}

public function hasNext() {
return $this->position < count($this->posts);
}

public function next() {
if ($this->hasNext()) {
$post = $this->posts[$this->position];
$this->position++;
return $post;
}
return null;
}
}

// 抽象聚合
interface AggregateInterface {
public function createIterator();
}

// 具体聚合
class BlogPostCollection implements AggregateInterface {
private $posts = [];

public function addPost($post) {
$this->posts[] = $post;
}

public function createIterator() {
return new BlogPostIterator($this->posts);
}
}

// 使用示例
$blogCollection = new BlogPostCollection();
$blogCollection->addPost("第一篇博客文章");
$blogCollection->addPost("第二篇博客文章");
$blogCollection->addPost("第三篇博客文章");

$iterator = $blogCollection->createIterator();
while ($iterator->hasNext()) {
echo $iterator->next() . "<br>";
}

UML类图

UML类图

迭代器模式的适用场景

  • 当需要访问一个聚合对象的内容而又无需暴露其内部表示时。
  • 当需要为聚合对象提供多种遍历方式时。
  • 当需要为不同的聚合结构提供统一的遍历接口时。

迭代器模式的优缺点

优点

  • 封装性良好:迭代器模式将遍历逻辑封装在迭代器中,与聚合对象分离,提升了代码的可维护性和可扩展性。
  • 支持多种遍历方式:能够为同一个聚合对象提供不同的迭代器,实现多种遍历方式。
  • 简化聚合类:聚合类只需专注于存储元素,将遍历操作交给迭代器处理,降低了聚合类的复杂度。

缺点

  • 增加类的数量:引入迭代器模式会增加抽象迭代器、具体迭代器等类,使代码结构变得复杂。
  • 对新的遍历方式支持有限:若要添加新的遍历方式,可能需要修改迭代器或聚合类的代码。

迭代器模式与for循环的区别

迭代器模式和直接使用for循环都是用于遍历集合元素的方式,但它们在多个方面存在明显差异。

区别

封装性

  • 迭代器模式:迭代器模式将遍历逻辑封装在迭代器类中。集合对象只需提供创建迭代器的方法,而具体的遍历操作由迭代器完成。这使得集合的内部结构和遍历逻辑分离,提高了代码的封装性和可维护性。例如,在前面的 PHP 图书馆示例中,BookCollection类只负责管理书籍集合,BookIterator类负责遍历书籍,当集合的内部结构发生变化时,只要迭代器接口保持不变,就不会影响到使用迭代器的代码。
  • 直接for循环:直接使用for循环需要在代码中直接访问集合的内部结构,如数组的索引。这意味着代码与集合的具体实现紧密耦合,如果集合的内部结构发生变化,如从数组改为链表,就需要修改for循环的代码。

灵活性

  • 迭代器模式:迭代器模式支持多种遍历方式。可以为同一个集合创建不同的迭代器,每个迭代器实现不同的遍历逻辑。例如,对于一个树形结构的集合,可以创建深度优先遍历迭代器和广度优先遍历迭代器。而且,迭代器可以在不改变集合类的情况下,方便地添加新的遍历方式。
  • 直接for循环:for循环的遍历方式相对固定,通常是按照索引顺序依次访问元素。如果需要实现不同的遍历方式,就需要修改for循环的代码逻辑,不够灵活。

对集合类型的要求

  • 迭代器模式:迭代器模式可以应用于各种类型的集合,无论是数组、链表、树形结构还是其他复杂的数据结构。只要集合类实现了相应的迭代器接口,就可以使用迭代器进行遍历。迭代器隐藏了集合的具体实现细节,使得代码可以统一处理不同类型的集合。
  • 直接for循环:for循环通常适用于具有连续索引的数据结构,如数组。对于一些复杂的数据结构,如链表、树形结构,直接使用for循环进行遍历会比较困难,需要手动处理指针或节点的移动。

代码复用性

  • 迭代器模式:迭代器类可以被多个地方复用。如果有多个不同的集合需要使用相同的遍历方式,只需要为每个集合创建相应的迭代器实例即可。这样可以避免代码重复,提高代码的复用性。
  • 直接for循环:for循环的代码通常是针对特定的集合编写的,难以直接复用。如果需要对不同的集合进行相同的遍历操作,就需要重复编写for循环的代码。

异常处理

  • 迭代器模式:迭代器模式可以在迭代器类中统一处理遍历过程中的异常,如越界访问等。这样可以将异常处理逻辑封装在迭代器内部,使调用代码更加简洁。
  • 直接for循环:在for循环中,需要在代码中手动处理可能出现的异常,如数组越界。这会使for循环的代码变得复杂,并且容易遗漏异常处理逻辑。

案例

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
// 迭代器模式示例
class MyArrayIterator implements Iterator {
private $array;
private $position = 0;

public function __construct($array) {
$this->array = $array;
}

public function rewind() {
$this->position = 0;
}

public function current() {
return $this->array[$this->position];
}

public function key() {
return $this->position;
}

public function next() {
++$this->position;
}

public function valid() {
return isset($this->array[$this->position]);
}
}

$myArray = [1, 2, 3, 4, 5];
$iterator = new MyArrayIterator($myArray);
foreach ($iterator as $value) {
echo $value . " ";
}

// 直接 for 循环示例
$myArray = [1, 2, 3, 4, 5];
for ($i = 0; $i < count($myArray); $i++) {
echo $myArray[$i] . " ";
}

适合直接使用for循环的场景

  • 简单数据结构:遍历数组、列表等线性结构,且遍历逻辑简单(如顺序访问)。
  • 高频次遍历或性能敏感场景:对性能要求极高的循环(如算法核心部分)。
  • 不需要扩展遍历逻辑:仅需顺序遍历,且未来不会增加新的遍历方式(如倒序、过滤等)。

适合迭代器模式的场景

  • 复杂数据结构:遍历链表、树、图等非线性结构,或需要隐藏内部实现细节。
  • 需要统一遍历接口:多个不同集合需使用相同遍历逻辑(如日志系统遍历不同存储类型的日志)。
  • 支持多种遍历方式:需要动态切换遍历逻辑(如先按 ID 排序,再按时间过滤)。
  • 与其他设计模式结合:在工厂模式、组合模式中使用迭代器统一处理元素。

总结

迭代器模式的代码结构更加清晰,将遍历逻辑封装在迭代器类中,而直接for循环则直接操作数组的索引。

场景特征 推荐方式 理由
简单线性结构,无需扩展 for 循环 代码简洁,性能更高
- - -
复杂数据结构或需要解耦 迭代器模式 隐藏内部细节,支持灵活扩展
需要统一遍历接口或复用逻辑 迭代器模式 通过接口标准化,提升代码复用性
性能敏感场景 for 循环 减少对象开销

PHP中的实践

  • 内置迭代器:PHP 的 ArrayIterator、RecursiveArrayIterator 等可直接用于常见数据结构。
  • 延迟加载:迭代器可实现惰性加载(如数据库查询结果分页),避免一次性加载所有数据。
  • 与 foreach 结合:实现 Iterator 接口后,可直接使用 foreach 语法糖,简化代码。

PHP中的Iterator接口

在 PHP 里,Iterator接口完全符合迭代器模式的设计规范,它为对象提供了一种自定义遍历行为的途径。当一个类实现了Iterator接口,就能够借助foreach循环对该对象进行遍历,这就如同遍历数组一样。

Iterator接口定义

Iterator接口定义了 5 个必须实现的方法,这些方法构成了迭代器的核心逻辑:

1
2
3
4
5
6
7
interface Iterator extends Traversable {
public function rewind();
public function current();
public function key();
public function next();
public function valid();
}
  • rewind():把迭代器的指针重置到起始位置,通常在遍历开始时调用。
  • current():返回当前指针所指向的元素。
  • key():返回当前元素的键(索引)。
  • next():将指针移动到下一个元素的位置。
  • valid():检查当前指针位置是否有效,也就是是否存在元素。

如何使用

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
class MyCollection implements Iterator {
private $items = [];
private $position = 0;

public function __construct($items) {
$this->items = $items;
}

public function rewind() {
$this->position = 0;
}

public function current() {
return $this->items[$this->position];
}

public function key() {
return $this->position;
}

public function next() {
++$this->position;
}

public function valid() {
return isset($this->items[$this->position]);
}
}

$collection = new MyCollection([1, 2, 3, 4, 5]);
foreach ($collection as $key => $value) {
echo "Key: $key, Value: $value<br>";
}
  1. 初始化:当执行foreach循环时,PHP 会自动调用$collection->rewind(),把position重置为 0。
    第一次循环:
  2. 调用$collection->valid(),因为position为 0,isset($this->items[$this->position])返回true,所以循环继续。
  3. 调用$collection->current(),返回$this->items[0],也就是 1。
  4. 调用$collection->key(),返回$this->position,即 0。
  5. 调用$collection->next(),将position加 1,变为 1。
  6. 后续循环:重复步骤 2,直到$collection->valid()返回false。当position超出数组范围时,isset($this->items[$this->position])返回false,循环结束。

通过实现Iterator接口,我们可以自定义对象的遍历逻辑,让对象在foreach循环中表现出特定的行为。