本文源代码地址:https://github.com/Erichain/design-patterns-in-typescript/tree/master/iterator

最近在阅读《图解设计模式》一书,书上每一个设计模式涉及的篇幅不是太长,但是,知识点却都涵盖了进去。在学习的同时,打算加上自己的理解,将这二十三种设计模式分篇章的一一分享出来。同时,结合相关的范例,我们将这些设计模式以 JavaScript 和 TypeScript 的方式来进行实现。

本文是分享的第一个设计模式 —— Iterator(迭代器模式)。

简介

只要你是一个程序员,那么,你肯定接触过循环,无论是普通的 for 循环还是 while 循环,或者是 for-of,for-in 循环等。从某种意义上来说,这些循环都属于遍历的范畴。

比如:

1
2
3
for (let i: number = 0; i < list.length; i++) {
console.log(list[i]);
}

这是一个使用 for 循环实现的最简单的迭代器,可以依次输出 list 中的元素。

我们可以注意到,我们的 list 是一个数组,可以直接使用 for 循环来进行遍历,但是,要是我们的 list 长度不一定呢?那么,我们每次进行遍历的时候,就会存在更改迭代器实现的问题。这样的话,就违反了我们所说的开闭原则,所以,我们要引入 Iterator 模式。

概念

用一句话来说,Iterator 模式就是分离集合和迭代器的职责。

让迭代器的实现不依赖于我们的集合,无论集合怎么更改,都不用去更改迭代器,这就是 Iterator 模式的目的所在。

涉及的名词

Aggregate(Interface)

集合接口,包含一个 iterator() 方法,用于创建迭代器。

Concrete Aggregate

具体的集合类,用于实现集合接口的 iterator() 方法来创建迭代器,以及定义集合自己拥有的方法。

Iterator(Interface)

迭代器接口,用于遍历集合,包含 next()hasNext() 方法。

Concrete Iterator

具体的迭代器类,用于实现 Iterator 接口中的 next()hasNext() 方法。

类图

@Iterator 类图

实现

Example 1

关于此模式的具体实现,我们可以先来看一个例子 —— 遍历人名。

我们一步一步按照类图来进行实现。

首先定义 Aggregate 接口,我们将其命名为 NamesList,它包含有一个 iterator 方法,返回一个 iterator,而这个 iterator 类型将由我们后面的 Iterator 接口定义,我们暂且将其命名为 NamesIterator

1
2
3
interface NamesList {
iterator(): NamesIterator;
}

然后,我们需要定义我们的 Iterator 接口 NamesIterator,包含了 next()hasNext() 方法。

next() 方法用来对我们的 NamesList 进行遍历,每调用一次该方法,内部的指针(简单来说就是内部用于标识当前元素的变量)就会指向下一个元素。

hasNext() 方法即用来表示是否还存在下一个元素。

1
2
3
4
interface NamesIterator {
next(): string;
hasNext(): boolean;
}

接口定义完成之后,我们现在需要做的就是实现这两个接口。

那么,首先是我们的 NamesList 接口。当然,我们具体的 NamesList 肯定不止是 iterator 这一个方法了。我们还需要新增元素的方法 add,删除元素的方法 deleteNameByIndex,获取 list 长度的方法 getLength,获取指定元素的方法 getNameByIndex

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class ConcreteNamesList implements NamesList {
private namesList: Array<string> = [];
add(name: string): void {
this.namesList.push(name);
}
deleteNameByIndex(index: number): void {
this.namesList.splice(index, 1);
}
getNameByIndex(index: number): string {
return this.namesList[index];
}
getLength(): number {
return this.namesList.length;
}
iterator(): NamesIterator {
return new ConcreteNamesIterator(this);
}
}

以上就是我们所定义的 ConcreteNamesList 类了,即类图中的 ConcreteAggregate

那么,现在我们需要的就是实现我们的 ConcreteNamesIterator 了。它包含两个方法,nexthasNext

另外,该类还包含两个私有属性,分别是我们的 ConcreteNamesList 实例和当前所迭代的索引值 currentIndex。每调用一次 next 方法,索引值自动加 1。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class ConcreteNamesIterator implements NamesIterator {
private namesList: ConcreteNamesList;
private currentIndex: number = 0;
constructor(namesList: ConcreteNamesList) {
this.namesList = namesList;
}
hasNext(): boolean {
return this.currentIndex < this.namesList.getLength();
}
next(): string {
const currentName: string = this.namesList.getNameByIndex(this.currentIndex);
this.currentIndex++;
return currentName;
}
}

定义好了我们所有的类和方法之后,我们就可以直接使用 Iterator 来对我们的 NamesList 进行遍历了。

1
2
3
4
5
6
7
8
9
10
11
const namesList: ConcreteNamesList = new ConcreteNamesList();
namesList.add('a');
namesList.add('b');
namesList.add('c');
const it: NamesIterator = namesList.iterator();
while (it.hasNext()) {
it.next(); // a, b, c
}

我们将集合的实现和 Iterator 的实现分开,这样,他们之间就不会存在互相影响的问题,无论我们怎么修改我们的 NamesList,无论是添加还是删除元素,只要这个集合能够返回可用的 NamesIterator 类型,我们都无需再次更新我们的迭代器实现,只管直接拿来用就行了。

Example 2

相对于面向对象中的 Iterator 模式,我们在 JavaScript 中也能够找到 Iterator 模式的影子 —— Symbol.iterator

在 JavaScript 中,可以使用 Symbol.iterator 来定义一个对象的迭代方法,就像下面这样:

1
2
3
4
5
6
7
8
9
10
11
const myObj = {
foo: 'foo',
bar: 'bar',
baz: 'baz',
*[Symbol.iterator]() {
for (let key in this) {
yield [key, this[key]];
}
},
};

然后,我们就可以直接对这个对象进行遍历了:

1
2
3
4
5
6
7
for (let item of myObj) {
console.log(item);
// ["foo", "foo"]
// ["bar", "bar"]
// ["baz", "baz"]
}

我们可以注意到,包含 Symbol.iterator 的这个函数其实就是一个 Iterator,我们如果将其抽象出来,那么,不管我们的对象是什么样,我们都可以在不更改这个函数的情况下对对象进行遍历。

当然,我们也可以按照面向对象的方式来实现对对象的 Iterator 模式。由于 JavaScript 里没有接口和抽象类的概念,所以我们此处就直接采用 class 来实现了。

本例中,我们将书本信息存成一个对象,然后对其进行遍历。

同样的,我们先实现我们的集合类 BooksMap

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
class BooksMap {
constructor() {
this.booksMap = {};
}
addBook(key, value) {
Object.assign(this.booksMap, {
[key]: value,
});
}
deleteBookByKey(key) {
delete this.booksMap[key];
}
getBookByKey(key) {
return this.booksMap[key];
}
getBookKeys() {
return Object.keys(this.booksMap);
}
getBooksCount() {
return Object.keys(this.booksMap).length;
}
iterator() {
return new BooksIterator(this);
}
}

然后是我们的 BooksIterator 类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class BooksIterator {
constructor(booksMapInstance) {
this.booksMapInstance = booksMapInstance;
this.currentIndex = 0;
}
next() {
const currentKey = this.booksMapInstance.getBookKeys()[this.currentIndex];
const currentItem = this.booksMapInstance.getBookByKey(currentKey);
this.currentIndex++;
return currentItem;
}
hasNext() {
return this.currentIndex < this.booksMapInstance.getBooksCount();
}
}

我们可以看到,其实这两个类所包含的方法和实现与我们之前实现的 NamesList 类似,只是说,本例中遍历的集合类型换成了对象而已。具体集合的实现和 Iterator 的实现并不耦合,我们修改了对象之后,依旧可以使用定义好的 Iterator。

Example 3

我们采用第一个例子中的 NamesList 集合和 NamesIterator 来说明本例。

本例中,我们改变一下我们的遍历方式,不采用一个一个的遍历,而是采用奇数遍历的方式,调用 next 方法的时候,索引值会加 2。

我们可以很容易的实现这个 Iterator:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class NamesIteratorByOdd implements NamesIterator {
private namesList: ConcreteNamesList;
private currentIndex: number = 0;
constructor(namesList: ConcreteNamesList) {
this.namesList = namesList;
}
hasNext(): boolean {
return this.currentIndex < this.namesList.getLength();
}
next(): string {
const currentName: string = this.namesList.getNameByIndex(this.currentIndex);
this.currentIndex = this.currentIndex + 2;
return currentName;
}
}

但是,这样的话,我们还需要去修改集合中的 iterator 方法来让其返回我们这个 NamesIteratorByOdd 的实例。

所以,我们将其修改一下,获取 Iterator 的时候,我们传入想要的 Iterator。

1
2
3
interface NamesList {
getIterator(iterator: NamesIterator): NamesIterator;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class BetterNamesList implements NamesList {
private namesList: Array<string> = [];
add(name: string): void {
this.namesList.push(name);
}
deleteNameByIndex(index: number): void {
this.namesList.splice(index, 1);
}
getNameByIndex(index: number): string {
return this.namesList[index];
}
getLength(): number {
return this.namesList.length;
}
getIterator(iterator: NamesIterator): NamesIterator {
return iterator;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
const namesList: BetterNamesList = new BetterNamesList();
namesList.add('a');
namesList.add('b');
namesList.add('c');
namesList.add('d');
namesList.add('e');
const it: NamesIterator = namesList.getIterator(new NamesIteratorByOdd(namesList));
while (it.hasNext()) {
it.next(); // a, c, e
}

按照这样的实现,我们就可以定义多种不同的 Iterator,然后只需要给 getIterator 传入我们想要的 Iterator 实例,就能按照我们想要的方式来进行遍历了。

总结

在第一个例子,第三个例子以及第二个例子的后半部分中,我们采用了面向对象的方式来实现 Iterator 模式,这其实是必要的。JavaScript 本来就是一门面向对象的语言,只是说,在某些方面,与 Java 这种高级语言相比还欠缺了许多东西,所以我们第一个例子会采用 TypeScript 来代替 JavaScript 实现。

其实,使用抽象的类或者接口是为了让我们更好的解耦各个类。一味的使用具体的类来进行编程的话会导致各个类之间的强耦合,也就会存在难以复用和维护的问题。

我们将在后续的文章中延续这种方式来讲解,以便于更容易理解。

对比其他模式

Iterator 模式为本系列的第一个模式,后续模式中将会讲解此模式与其他模式的对比。