💡gntk.dev

【Iterator】GoFのデザインパターンを学ぶ

GoFのデザインパターンをTypeScriptで写経しました。初回はIteratorパターンです(シリーズ化したい)。

post-cover

GoFのデザインパターンを学びたい。あとTypeScriptのclass構文全然知らないなと思い、GoFのデザインパターンをTypeScriptで写経しようと思った。初回はIteratorパターン。

概要

配列に含まれる要素を順に走査し、何かしらの処理をするための働きを一般化して再利用を促すパターンをIteratorパターンと呼ぶ。

サンプルプログラム

結城浩先生の増補改訂版Java言語で学ぶデザインパターン入門のJavaプログラムをTypeScriptで写経した。

Iteratorパターンを表現するinterface

iterator.ts

export interface Iterator {
  hasNext: () => boolean;
  next: () => unknown;
}

aggregate.ts

import { Iterator } from 'iterator/iterator';

export interface Aggregate {
  iterator: () => Iterator;
}

Iteratorパターンを利用する「集合体」は、implements Aggregateとなるようにする。Aggregateを実装するクラスは、Iteratorを作成するiteratorメソッドを必ず実装しなければならない。iteratorメソッドによって作成されるIteratorは、「次の要素」が存在するかどうかをチェックするhasNextメソッドと「次の要素」を取得するためのnextメソッドを持つ。

Iteratorパターンを適用するクラス

本と本棚のクラスを用意し、Iteratorパターンを利用して本棚から本を取り出せるようにメソッドを実装する。

book.ts

export class Book {
  private name: string;

  public constructor(name: string) {
    this.name = name;
  }

  public getName(): string {
    return this.name;
  }
}

bookShelf.ts

import { Aggregate } from 'iterator/aggregate';
import { Book } from 'iterator/book';
import { Iterator } from 'iterator/iterator';
import { BookShelfIterator } from 'iterator/bookShelfIterator';

export class BookShelf implements Aggregate {
  private books: Book[];
  private last: number = 0;

  public constructor(maxsize: number) {
    this.books = Array(maxsize);
  }

  public getBookAt(index: number): Book {
    return this.books[index];
  }

  public appendBook(book: Book): void {
    this.books[this.last++] = book;
  }

  public getLength(): number {
    return this.last;
  }

  public iterator(): Iterator {
    return new BookShelfIterator(this);
  }
}

bookはnameプロパティを持つ本インスタンスを作成でき、bookShelfはbookの配列と配列の長さ(本の数)をプロパティに持つ本棚インスタンスを作成する。bookShelfのappendBookメソッドを使って、本棚に本を追加できる。また、本棚の中の本を操作するためのIteratorを作成するiteratorメソッドも用意してある。本棚のIteratorを作成するBookShelfIteratorクラスは以下。

bookShelfIterator.ts

import { Iterator } from 'iterator/iterator';
import { BookShelf } from 'iterator/bookShelf';
import { Book } from 'iterator/book';

export class BookShelfIterator implements Iterator {
  private bookShelf: BookShelf;
  private index: number;

  public constructor(bookShelf: BookShelf) {
    this.bookShelf = bookShelf;
    this.index = 0;
  }

  public hasNext(): boolean {
    return this.index < this.bookShelf.getLength();
  }

  public next(): Book {
    const book: Book = this.bookShelf.getBookAt(this.index++);
    return book;
  }
}

Iteratorinterfaceの実装なので、hasNextメソッドとnextメソッドを用意している。

動作確認のためのテストは以下のとおり。

import { BookShelf } from 'iterator/bookShelf';
import { Book } from 'iterator/book';
import { Iterator } from 'iterator/iterator';

describe('Iterator pattern', () => {
  const bookShelf = new BookShelf(4);
  bookShelf.appendBook(new Book('Around the World in 80 Days'));
  bookShelf.appendBook(new Book('Bible'));
  bookShelf.appendBook(new Book('Cinderella'));
  bookShelf.appendBook(new Book('Daddy-Long-Legs'));

  test('iteratorによって本が取り出せる', () => {
    const it: Iterator = bookShelf.iterator();

    const books = [];
    while (it.hasNext()) {
      const book = it.next() as Book;
      books.push(book.getName());
    }

    const expected = [
      'Around the World in 80 Days',
      'Bible',
      'Cinderella',
      'Daddy-Long-Legs',
    ];
    expect(books).toEqual(expected);
  });
});

考察・感想

抽象クラスの意味・役割が理解できた

Aggregateを実装するクラスは必ずIteratorを返すメソッドを用意しなければいけない、IteratorはhasNextとnextを実装しなければいけない、を決めておけるのは便利だと感じた。

テストしにくいと思った

クラスのプロパティが「状態」になっており、テストで「状態」を再現しにくくてやりにくいと感じた。クラスのメソッドのテストってこれでやりかたあってるのだろうか。要調査・検討。

© 2021 gntk.dev