カナデKanade

15. イテレーターパターン (Iterator)

コレクションの中身がどうなっていても、同じやり方で1つずつ取り出せるようにするイテレーターパターンについて学びましょう。

ひとことで言うと
コレクション(リスト、リストの中のリスト、木など)の中身がどうなっているかを見せずに、「残ってる?」と「次をちょうだい」の2つだけで1つずつ取り出せるようにするパターンです。

問題: コレクションの中身を全部知らないと使えないコード

音楽アプリを作るとしましょう。曲を集めておくやり方が2つあります。

  • プレイリストは、曲をリスト1つに順番に入れます。
  • アルバム棚は、アルバムをいくつも入れ、アルバムごとに曲のリストがあります。リストの中にリストがあるわけです。

どちらのコレクションでも「最初から最後まで再生」と「最初の何曲かだけ試聴」をしたいです。まず思いつくのは、使う側がコレクションの中身を直接回る方法です。

【プレイリスト】を設計しよう:
    『曲たち』を【】にしよう

    〈入れる〉を作ろう(【文字列】の『曲』):
        『私』の『曲たち』に『曲』を追加しよう

【アルバム】を設計しよう:
    『名前』を【文字列】の「」にしよう
    『曲たち』を【】にしよう

    最初に作られる時(【文字列】の『名前』, 『曲たち』)次のようにしよう:
        『私』の『名前』を『名前』にしよう
        『私』の『曲たち』を『曲たち』にしよう

【アルバム棚】を設計しよう:
    『アルバムたち』を【】にしよう

    〈入れる〉を作ろう(【アルバム】の『アルバム』):
        『私』の『アルバムたち』に『アルバム』を追加しよう

(参考:使う側がコレクションの中身を知らなければなりません。プレイリストはリスト1つ、アルバム棚はリストの中のリストです)
〈プレイリスト再生〉を作ろう(『リスト』):
    『番号』を1にしよう
    『リスト』の『曲たち』の『曲』ごとに繰り返そう:
        枠「{『番号』}. {『曲』}」を出力しよう
        『番号』を(『番号』 + 1)にしよう

〈アルバム棚再生〉を作ろう(『棚』):
    『番号』を1にしよう
    『棚』の『アルバムたち』の『アルバム』ごとに繰り返そう:
        『アルバム』の『曲たち』の『曲』ごとに繰り返そう:
            枠「{『番号』}. {『曲』}」を出力しよう
            『番号』を(『番号』 + 1)にしよう

(参考:「最初の何曲かだけ試聴」もコレクションごとに別々に作ります。繰り返しが二重だと「繰り返しを終わろう」では内側しか終わらないので、返そうで抜け出します)
〈プレイリスト試聴〉を作ろう(『リスト』, 【数字】の『曲数』):
    『聞いた数』を0にしよう
    『リスト』の『曲たち』の『曲』ごとに繰り返そう:
        もし(『聞いた数』が『曲数』と同じだ)なら:
            繰り返しを終わろう
        枠「試聴: {『曲』}」を出力しよう
        『聞いた数』を(『聞いた数』 + 1)にしよう

〈アルバム棚試聴〉を作ろう(『棚』, 【数字】の『曲数』):
    『聞いた数』を0にしよう
    『棚』の『アルバムたち』の『アルバム』ごとに繰り返そう:
        『アルバム』の『曲たち』の『曲』ごとに繰り返そう:
            もし(『聞いた数』が『曲数』と同じだ)なら:
                返そう
            枠「試聴: {『曲』}」を出力しよう
            『聞いた数』を(『聞いた数』 + 1)にしよう

『通勤』を新しい【プレイリスト】()にしよう
『通勤』の〈入れる〉(「朝の光」)を実行しよう
『通勤』の〈入れる〉(「走るバス」)を実行しよう
『通勤』の〈入れる〉(「コーヒー一杯」)を実行しよう

『私の棚』を新しい【アルバム棚】()にしよう
『私の棚』の〈入れる〉(新しい【アルバム】(「春」, 【「桜」, 「新芽」】))を実行しよう
『私の棚』の〈入れる〉(新しい【アルバム】(「夏」, 【「海」, 「夕立」, 「スイカ」】))を実行しよう

〈プレイリスト再生〉(『通勤』)を実行しよう
〈アルバム棚再生〉(『私の棚』)を実行しよう
〈プレイリスト試聴〉(『通勤』, 2)を実行しよう
〈アルバム棚試聴〉(『私の棚』, 3)を実行しよう

実行結果\

  1. 朝の光\
  2. 走るバス\
  3. コーヒー一杯\
  4. 桜\
  5. 新芽\
  6. 海\
  7. 夕立\
  8. スイカ
    試聴: 朝の光
    試聴: 走るバス
    試聴: 桜
    試聴: 新芽
    試聴: 海

ちゃんと動きます。でも、コレクションとやることが増えると痛くなります。

  • 使う側がコレクションの中身を全部知っています。〈アルバム棚再生〉は、アルバム棚の中に『アルバムたち』があり、アルバムごとに『曲たち』があることまで知らなければなりません。曲を取り出したいだけなのに。
  • やることとコレクションの種類をかけた数だけ関数ができます。 やること2つ(再生、試聴)にコレクション2つなので、関数が4つです。「曲数を数える」を足せばまた2つ増え、「おすすめリスト」のような新しいコレクションができれば、やることごとに1つずつまた増えます。
  • 中身を変えると、使う側が全部壊れます。 アルバム棚がアルバムを辞書に入れるように変わったら、『アルバムたち』を回る関数を全部直さなければなりません。
  • 中身を隠せません。 使う側が『曲たち』を直接取り出さなければならないので、隠そうが使えません。すると、だれでも『曲たち』に変なものを入れられてしまいます。
  • 繰り返しが二重だと、止めるのもぎこちないです。繰り返しを終わろうはいちばん内側の繰り返ししか終わらないので、〈アルバム棚試聴〉は返そうで関数ごと抜け出すしかありませんでした。

解決策: 「1つずつ取り出す」係を別に置く

イテレーターパターンは、コレクションを回る仕事を反復子(イテレーター)というオブジェクトに任せます。

  1. 「1つずつ取り出す」の約束を決めます。〈残っているか〉(まだ残っている?)と〈次〉(次のものをちょうだい)の2つで十分です。
  2. コレクションごとに、自分の中身に合った反復子を作ります。プレイリストの反復子はリストの何番目まで取り出したかを覚え、アルバム棚の反復子は何番目のアルバムの何番目の曲まで取り出したかを覚えます。
  3. コレクションは反復子を1つ作ってあげる仕事だけをします。中身は隠しておきます。
  4. 使う側は反復子に「残ってる?次をちょうだい」だけをくり返します。相手がプレイリストなのかアルバム棚なのか知りません。

たとえ: しおり

本を読む時、本が何ページずつ束ねて製本されているかを知らなくても大丈夫です。しおりが「ここまで読みました」を覚えてくれるので、しおりの次のページを開くだけです。同じ本にしおりを2つはさめば、2人がそれぞれ別のところまで読むこともできます。反復子は、コレクションにはさむしおりです。

だれが何をするの?

役割 この例では やること
反復子の約束 【反復子】(規定) 〈残っているか〉と〈次〉を決めます
具体的な反復子 【リスト反復子】、【アルバム棚反復子】 自分のコレクションの中身を知っていて、どこまで取り出したかを覚えます
コレクションの約束 【曲の集まり】(規定) 反復子を1つ作ってあげられなければなりません
具体的なコレクション 【プレイリスト】、【アルバム棚】 中身を隠し、自分に合った反復子を作ってあげます
使う側 〈再生〉、〈試聴〉 反復子に聞いて受け取るだけです

カナデで作る

同じ音楽アプリを、イテレーターパターンで作り直してみましょう。

(参考:ステップ1 — 「1つずつ取り出す」約束。残っているか聞いて、次のものを受け取ります)
【反復子】を規定しよう:
    【論理】を返す〈残っているか〉がなければならない()
    【文字列】を返す〈次〉がなければならない()

(参考:コレクションの約束。中身がどうであれ、反復子を1つ作ってあげられればいいんです)
【曲の集まり】を規定しよう:
    【反復子】を返す〈順に〉がなければならない()

(参考:ステップ2 — プレイリストとその反復子。「今どこまで取り出したか」を反復子が覚えます)
【反復子】に従う【リスト反復子】を設計しよう:
    『曲たち』を【】に隠そう
    『位置』を【数字】の1に隠そう

    最初に作られる時(『曲たち』)次のようにしよう:
        『私』の『曲たち』を『曲たち』にしよう

    【論理】を返す〈残っているか〉を作ろう():
        (『私』の『位置』 <= 『私』の『曲たち』の『長さ』)を返そう

    【文字列】を返す〈次〉を作ろう():
        『場所』を『私』の『位置』にしよう
        『曲』を『私』の『曲たち』の『場所』番目にしよう
        『私』の『位置』を(『私』の『位置』 + 1)にしよう
        『曲』を返そう

【曲の集まり】に従う【プレイリスト】を設計しよう:
    『曲たち』を【】に隠そう

    〈入れる〉を作ろう(【文字列】の『曲』):
        『私』の『曲たち』に『曲』を追加しよう

    【反復子】を返す〈順に〉を作ろう():
        新しい【リスト反復子】(『私』の『曲たち』)を返そう

(参考:ステップ3 — アルバム棚とその反復子。リストの中のリストを回る仕事を反復子が引き受けます)
【アルバム】を設計しよう:
    『名前』を【文字列】の「」にしよう
    『曲たち』を【】にしよう

    最初に作られる時(【文字列】の『名前』, 『曲たち』)次のようにしよう:
        『私』の『名前』を『名前』にしよう
        『私』の『曲たち』を『曲たち』にしよう

【反復子】に従う【アルバム棚反復子】を設計しよう:
    『アルバムたち』を【】に隠そう
    『アルバム位置』を【数字】の1に隠そう
    『曲位置』を【数字】の1に隠そう

    最初に作られる時(『アルバムたち』)次のようにしよう:
        『私』の『アルバムたち』を『アルバムたち』にしよう

    【論理】を返す〈残っているか〉を作ろう():
        (参考:今のアルバムを取り出し終えたら、次のアルバムの最初の曲へ進みます。空のアルバムは飛ばします)
        (『私』の『アルバム位置』 <= 『私』の『アルバムたち』の『長さ』)間繰り返そう:
            『何番目のアルバム』を『私』の『アルバム位置』にしよう
            『アルバム』を『私』の『アルバムたち』の『何番目のアルバム』番目にしよう
            もし(『私』の『曲位置』 <= 『アルバム』の『曲たち』の『長さ』)なら:
                真を返そう
            『私』の『アルバム位置』を(『私』の『アルバム位置』 + 1)にしよう
            『私』の『曲位置』を1にしよう
        偽を返そう

    【文字列】を返す〈次〉を作ろう():
        『何番目のアルバム』を『私』の『アルバム位置』にしよう
        『何番目の曲』を『私』の『曲位置』にしよう
        『アルバム』を『私』の『アルバムたち』の『何番目のアルバム』番目にしよう
        『曲』を『アルバム』の『曲たち』の『何番目の曲』番目にしよう
        『私』の『曲位置』を(『私』の『曲位置』 + 1)にしよう
        『曲』を返そう

【曲の集まり】に従う【アルバム棚】を設計しよう:
    『アルバムたち』を【】に隠そう

    〈入れる〉を作ろう(【アルバム】の『アルバム』):
        『私』の『アルバムたち』に『アルバム』を追加しよう

    【反復子】を返す〈順に〉を作ろう():
        新しい【アルバム棚反復子】(『私』の『アルバムたち』)を返そう

(参考:ステップ4 — 使う側はコレクションの中身を知りません。残っているか聞いて、次を受け取るだけです)
〈再生〉を作ろう(【曲の集まり】の『集まり』):
    『取り出し』を『集まり』の〈順に〉()にしよう
    『番号』を1にしよう
    (『取り出し』の〈残っているか〉()が真と同じだ)間繰り返そう:
        枠「{『番号』}. {『取り出し』の〈次〉()}」を出力しよう
        『番号』を(『番号』 + 1)にしよう

〈試聴〉を作ろう(【曲の集まり】の『集まり』, 【数字】の『曲数』):
    『取り出し』を『集まり』の〈順に〉()にしよう
    『聞いた数』を0にしよう
    ((『取り出し』の〈残っているか〉()が真と同じだ) かつ (『聞いた数』 < 『曲数』))間繰り返そう:
        枠「試聴: {『取り出し』の〈次〉()}」を出力しよう
        『聞いた数』を(『聞いた数』 + 1)にしよう

『通勤』を新しい【プレイリスト】()にしよう
『通勤』の〈入れる〉(「朝の光」)を実行しよう
『通勤』の〈入れる〉(「走るバス」)を実行しよう
『通勤』の〈入れる〉(「コーヒー一杯」)を実行しよう

『私の棚』を新しい【アルバム棚】()にしよう
『私の棚』の〈入れる〉(新しい【アルバム】(「春」, 【「桜」, 「新芽」】))を実行しよう
『私の棚』の〈入れる〉(新しい【アルバム】(「夏」, 【「海」, 「夕立」, 「スイカ」】))を実行しよう

〈再生〉(『通勤』)を実行しよう
〈再生〉(『私の棚』)を実行しよう
〈試聴〉(『通勤』, 2)を実行しよう
〈試聴〉(『私の棚』, 3)を実行しよう

実行結果\

  1. 朝の光\
  2. 走るバス\
  3. コーヒー一杯\
  4. 桜\
  5. 新芽\
  6. 海\
  7. 夕立\
  8. スイカ
    試聴: 朝の光
    試聴: 走るバス
    試聴: 桜
    試聴: 新芽
    試聴: 海

結果はリファクタリング前と1行も変わりません。でも今は、やることごとに関数が1つずつだけで、コレクションの中身はすべて隠れています。

ステップごとにもう一度

  • ステップ1 — 1つずつ取り出す約束。【反復子】は〈残っているか〉と〈次〉の2つだけを約束します。【曲の集まり】は「反復子を作ってあげられれば曲の集まり」という約束です。
  • ステップ2 — リストを回る反復子。【リスト反復子】は曲のリストと『位置』を持っています。〈次〉を呼ぶたびに今の位置の曲を渡し、位置を1つ進めます。この「どこまで取り出したか」を反復子が覚えているのが大事なところです。
  • ステップ3 — 二重を回る反復子。【アルバム棚反復子】は、アルバムの位置と曲の位置を一緒に覚えます。今のアルバムを取り出し終えたら、〈残っているか〉が次のアルバムの最初の曲へ進みます。二重の繰り返しが反復子の中に隠れました。
  • ステップ4 — 使う側は聞いて受け取るだけ。〈再生〉と〈試聴〉は【曲の集まり】を受け取って〈順に〉()で反復子をもらい、〈残っているか〉が真の間〈次〉を受け取ります。プレイリストでもアルバム棚でも、同じ関数1つで済みます。試聴も条件1つで止まるので、二重の繰り返しから抜け出す心配がありません。

反復子を2つ作ったら?
〈順に〉()を呼ぶたびに新しい反復子ができ、位置は反復子ごとに別々です。だから1つのプレイリストを、2人が別々に進めながら聞けます。1冊の本にしおりを2つはさむのと同じです。

これでコレクションの中身は隠そうで隠せます。使う側が前のように中身を直接取り出そうとすると、その場で止められます。

【プレイリスト】を設計しよう:
    『曲たち』を【】に隠そう

    〈入れる〉を作ろう(【文字列】の『曲』):
        『私』の『曲たち』に『曲』を追加しよう

『通勤』を新しい【プレイリスト】()にしよう
『通勤』の〈入れる〉(「朝の光」)を実行しよう

(参考:使う側が中身を直接取り出そうとすると止められます)
『通勤』の『曲たち』の『曲』ごとに繰り返そう:
    『曲』を出力しよう

新しいコレクションを足してみると

「おすすめリスト」という新しいコレクションができたとしましょう。中身はまた違う形です。

  • 前のやり方:〈おすすめリスト再生〉、〈おすすめリスト試聴〉のように、やることごとに関数をまた1つずつ作らなければなりません。
  • イテレーターパターン:【曲の集まり】に従う【おすすめリスト】とその反復子を1つずつだけ作れば済みます。〈再生〉と〈試聴〉は直さなくても、そのままおすすめリストを再生します。

反対に、新しいやることができても(曲数を数えるなど)、反復子を使う関数を1つだけ作れば、すべてのコレクションで使えます。

やってみよう
【プレイリスト】に、最後から取り出す〈逆順に〉を足してみてください。『位置』が最後の曲から始まって1つずつ減っていく【逆順反復子】を作り、〈逆順に〉がそれを返せばいいんです。それから【反復子】を受け取って再生する関数を作り、『通勤』の〈逆順に〉()を渡してみましょう。3. コーヒー一杯からではなく1. コーヒー一杯から出れば成功です。

カナデのごとに繰り返そうとの関係

『リスト』の『曲』ごとに繰り返そうは、リストから1つずつ取り出す仕事をカナデが代わりにやってくれます。イテレーターパターンと同じ考え方です。ただ、ごとに繰り返そうが回れるのは、リスト、文字列、辞書のようにカナデがもともと知っているコレクションだけです。自分で作った【アルバム棚】のように中身が特別なコレクションは、このページのように反復子を作って、同じやり方で取り出せるようにしてあげます。

どんな時に使うといいの?

  • コレクションの中身を隠したい時
  • 中身がそれぞれ違うコレクションを、同じやり方で回りたい時
  • 1つのコレクションをいくつもの順番で回りたい時(順に、逆順に、飛ばしながら)
  • 同じコレクションをいくつもの場所で別々に回る必要がある時

いいところと気になるところ

いいところ 気になるところ
使う側がコレクションの中身を知らなくて済みます リストが1つだけなら、ごとに繰り返そうより長くて面倒です
回り方が反復子の中にまとまるので、やることごとにコピーされません 反復子が位置を覚えている間にコレクションが変わると、違う曲を渡すことがあります
新しいコレクションや新しい順番を足しても、使う側はそのままです コレクションごとにクラスが1つずつ増えます
反復子ごとに位置が別なので、いくつもの場所で同時に回れます

早すぎるパターンは毒です。
コレクションがリスト1つだけで、中身を隠す理由もないなら、ごとに繰り返そうで十分です。中身の違うコレクションが2つ以上できたり、同じコレクションをいくつもの順番で回る必要が出てきたりした時に、反復子を取り出しても遅くありません。

ほかのパターンとの関係

  • コンポジットパターンの木を回るのによく合います。 フォルダの中のフォルダまで、すべてのファイルを1つずつ取り出す仕事を反復子に任せれば、使う側は木の深さを知らなくて済みます。
  • ファクトリーパターンが隠れています。〈順に〉()は、コレクションごとにふさわしい反復子を選んで作ってくれるファクトリーメソッドです。
  • ストラテジーパターンと同じように入れ替えられます。 順に、逆順に、飛ばしながらのように回り方が反復子ごとに違うので、使う側はどの反復子を受け取っても同じように使います。
プレイグラウンド
結果
コードを書いて「実行」ボタンを押してください。