反骨仔

一个业余的 .NET Core 攻城狮

0%

19 遍历聚合对象中的元素 -- 迭代器模式

可以能够电视机看成一个存储电视频道的集合对象,通过遥控器可以对电视机中的电视频道几何进行操作,例如返回上一个频道、跳转到下一个频道或者跳转至指定的频道。遥控器为操作电视频道带来很大方便,用户并不需要知道这些频道到底如何存储在电视机中。

image-20201204111019763

在软件开发中,也存在大量类似电视机一样的类,它们可以存储多个成员对象(元素),这些类通常称为聚合类(Aggregate Classes),对应的对象称为聚合对象。为了更加方便地操作这些聚合对象,同时可以很灵活地为聚合对象增加不同的遍历方法,也需要类似电视机遥控器一样的角色,可以访问一个聚合对象中的元素但又不需要暴露它的内部结构。

通过如迭代器,客户端无需了解聚合对象的内部结构即可实现对聚合对象中成员的遍历,还可以根据需要很方便地增加新的遍历方式。

19.1 销售管理系统中数据的遍历

Sunny 发现经常需要对系统中的商品数据、客户数据等进行遍历,为了复用这些遍历代码,Sunny 公司开发人员设计了一个抽象的数据聚合类 AbstractObjectList,而将存储商品和客户等数据的类作为其子类。AbstractObjectList 类如图。

image-20201204111104927

List 类型的对象 objects 用于存储数据,AbstractObjectList 类的方法。

AbstractObjectList 方法说明

方法名 说明
AbstractObjectList() 构造方法,用于给 objects 对象赋值
addObject() 增加元素
removeObject() 删除元素
getObjects() 获取所有元素
next() 移至下一个元素
isLast() 判断当前元素是否是最后一个元素
previous() 移至上一个元素
isFirst() 判断当前元素是否是第一个元素
getNextItem() 获取下一个元素
getPreviousItem() 获取上一个元素

AbstractObjectList 类的子类 ProductList 和 CustomerList 分别用于存储商品数据和客户数据。

目前存在的问题:

  1. addObject(),removeObject() 等方法用于管理数据,而 next()、isLast()、previous()、isFirst() 等方法用于遍历数据。
    • 导致聚合类的职责过重:既负责存储和管理数据,又负责遍历数据,违反单一职责原则
  2. 如果将抽象聚合类声明为一个接口,则在这个接口中充斥着大量方法,不利于子类实现,违反了接口隔离原则
  3. 如果将所有的遍历操作都交给子类来实现,将导致子类代码庞大,而且必须暴露 AbstractObjectList 的内部存储细节,向子类公开自己的私有属性,否则子类无法实施对数据的遍历,这将破坏 AbstractObjectList 类的封装性

如何解决上述问题:

  • 将聚合类中负责遍历数据的方法提取出来,封装到专门的类中,实现数据存储和数据遍历分离,无需暴露聚合类的内部属性即可对其进行操作

19.2 迭代器模式概述

聚合对象拥有两个职责,一是存储数据;二是遍历数据。从依赖性来看,前者是聚合对象的基本职责;而后者近视可变化的,又是可分离的。因此,可以将遍历数据的行为从聚合对象中分离出来,封装在一个被称之为“迭代器”的对象中,由迭代器来提供遍历聚合对象内部数据的行为,可以简化聚合对象的设计。

迭代器模式(Iterator Pattern):提供一种方法来访问聚合对象,而不用暴露这个对象的内部表示,其别名为游标(Cursor)。

迭代器模式是这一种对象行为型模式。

迭代器模式中包含聚合和迭代器两个层次结构。

image-20201204132427390

角色:

  1. Iterator(抽象迭代器):定义了访问和遍历元素的接口,声明了用于遍历数据元素的方法
    1. first():获取第一个元素
    2. next():访问下一个元素
    3. hasNext():是否还有下一个元素
    4. currentItem():获取当前元素
  2. ConcreteIterator(具体迭代器):实现了抽象迭代器接口,完成对聚合对象的遍历,同时在具体迭代器中通过游标来记录在聚合对象中所处的当前位置,在具体实现时,游标通常是一个表示位置的非负整数
  3. Aggregate(抽象聚合类):用于存储和管理元素对象,声明一个 createIterator() 方法用于创建一个迭代器对象,充当抽象迭代器工厂角色
  4. ConcreteAggregate(具体聚合类):实现了在抽象聚合类中声明的 createIterator() 方法,该方法返回一个与该具体聚合类对应的具体迭代器 ConcreteIterator 实例。

在迭代器模式中,提供了一个外部的迭代器来对聚合对象进行访问和遍历,迭代器定义了一个访问该聚合元素的接口,并且可以跟踪当前遍历的元素,了解哪些元素已经遍历过而哪些没有。迭代器的引入,将使得对一个复杂聚合对象的操作变得简单。

在迭代器模式中应用了工厂方法模式,抽象迭代器对应于抽象产品角色,具体迭代器对应于具体产品角色,抽象聚合类对应于抽象工厂角色,具体聚合类对应于具体工厂角色。

在抽象迭代器中声明了用于遍历聚合对象中所存储元素的方法:

image-20201204133648768

在具体迭代器中将实现抽象迭代器声明的遍历数据的方法:

image-20201204133706031

【注意】

抽象迭代器接口的设计非常重要,一方面需要充分满足各种遍历操作的要求,尽量为各种遍历方法都提供声明,另一方面又不能包含太多方法,接口中方法太多将给子类的实现带来麻烦。因此,可以考虑使用抽象类来设计抽象迭代器,在抽象类中为每一个方法提供一个空的默认实现。如果需要在具体迭代器中为聚合对象增加全新的遍历操作,则必须修改抽象迭代器和具体迭代器的源代码,这将违反开闭原则。

聚合类用于存储数据并负责创建迭代器对象:

image-20201204134451873

具体聚合类作为抽象聚合类的子类,一方面负责存储数据,另一方面实现了在抽象聚合类中声明的工厂方法 createIterator(),用于返回一个与该具体聚合类对应的具体迭代器对象:

image-20201204134519712

【思考】如何理解迭代器模式中具体你聚合类与具体迭代器类之间存在的依赖关系和关联关系?

19.3 完整解决方案

重构后:

  • AbstractObjectList:抽象聚合类
  • ProductList:具体聚合类
  • AbstractIterator:抽象迭代器
  • ProductIterator:具体迭代器

image-20201204183604643

AbstractObjectList.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public abstract class AbstractObjectList
{
protected ArrayList Objects;

protected AbstractObjectList(ArrayList objects)
{
Objects = objects;
}

public void Add(object obj)
{
Objects.Add(obj);
}

public void Remove(object obj)
{
Objects.Remove(obj);
}

public ArrayList Get()
{
return Objects;
}
}

IAbstractIterator.cs

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
/// <summary>
/// 抽象迭代器
/// </summary>
internal interface IAbstractIterator
{
/// <summary>
/// 移动到下一个元素
/// </summary>
void Next();

/// <summary>
/// 判断是否为最后一个元素
/// </summary>
/// <returns></returns>
bool IsLast();

/// <summary>
/// 移动到上一个元素
/// </summary>
void Previous();

/// <summary>
/// 第一个元素
/// </summary>
/// <returns></returns>
bool IsFirst();

/// <summary>
/// 获取下一个元素
/// </summary>
/// <returns></returns>
object GetNextItem();

/// <summary>
/// 获取上一个元素
/// </summary>
/// <returns></returns>
object GetPreviousItem();
}

ProductIterator.cs

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
internal class ProductIterator : IAbstractIterator
{
private readonly ProductList _productList;
private readonly ArrayList _products;
private int _cursor1;
private int _cursor2;

public ProductIterator(ProductList productList)
{
_productList = productList;
_products = productList.Get();
_cursor2 = _products.Count - 1;
}

public void Next()
{
if (_cursor1 < _products.Count)
{
_cursor1++;
}
}

public bool IsLast()
{
return _cursor1 == _products.Count;
}

public void Previous()
{
if (_cursor2 > -1)
{
_cursor2--;
}
}

public bool IsFirst()
{
return _cursor2 == -1;
}

public object GetNextItem()
{
return _products[_cursor1];
}

public object GetPreviousItem()
{
return _products[_cursor2];
}
}

ProductList.cs

1
2
3
4
5
6
7
8
9
10
11
internal class ProductList : AbstractObjectList
{
public ProductList(ArrayList objects) : base(objects)
{
}

public IAbstractIterator CreateIterator()
{
return new ProductIterator(this);
}
}

Program.cs

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
class Program
{
static void Main(string[] args)
{
var products = new ArrayList()
{
"倚天剑",
"屠龙刀",
"断肠草",
"葵花宝典",
"四十二章经"
};

var list = new ProductList(products);
var iterator = list.CreateIterator();

Console.WriteLine("正向遍历");

while ((!iterator.IsLast()))
{
Console.WriteLine(iterator.GetNextItem() + ",");
iterator.Next();
}

Console.WriteLine();
Console.WriteLine("=================================");
Console.WriteLine("逆向遍历:");

while (!iterator.IsFirst())
{
Console.WriteLine(iterator.GetPreviousItem() + ",");
iterator.Previous();
}
}
}

如果需要增加一个新的具体聚合类,例如客户数据聚合类,并且需要为客户数据聚合类提供不同于商品数据聚合类的正向遍历和逆向遍历操作,只需要增加一个新的聚合子类和一个新的具体迭代器类即可,原有类库代码无需修改,符合开闭原则;如果需要为 ProductList 类更换一个迭代器,只需要增加一个新的具体迭代器类作为抽象迭代器类的子类,重新实现遍历方法,原有迭代器代码无需修改,从迭代器的角度来看,也符合开闭原则;但是如果要在迭代器中增加新的方法,则需要修改抽象迭代器源代码,将违背开闭原则。

19.4 使用内部类实现迭代器

具体迭代器类和具体聚合类之间存在双重关系,其中一个关系为关联关系,在具体迭代器中需要维持一个对具体聚合对象的引用,该关联关系的目的是访问存储在聚合对象中的数据,以便于迭代器能够对这些数据进行遍历操作。除了使用关联关系外,为了能够让迭代器可以访问到聚合对象中的数据,可以将迭代器类设计为聚合类的内部类。

image-20201206165438386

image-20201206165450529

无论使用哪种实现细节,客户端代码都是一样的,也就是说客户端无需关心具体迭代器对象的创建细节,只需要通过调用工厂方法 createIterator() 即可得到一个可用的迭代器对象,这也是使用工厂方法模式的好处,通过工厂来封装对象的创建过程,简化了客户端的调用

19.5 JDK 内置迭代器

image-20201206165754987

Collection 接口和 Iterator 接口充当了迭代器模式的抽象层

抽象聚合类 Collection 接口
抽象迭代器 Iterator 接口
具体聚合类 Collection 接口的子类

【思考】

为什么使用 iterator() 方法创建的迭代器无法实现逆向遍历?

19.6 迭代器模式总结

通过引入迭代器可以将数据的遍历功能从聚合对象中分离出来,聚合对象只负责存储数据,遍历数据由迭代器来完成。

很多编程语言的类库已经实现了迭代器模式。

主要优点

  1. 支持以不同的方式遍历一个聚合对象,在同一个聚合对象上可以定义多种遍历方式。在迭代器模式中只需要用一个不同的迭代器来替换原有迭代器即可改变遍历算法,也可以自己定义迭代器的子类以支持新的遍历方式
  2. 迭代器简化了聚合类。由于引入了迭代器,在原有的聚合对象中不需要再自行提供数据遍历等方法,这样可以简化聚合类的设计
  3. 在迭代器模式中,由于引入了抽象层,增加新的聚合类和迭代器类都很方便,无需修改原有代码,满足开闭原则的要求

主要缺点

  1. 由于迭代器模式将存储数据和遍历数据的职责分离,增加新的聚合类需要对应增加新的迭代器类,累的个数成对增加,这在一定程度上增加了系统的复杂性
  2. 抽象迭代器的设计难度较大,需要充分考虑到系统将来的扩展。

适用场景

  1. 访问一个聚合对象的内容而无需暴露它的内部表示。将聚合对象的访问与内部数据的存储分离,使用访问聚合对象时无须了解其内部实现细节
  2. 需要为一个聚合对象提供多种遍历方式
  3. 为遍历不同的聚合结构提供一个统一的接口,在改接口的实现类中为不同的聚合结构提供不同的遍历方式,而客户端可以一致性地操作该接口

参考