dorivenの日記

気がついたら社会人。気になる技術的なことについて少しずつ書いていけたらと思っております。

【デザインパターン】イテレータパターン

どうも、明日合宿だというのに木曜日に風邪引いて色々とタイミング悪いなと思ってます。
明日の合宿どうなるのか楽しみ半分、不安半分といった所でしょうか。
さて、今日は【Iterator】パターンについてです。

参考書



さらっと読める「OREILLY Head First デザインパターン」を参考にしながら書いています。
しばらくはこいつを参考にデザインパターンについて書いていくよ!

一覧



この記事は以下の様な流れで記述される。

  1. Iteratorパターンって何ですか?
  2. Iteratorパターンのメリット
  3. 具体的な例

Iteratorパターンって何ですか?



コレクション(配列、ArrayList、Hash、Map、etc)による反復方法の違いを吸収し、同様にイテレーションが行える。

Wikipediaさんも参照。

コンテナオブジェクトの要素を列挙する手段を独立させることによって、コンテナの内部仕様に依存しない反復子を提供することを目的とする。
Iteratorパターンクラス図

別にIteratorという項目もあり、そちらも掲載しておきます。
イテレータ - Wikipedia

イテレータ(英語: Iterator)とは、プログラミング言語において配列やそれに類似するデータ構造の各要素に対する繰返し処理の抽象化である。実際のプログラミング言語では、オブジェクトまたは文法などとして現れる。反復するためのものの意味で反復子(はんぷくし)と訳される。繰返子(くりかえし)という抄訳もある。

Iteratorパターンのメリット


  1. 内部実装に依らない一括した反復処理を行えるようになる
  2. クラスの中で反復方法の違いによる処理の追記をしなくてよいので、コードが簡潔になり、依存性も下げられる

具体的な例



教科書の例では、配列とArrayListによる実装の違いを吸収するために用います。

オブジェクト町食堂とオブジェクト町パンケーキハウスが合併する事になりました。
しかし、食堂では「配列」、パンケーキハウスでは「ArrayList」を使用してメニューを管理していました。

この合併で問題になるのは、配列では`length`、ArrayListでは`size()`を使って全てのメニューを調べる必要があります。
ここでは2つの店が合併した後、ウェイトレスがメニューを全て表示する `printMenue()` 関数を作成することにします。

現在のままでは`printMenue()`の関数を作成するとどうなるか見てみましょう。

void printMenue()
{
    // 朝食はパンケーキ店が担当
    PancakeHouseMenu pancakeHouseMenu = new PancakeHouseMenu();
    ArrayList breakfastItems = pancakeHouseMenu.getMenuItems();
    
    // 昼食は食堂が担当
    DinerMenu dinerMenu = new DinerMenu();
    MenuItem[] lunchItems = dinerMenu.getMenuItems();

    for(int i = 0; i < breakfastItems.size(); ++i)
    {
        // 朝食のメニュー情報の表示
    }

    for(int i = 0; i < lunchItems.length; ++i)
    {
        // ランチのメニュー情報の表示
    }
}

さて、ここで現在の処理はまだ2つだけで済みますが、他の店とも合併する場合はどんどんループが増えていくのが予想できます。

ここでIteratorパターンです。
Iteratorパターンを使用することで反復処理を統一して行うことが出来るようになります。

public interface Iterator
{
    boolean hasNext();
    Object next();
}

public class DinerMenuIterator implements Iterator
{
    MenuItem[] items;
    int

    public DinerMenuIterator(MenuItem[] items)
    {
        this.items = items;
    }

    // 次の要素へ移動する
    public Object next()
    {
        MenuItem menuItem = items[position];
        position = position + 1;
        return menuItem;
    }

    // 次の要素が存在するか
    public boolean hasNext()
    {
        if(position >= items.length || items[position] == null)
        {
            return false;
        }
        else
        {
            return true;
        }
    }
}

public class PancakeIterator implements Iterator
{
     // DinerIteratorと同様に実装を行う
}

さて、これでIteratorの準備が整いました。
どのように最初のコードが改善されるかを覗いてみましょう。

void printMenu()
{
    PancakeHouseMenu pancakeHouseMenu = new PancakeHouseMenu();
    DinerMenu dinerMenu = new DinerMenu();

    // getMenuItemsの代わりにcreateIterator()という関数でコレクションでなくIteratorを返すように変更
    Iterator pancakeIterator = pancakeHouseMenu.createIterator();
    Iterator dinerIterator = dinerMenu.createIterator();
    printMenu(pancakeIterator);
    printMenu(dinerIterator);
}

void printMenu(Iterator iterator)
{
    while(iterator.hasNext())
    {
        // メニューを表示する処理
    }
}

どうでしょうか?
先ほど、2つに分かれていた処理が1つに統合されました。
PancakeHouseMenuとDinerMenuではgetMenuItems()という関数でなくcreateIterator()という処理に変えただけで、他には手を加えていません*1

これにより、どれだけ異なるコレクションを使ってもループが別れることがありません。
これがIteratorパターンです。

最後に



最後まで呼んで頂きありがとうございました。
実はご存知の方も居るかと思いますが*2Iteratorパターンは大体の言語で実装がされています。
Wikiの2つ目の紹介でも載せていますが、かなり身近なパターンであることを理解できたと思います。
実装がされているので、Iteratorに対応していない純粋な配列以外ではあまり役に立つ機会が少ないかもしれませんが、Iteratorがどのようなパターンか、ということを知れたのはいい機会だったと思います。

*1:今回はその処理の内容の記述を省いたのでありがたみが分からないかもしれませんが

*2:そもそもWikiのリンクで知ってるかな