談談設計模式 —— Iterator

Erichain發表於2017-09-26

本文原始碼地址:github.com/Erichain/de…

最近在閱讀《圖解設計模式》一書,書上每一個設計模式涉及的篇幅不是太長,但是,知識點卻都涵蓋了進去。在學習的同時,打算加上自己的理解,將這二十三種設計模式分篇章的一一分享出來。同時,結合相關的範例,我們將這些設計模式以 JavaScript 和 TypeScript 的方式來進行實現。

本文是分享的第一個設計模式 —— Iterator(迭代器模式)。

簡介

只要你是一個程式設計師,那麼,你肯定接觸過迴圈,無論是普通的 for 迴圈還是 while 迴圈,或者是 for-of,for-in 迴圈等。從某種意義上來說,這些迴圈都屬於遍歷的範疇。

比如:

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 類圖
Iterator 類圖

實現

Example 1

關於此模式的具體實現,我們可以先來看一個例子 —— 遍歷人名。

我們一步一步按照類圖來進行實現。

首先定義 Aggregate 介面,我們將其命名為 NamesList,它包含有一個 iterator 方法,返回一個 iterator,而這個 iterator 型別將由我們後面的 Iterator 介面定義,我們暫且將其命名為 NamesIterator

interface NamesList {
  iterator(): NamesIterator;
}複製程式碼

然後,我們需要定義我們的 Iterator 介面 NamesIterator,包含了 next()hasNext() 方法。

next() 方法用來對我們的 NamesList 進行遍歷,每呼叫一次該方法,內部的指標(簡單來說就是內部用於標識當前元素的變數)就會指向下一個元素。

hasNext() 方法即用來表示是否還存在下一個元素。

interface NamesIterator {
  next(): string;
  hasNext(): boolean;
}複製程式碼

介面定義完成之後,我們現在需要做的就是實現這兩個介面。

那麼,首先是我們的 NamesList 介面。當然,我們具體的 NamesList 肯定不止是 iterator 這一個方法了。我們還需要新增元素的方法 add,刪除元素的方法 deleteNameByIndex,獲取 list 長度的方法 getLength,獲取指定元素的方法 getNameByIndex

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。

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 進行遍歷了。

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 來定義一個物件的迭代方法,就像下面這樣:

const myObj = {
  foo: 'foo',
  bar: 'bar',
  baz: 'baz',

  *[Symbol.iterator]() {
    for (let key in this) {
      yield [key, this[key]];
    }
  },
};複製程式碼

然後,我們就可以直接對這個物件進行遍歷了:

for (let item of myObj) {
  console.log(item);

  // ["foo", "foo"]
  // ["bar", "bar"]
  // ["baz", "baz"]
}複製程式碼

我們可以注意到,包含 Symbol.iterator 的這個函式其實就是一個 Iterator,我們如果將其抽象出來,那麼,不管我們的物件是什麼樣,我們都可以在不更改這個函式的情況下對物件進行遍歷。

當然,我們也可以按照物件導向的方式來實現對物件的 Iterator 模式。由於 JavaScript 裡沒有介面和抽象類的概念,所以我們此處就直接採用 class 來實現了。

本例中,我們將書本資訊存成一個物件,然後對其進行遍歷。

同樣的,我們先實現我們的集合類 BooksMap

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 類。

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。

總結

在第一個例子和第二個例子的後半部分中,我們採用了物件導向的方式來實現 Iterator 模式,這其實是必要的。JavaScript 本來就是一門物件導向的語言,只是說,在某些方面,與 Java 這種高階語言相比還欠缺了許多東西,所以我們第一個例子會採用 TypeScript 來代替 JavaScript 實現。

其實,使用抽象的類或者介面是為了讓我們更好的解耦各個類。一味的使用具體的類來進行程式設計的話會導致各個類之間的強耦合,也就會存在難以複用和維護的問題。

我們將在後續的文章中延續這種方式來講解,以便於更容易理解。

對比其他模式

Iterator 模式為本系列的第一個模式,後續模式中將會講解此模式與其他模式的對比。

相關文章