no-image

手を動かして学ぶIteratorパターン@TypeScript

デザインパターンのお勉強です。
GoFデザインパターンはよく耳にしますが実際にちゃんと理解しているものは少なく、今回のこのIteratorパターンもその一つでした。
今回これを手を動かして実装するにあたり、モデルはPHPで書かれたこの記事を参考にしました。
言語は何でも良かったのですが、PHPはあまり好きではないので慣れているTypeScriptでやってみました。

Iteratorパターンの使いみち

これに関しては、冒頭で挙げたこの記事の前半部分の説明がわかりやすかったです。

要約すると、データをイテレートする処理を書く場合for文を使うことが多いですが、データの構造が変わったり、処理する順番を変えたりと、少し変更を加えた処理をしようとすると、以前に書いたfor文をたびたび書き直す必要があります。

これを回避するためにIteratorオブジェクトを使うことで、既存の部分に手を入れることなく柔軟に実装できます。

実装してみる

では順番に作っていきます。

僕のリポジトリにTypeScriptの最小のテンプレート(と、その作り方)があるので、実際に手を動かして作って見る場合は参考にしてみてください。

【参考】PracticeProgrammingLanguage/TypeScript/ts_sample_template at master · mrsekut/PracticeProgrammingLanguage

また、完成品は以下のリポジトリにあります。

【参考】PracticeProgrammingLanguage/TypeScript/iterator_pattern at master · mrsekut/PracticeProgrammingLanguage

構成と作る順番

以下の画像はIteratorパターンのクラス図です。
上段の2つが抽象クラス、下段の2つが具象クラスです。

抽象クラス Aggregate は Iterator を作成するための操作 iterator() を実装する。Iterator は、操作 next(), hasNext() を実装する。クラス CocreteAggregate は Aggregate を継承する。ConcreteAggregate は ConcreteIterator を作成する。ConcreteIterator は Iterator を継承し、ConcreteAggregate を属性に持つ。

【画像引用元】Iterator パターン – Wikipedia

2つの抽象クラス

Aggregateインターフェース

集合体を表すインターフェースで、Iteratorを組み込むものにはこれをimplementsして実装します。

Iteratorインターフェース

イテレータのインターフェースで、反復処理を書くときにこれをimplementsして実装します。
そのイテレート中のオブジェクトが次の要素を持っているかの論理値を返すhasNextメソッドと、ある場合は次の要素を返すnextメソッドを持っています。

今回作るものの概要

今回は、著者の情報のリストをイテレートして処理をするモデルを作成します。
まずは、簡単のために以下のような著者の名前だけのリストをイテレートしてみます。
リストの方もひとまずstring[]で進めていきます。

また、わかりやすさのためにそれぞれのファイル名は上のクラス図と同じ名前にし、クラス名はもう少し具体的な名前にしています。

TypeScript的な話をすると、tsconfigでstrict: trueにし、厳し目のルールを適用しています。
また、型推論の利くところは明示的な型の宣言を省き、メソッドのアクセス修飾子のpublicなど何も書かずにデフォルトで設定される部分も省いています。

2つの抽象クラスをつくる

2つのインターフェースを定義します。
サイズも小さくて、上で少し書いたのでコードの掲載は省略します。

【参考】

concreteIterator.ts

Iteratorインターフェースを実装するAuthorListSimpleIteratorクラスを書いていきます。

次の要素があるかどうかの論理値を返すhasNext()と、次の要素を返すnext()メソッドを定義します。

少し話が逸れますが、クラス内でのメソッドの定義にアロー関数を使わない理由が以下のページ書かれてありました。
アロー関数を使った場合、クラス継承時にオーバーライドができないなどいくつかの点で柔軟でないからのようです。

【参考】typescript – Should I write methods as arrow functions in Angular’s class – Stack Overflow

また、TypeScriptでのクラス定義ではconstructorの引数でprivateなどのアクセス修飾子をつけると、プロパティの宣言を省略できます。

そして、上述したとおりhasNext()やnext()にはアクセス修飾子を付していませんが、TypeScriptでは省略した場合はpublicになります。
さらに、メソッドには返り値の型を明示していませんが、interface上で定義されており自明なので型推論されます。

では、試しにこのクラスを動かしてみましょう。
簡単に利用するコードを書いてみます。

結果です。

次の値があるときはtrueと著者名、最後は次の値がないのでfalseとundefinedが返されました。

concreteAggregate.ts

aggregateインターフェースを実装します。

同様に、このクラスを動かしてみます。

結果。末尾に値が追加されているのがわかります。

createIterator()メソッドも触ってみます。
conreteIterator.tsのところでみたものと同じようにして使えます。

結果。最初の要素が返ります。

Book

今まで触ってみたときは、hasNext()などをリストの要素数個書いて実行していましたが、それを自動でやってくれるようにリストの中身をすべて表示してくれるクラスを作成します。

hasNext()の返り値がboolean型であることを利用してwhile文の判定に使っているのがミソですね。

index.ts

さきほどのBookクラスを使って実行していきます。

出力

これで、リストを入れれば自動的に処理を実行してくれるクラスが作ることができました。

複数のクラスを作っていく

これまでは、単純なstring[]型のauthorsSimpleListをイテレートしてただその要素の順番に出力するだけのものでしたが、ここからは少し拡張して以下のような複雑なリストをイテレートさせていきます。

著者の苗字と名前とidが振られています。

今回は、このオブジェクトを突っ込んで、「苗字+名前を出力するクラス」と「id順にソートされた苗字+名前を出力するクラス」を作っていきます。

前者を「AuthorsDetailed」、後者を「AuthorsDetailedOrderById」とクラス名を付けます。
ファイル名はわかりやすいように、「concreteAggregate2.ts」や「concreteIterator2.ts」などとしています。

また、以降はさきほどのauthorsSimpleListの型であるstring[]に新たにエイリアスを定義してAuthor[]としています。

基本的な作成の手順はAuthorSimpleクラスと同じなので省略しますが(githubを参考にしてください)、これらのクラスの中には共通メソッドを扱う部分があるので、これらを切り出すために、mixin的な実装をしている部分があるので、そこだけ紹介します。

concreteAggregateMixin.ts

3つのconcreteAggregateファイルには以下のような共通のメソッドを持っています。
全く同じ定義を3ファイルに書くのも嫌なので、concreteAggregateMixin.tsというファイルに切り出しました。

複数の型に柔軟に対応させるためにジェネリック型を使っているのがミソです。(<T>のところ)
これにより、最初の例のAuthor型にも新たに定義したAuthorDetailed型にも一つのコードで対応できます。

【参考】【TypeScript】ジェネリック型について | 集の一期一会

このmixinクラスは以下のように継承して利用します。

通常、親クラスは継承して利用しますが、mixinはimplementsで実装します。

addToList!: (author: Author) => void;の部分はTypeScriptの「definite assignment assertion」という書き方で、初期化チェックを省略できます。

実行する

では実行していきます。
まずは、「苗字+名前を出力するクラス」です。

結果。

もういっこ。「id順にソートされた苗字+名前を出力するクラス」です

結果。

うまくできました。

メリット

イテレートする対象のリストの構造や、イテレートの仕方に変更があったときでも、concreteIteratorの中身を書き換えるだけで済みます。

100種類のfor文を至るところに書いていると、変更があったときに100箇所書き換えないといけなくなります。

所感

3つのconcreteAggregateの中身って結構似てて、型と、createIterator()の中でnewするIteratorクラスの違いだけなので、クラスがクラスを引数に取れればもっとシンプルになるんじゃないかなとか思いました。

僕がオブジェクト指向やクラス指向のプログラミングを理解していないから思うことかもしれません。
できないってことは多分どこかが破綻するんだろうな。

TypeScriptはかれこれ3,4ヶ月ほど触っていますが、ちゃんとオブジェクト指向っぽい書き方をしたのは初めてで得るものが沢山あってよかったと思いました。

大まかには同じですが、抽象メソッドを定義できないなどPHPと細かい違いがあることにも気づきました。

また、VSCodeの補完の強さに感動したりstrict: trueで型を当てるのに苦労したりしました。

参考