手を動かして学ぶIteratorパターン@TypeScript
2018/10/24
GoFデザインパターンはよく耳にしますが実際にちゃんと理解しているものは少なく、今回のこのIteratorパターンもその一つでした。 言語は何でも良かったのですが、PHPはあまり好きではないので慣れているTypeScriptでやってみました。 Iteratorパターンの使いみち
要約すると、データをイテレートする処理を書く場合for文を使うことが多いですが、データの構造が変わったり、処理する順番を変えたりと、少し変更を加えた処理をしようとすると、以前に書いたfor文をたびたび書き直す必要があります。
これを回避するためにIteratorオブジェクトを使うことで、既存の部分に手を入れることなく柔軟に実装できます。
実装してみる
では順番に作っていきます。
構成と作る順番
以下の画像はIteratorパターンのクラス図です。
上段の2つが抽象クラス、下段の2つが具象クラスです。 ref https://upload.wikimedia.org/wikipedia/commons/thumb/1/13/Iterator_UML_class_diagram.svg/500px-Iterator_UML_class_diagram.svg.png
2つの抽象クラス
Aggregateインターフェース
集合体を表すインターフェースで、Iteratorを組み込むものにはこれをimplementsして実装します。
Iteratorインターフェース
イテレータのインターフェースで、反復処理を書くときにこれをimplementsして実装します。
そのイテレート中のオブジェクトが次の要素を持っているかの論理値を返すhasNextメソッドと、ある場合は次の要素を返すnextメソッドを持っています。
今回作るものの概要
今回は、著者の情報のリストをイテレートして処理をするモデルを作成します。
まずは、簡単のために以下のような著者の名前だけのリストをイテレートしてみます。
リストの方もひとまずstring[]で進めていきます。
code:ts
const authorsSimpleList: string[] = [
'Kishi Yusuke',
'Inui Kurumi',
'Higashino Keigo',
'Ayatsuji Yukito',
'Hyakuta Naoki'
];
また、わかりやすさのためにそれぞれのファイル名は上のクラス図と同じ名前にし、クラス名はもう少し具体的な名前にしています。
TypeScript的な話をすると、tsconfigでstrict: trueにし、厳し目のルールを適用しています。
また、型推論の利くところは明示的な型の宣言を省き、メソッドのアクセス修飾子のpublicなど何も書かずにデフォルトで設定される部分も省いています。
2つの抽象クラスをつくる
2つのインターフェースを定義します。
サイズも小さくて、上で少し書いたのでコードの掲載は省略します。
【参考】
concreteIterator.ts
Iteratorインターフェースを実装するAuthorListSimpleIteratorクラスを書いていきます。
code:ts
import AuthorIterator from './iterator';
export default class AuthorListSimpleIterator implements AuthorIterator {
private index: number;
constructor(private authors: string[]) {
this.index = 0;
}
hasNext() {
return this.index < this.authors.length;
}
next() {
}
}
次の要素があるかどうかの論理値を返すhasNext()と、次の要素を返すnext()メソッドを定義します。
少し話が逸れますが、クラス内でのメソッドの定義にアロー関数を使わない理由が以下のページ書かれてありました。
アロー関数を使った場合、クラス継承時にoverrideができないなどいくつかの点で柔軟でないからのようです。 また、TypeScriptでのクラス定義ではconstructorの引数でprivateなどのアクセス修飾子をつけると、プロパティの宣言を省略できます。
そして、上述したとおりhasNext()やnext()にはアクセス修飾子を付していませんが、TypeScriptでは省略した場合はpublicになります。
さらに、メソッドには返り値の型を明示していませんが、interface上で定義されており自明なので型推論されます。
では、試しにこのクラスを動かしてみましょう。
簡単に利用するコードを書いてみます。
code:ts
const iterator = new AuthorListSimpleIterator(authorsSimpleList);
console.log(iterate.hasNext());
console.log(iterate.next());
console.log(iterate.hasNext());
console.log(iterate.next());
console.log(iterate.hasNext());
console.log(iterate.next());
console.log(iterate.hasNext());
console.log(iterate.next());
console.log(iterate.hasNext());
console.log(iterate.next());
console.log(iterate.hasNext());
console.log(iterate.next());
結果です。
code:ts
true
Kishi Yusuke
true
Inui Kurumi
true
Higashino Keigo
true
Ayatsuji Yukito
true
Hyakuta Naoki
false
undefined
次の値があるときはtrueと著者名、最後は次の値がないのでfalseとundefinedが返されました。
concreteAggregate.ts
aggregateインターフェースを実装します。
code:ts
import AuthorList from './aggreagate';
import AuthorListSimpleIterator from './concreteIterator';
import AuthorIterator from './Iterator';
export default class AuthorSimple implements AuthorList {
public authorList: string[];
constructor(authors: string[]) {
this.authorList = authors;
}
public addToList(author: string[]) {
this.authorList.push(author);
}
public getAuthorList() {
return this.authorList;
}
public createIterator(): AuthorIterator {
return new AuthorListSimpleIterator(this.authorList);
}
}
同様に、このクラスを動かしてみます。
code:ts
const authorSimple = new AuthorSimple(authorsSimpleList);
authorSimple.addToList('Ikeido Jun');
console.log(authorSimple.getAuthorList());
結果。末尾に値が追加されているのがわかります。
code:ts
[ 'Kishi Yusuke',
'Inui Kurumi',
'Higashino Keigo',
'Ayatsuji Yukito',
'Hyakuta Naoki',
'Ikeido Jun' ]
createIterator()メソッドも触ってみます。
conreteIterator.tsのところでみたものと同じようにして使えます。
code:ts
const iterate = authorSimple.createIterator();
console.log(iterate.next());
結果。最初の要素が返ります。
code:ts
Kishi Yusuke
Book
今まで触ってみたときは、hasNext()などをリストの要素数個書いて実行していましたが、それを自動でやってくれるようにリストの中身をすべて表示してくれるクラスを作成します。
code:ts
import AuthorList from './aggreagate';
import AuthorIterator from './iterator';
export default class Book {
private authorIterator: AuthorIterator;
constructor(authorList: AuthorList) {
this.authorIterator = authorList.createIterator();
}
printAuthors() {
while (this.authorIterator.hasNext()) {
const author = this.authorIterator.next();
console.log(author);
}
}
}
hasNext()の返り値がboolean型であることを利用してwhile文の判定に使っているのがミソですね。
index.ts
さきほどのBookクラスを使って実行していきます。
code:ts
import Book from './book';
import AuthorSimple from './conreteAggregate';
const authorsSimpleList = [
'Matumoto Jun',
'Kobayashi Kentaro',
'Mudata Shuichi',
'Murakami Ryu',
'Kamijou Touma'
];
const bookA = new Book(new AuthorSimple(authorsSimpleList));
bookA.printAuthors();
出力
code:ts
Kishi Yusuke
Inui Kurumi
Higashino Keigo
Ayatsuji Yukito
Hyakuta Naoki
これで、リストを入れれば自動的に処理を実行してくれるクラスが作ることができました。
複数のクラスを作っていく
これまでは、単純なstring[]型のauthorsSimpleListをイテレートしてただその要素の順番に出力するだけのものでしたが、ここからは少し拡張して以下のような複雑なリストをイテレートさせていきます。
code:ts
type AuthorDetailed = {
familyName: string;
givenName: string;
id: number;
};
const authorsDetailedList: AuthorDetailed[] = [
{
familyName: 'Kishi',
givenName: 'Yusuke',
id: 5
},
{
familyName: 'Inui',
givenName: 'Kurumi',
id: 17
},
{
familyName: 'Higashino',
givenName: 'Keigo',
id: 25
},
{
familyName: 'Ayatsuji',
givenName: 'Yukito',
id: 1
},
{
familyName: 'Hyakuta',
givenName: 'Naoki',
id: 48
}
];
著者の苗字と名前とidが振られています。
今回は、このオブジェクトを突っ込んで、「苗字+名前を出力するクラス」と「id順にソートされた苗字+名前を出力するクラス」を作っていきます。
前者を「AuthorsDetailed」、後者を「AuthorsDetailedOrderById」とクラス名を付けます。
ファイル名はわかりやすいように、「concreteAggregate2.ts」や「concreteIterator2.ts」などとしています。
また、以降はさきほどのauthorsSimpleListの型であるstring[]に新たにエイリアスを定義してAuthor[]としています。
code:ts
export type Author = string;
基本的な作成の手順はAuthorSimpleクラスと同じなので省略しますが(githubを参考にしてください)、これらのクラスの中には共通メソッドを扱う部分があるので、これらを切り出すために、mixin的な実装をしている部分があるので、そこだけ紹介します。
concreteAggregateMixin.ts
3つのconcreteAggregateファイルには以下のような共通のメソッドを持っています。
全く同じ定義を3ファイルに書くのも嫌なので、concreteAggregateMixin.tsというファイルに切り出しました。
code:ts
export default class AuthorListMethods<T> {
constructor(public authorList: T[]) {}
public addToList(author: T) {
this.authorList.push(author);
}
public getAuthorList() {
return this.authorList;
}
}
複数の型に柔軟に対応させるためにジェネリック型を使っているのがミソです。(<T>のところ)
これにより、最初の例のAuthor型にも新たに定義したAuthorDetailed型にも一つのコードで対応できます。
このmixinクラスは以下のように継承して利用します。
code:ts
import AuthorList from './aggreagate';
import AuthorListSimpleIterator from './concreteIterator';
import AuthorListMethods from './concreteAggregateMixin';
import { Author } from './index.d';
export default class AuthorSimple
implements AuthorListMethods<Author>, AuthorList {
addToList!: (author: Author) => void;
getAuthorList!: () => Author[];
constructor(public authorList: Author[]) {}
createIterator() {
return new AuthorListSimpleIterator(this.authorList);
}
}
通常、親クラスは継承して利用しますが、mixinはimplementsで実装します。
addToList!: (author: Author) => void;の部分はTypeScriptの「definite assignment assertion」という書き方で、初期化チェックを省略できます。
実行する
では実行していきます。
まずは、「苗字+名前を出力するクラス」です。
code:ts
const bookB = new Book(new AuthorsDetailed(authorsDetailedList));
bookB.printAuthors();
結果。
code:ts
Kishi-Yusuke
Inui-Kurumi
Higashino-Keigo
Ayatsuji-Yukito
Hyakuta-Naoki
もういっこ。「id順にソートされた苗字+名前を出力するクラス」です
code:ts
const bookC = new Book(new AuthorsDetailedOrderById(authorsDetailedList));
bookC.printAuthors();
結果。
code:ts
Kishi-Yusuke
Inui-Kurumi
Higashino-Keigo
Ayatsuji-Yukito
Hyakuta-Naoki
うまくできました。
メリット
イテレートする対象のリストの構造や、イテレートの仕方に変更があったときでも、concreteIteratorの中身を書き換えるだけで済みます。
100種類のfor文を至るところに書いていると、変更があったときに100箇所書き換えないといけなくなります。
所感
3つのconcreteAggregateの中身って結構似てて、型と、createIterator()の中でnewするIteratorクラスの違いだけなので、クラスがクラスを引数に取れればもっとシンプルになるんじゃないかなとか思いました。
僕がオブジェクト指向やクラス指向のプログラミングを理解していないから思うことかもしれません。
できないってことは多分どこかが破綻するんだろうな。
TypeScriptはかれこれ3,4ヶ月ほど触っていますが、ちゃんとオブジェクト指向っぽい書き方をしたのは初めてで得るものが沢山あってよかったと思いました。
大まかには同じですが、抽象メソッドを定義できないなどPHPと細かい違いがあることにも気づきました。
また、VSCodeの補完の強さに感動したりstrict: trueで型を当てるのに苦労したりしました。
参考