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;
}
}
Iterator
interfaceの実装なので、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を実装しなければいけない、を決めておけるのは便利だと感じた。
テストしにくいと思った
クラスのプロパティが「状態」になっており、テストで「状態」を再現しにくくてやりにくいと感じた。クラスのメソッドのテストってこれでやりかたあってるのだろうか。要調査・検討。