Iterator
デザインパターン

デザインパターン勉強会 第一回:Iteratorパターン

はじめに

本エントリーは某社内で実施するデザインパターン勉強会向けの資料となります。
本エントリーで書籍「Java言語で学ぶデザインパターン入門」をベースに学習を進めますが、サンプルコードはC#に置き換えて解説します。
なお第二回以降はこちら

第二回:Adapterパターン
第三回:Template Methodパターン
第四回:Factory Methodパターン
第五回:Singletonパターン
第六回:Prototypeパターン
第七回:Builderパターン
第八回:Abstract Factoryパターン
第九回:Bridgeパターン
第十回:Strategyパターン
第十一回:Compositeパターン
第十二回:Decoratorパターン
第十三回:Visitorパターン
第十四回:Chain of Responsibilityパターン
第十五回:Facadeパターン
第十六回:Mediatorパターン
第十七回:Observerパターン
第十八回:Mementoパターン
第十九回:Stateパターン
第二十回:Flyweightパターン
第二十一回:Proxyパターン
第二十二回:Commandパターン
第二十三回:Interpreterパターン


Iteratorパターンとは

Iteratorパターンとは、多数の集合に対してその内部の実装を意識することなく、逐次処理するためのデザインパターンです。
ここでは、多数の書籍が保管された書架を想像してください。
利用者は書架から蔵書を逐次取り出して何らかの処理を行うものと想定しましょう。


サンプルクラス図

先の想定実装したモデルがつぎの通りです。

Original.png


各クラスの役割

名前 役割
IAggregate 集合体を表すインターフェース
IIterator 逐次処理を行うインターフェース
Book 本を表すクラス
BookShelf 本棚を表すクラス
BookShelfIterator 本棚内の本を逐次処理するクラス
Program 動作確認用クラス

IAggregate

IAggregateではIIteratorを返すGetIteratorメソッドのみが存在します。

public interface IAggregate
{
    IIterator GetIterator();
}

IIterator

逐次処理を行う為のインターフェースです。

public interface IIterator
{
    bool HasNext { get; }
    object Next();
}

まだ未処理の要素が存在するかどうか取得するHasNextプロパティと(書籍ではメソッドですがC#ではプロパティのほうが適切でしょう)、値を取得するNextメソッドがあります。
Nextメソッドには注意が必要で、このメソッドでは内部的に二つの処理を実装する必要があります。

  1. 現在の値を取得し返却する
  2. 逐次処理する「現在の値」を一つ進める

実装コード側を見た方が分かりやすいでしょう。


Book

書籍を表すクラスで、書籍名のみをプロパティとして所持します。

public class Book
{
    public string Name { get; set; }
}

書籍名はコンストラクタで設定した方が本来は好ましいかもしれません。


BookShelf

書架を表すクラスです。IAggregateを実装しています。
次のメンバーが実装されています。

  • Bookを保持するListフィールド
  • 保持している書籍数を返すLengthプロパティ
  • 書籍を追加するAppendBookメソッド
  • 指定されたインデックスの書籍を取得するGetBookAtメソッド
  • 書籍を逐次処理するIIteratorを取得するGetIteratorメソッド(IAggregateインターフェースで定義されているメソッドの実装)
public class BookShelf : IAggregate
{
    private readonly List<Book> _books = new List<Book>();

    public int Length => _books.Count;

    public void AppendBook(Book book)
    {
        _books.Add(book);
    }

    public Book GetBookAt(int index)
    {
        return _books[index];
    }

    public IIterator GetIterator()
    {
        return new BookShelfIterator(this);
    }
}

BookShelfIterator

BookShelfが保持する書籍を逐次処理するためのIIteratorの実装クラスです。

ポイントは二つあります。

  1. 内部にBookShelfをフィールドとして保持しており利用している
  2. Nextメソッドでは現在値の取得と、インデックスを進める二つの処理が実装されている
public class BookShelfIterator : IIterator
{
    private readonly BookShelf _bookShelf;

    private int index = 0;

    public BookShelfIterator(BookShelf bookShelf)
    {
        _bookShelf = bookShelf;
    }

    public bool HasNext
    {
        get { return index < _bookShelf.Length; }
    }

    public object Next()
    {
        var book = _bookShelf.GetBookAt(index);
        index++;
        return book;
    }
}

Program

つぎのコードがこれらを利用するイメージです。

BookShelfに4冊のBookを追加し、BookShelfからイテレーターを取得して全ての本の名称をコンソールへ出力しています。

class Program
{
    static void Main(string[] args)
    {
        var bookShelf = new BookShelf();
        bookShelf.AppendBook(new Book { Name = "Around the World in 80 Days" });
        bookShelf.AppendBook(new Book { Name = "Bible" });
        bookShelf.AppendBook(new Book { Name = "Cinderella" });
        bookShelf.AppendBook(new Book { Name = "Daddy-Long-Legs" });
        var iterator = bookShelf.GetIterator();
        while (iterator.HasNext)
        {
            var book = (Book)iterator.Next();
            Console.WriteLine(book.Name);
        }
        Console.ReadLine();
    }
}

Program実行結果

先のProgramを実行した結果が以下の通りです。
追加された書籍名が全て表示されていますね?

Around the World in 80 Days
Bible
Cinderella
Daddy-Long-Legs

Iteratorパターンのメリット

Iteratorパターンの最大のメリットは、Iteratorの利用者側が、その実装に依存せず(どう実装されているのか知る必要なく)逐次処理を実現できる点にあります。
サンプルの例では、BookShelfのメンバ変数にBookを保持していましたが、例えばBookを取得してくる箇所がファイルシステムであったりデータベースであったりしても、BookShelfの実装を修正するだけで、その利用者(Program)を変更しないでよいというのがメリットです。
実際に、そう言った変更が発生する事は稀でしょうが、内部を理解していなくてもIteratorとして利用できるとだけ知っていればよいというのは、大きな利点です。


ところで.NET Frameworkでは

foreachなどで利用するため、ベースクラスライブラリにIteratorパターンの仕組みが組み込まれています。以下がその対応表になります。

Iteratorパターン C#
IAggregate IEnumerable
IIterator IEnumerator

IEnumerableとIEnumeratorは.NET Framework側で提供されます。
このため変化があるのはBookShelfとBookShelfIteratorになります。
違いを見てみましょう。


BookShelf

大きな変更点が4つあります。

  1. 実装インターフェースをIAggregateからIEnumerableに変更
  2. イテレーター(IEnumerator)取得メソッドをGenerics非対応とGenerics対応の2種類を実装
public class BookShelf : IEnumerable<Book>
{
    private readonly List<Book> _books = new List<Book>();

    public int Length => _books.Count;

    public void AppendBook(Book book)
    {
        _books.Add(book);
    }

    public Book GetBookAt(int index)
    {
        return _books[index];
    }

    public IEnumerator<Book> GetEnumerator()
    {
        return new BookShelfEnumerator(this);
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

BookShelfEnumerator

次の変更が加えられています。

  1. 実装インターフェースをIIteratorからIEnumeratorに変更し、あわせてクラス名を変更
  2. 現在値の取得と現在値の移動の二つの処理を、Nextメソッドひとつで実施していたがCurrentプロパティとMoveNextメソッドの二つに役割を分離
  3. Reset及びDisposeメソッドの追加

やや実装の手間は増えていますが、利用者側の利便性は高くなっていますので確認してみましょう。

public class BookShelfEnumerator : IEnumerator<Book>
{
    private readonly BookShelf _bookShelf;

    private int index = 0;
    public Book Current => _bookShelf.GetBookAt(index);

    object IEnumerator.Current => Current;

    public BookShelfEnumerator(BookShelf bookShelf)
    {
        _bookShelf = bookShelf;
    }

    public bool MoveNext()
    {
        index++;
        return index < _bookShelf.Length;
    }

    public void Reset()
    {
        index = 0;
    }
    public void Dispose()
    {
    }
}

Program

次の二つの点が改善されていることが見て取れます。

  1. foreach文を利用することで簡潔に記述可能
  2. 取得する型がBook型で取得されるため、型安全性が保障される(ダウンキャストが発生しないため、安全かつ高速)
class Program
{
    static void Main(string[] args)
    {
        var bookShelf = new BookShelf();
        bookShelf.AppendBook(new Book { Name = "Around the World in 80 Days" });
        bookShelf.AppendBook(new Book { Name = "Bible" });
        bookShelf.AppendBook(new Book { Name = "Cinderella" });
        bookShelf.AppendBook(new Book { Name = "Daddy-Long-Legs" });
        foreach (var book in bookShelf)
        {
            Console.WriteLine(book.Name);
        }
        Console.ReadLine();
    }
}

イテレーター ブロック

さて、ここまでの例ではIEnumerableとIEnumeratorを自力で実装してきましたが、実はC#にはIEnumerableを簡単に実装するための手段として、イテレーター ブロックが用意されています。

BookShelfにイテレーター ブロックを組み込んで、比較してみましょう。

次のコードは変更前のBookShelfのコードです。

public class BookShelf : IEnumerable<Book>
{
    private readonly List<Book> _books = new List<Book>();

    public int Length => _books.Count;

    public void AppendBook(Book book)
    {
        _books.Add(book);
    }

    public Book GetBookAt(int index)
    {
        return _books[index];
    }

    public IEnumerator<Book> GetEnumerator()
    {
        return new BookShelfEnumerator(this);
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

イテレーターブロックを利用したBookShelf

非常に簡潔に記述できますね。
これは一例です。もう一つのパターンを次のスライドで見てみましょう。

public class BookShelf : IEnumerable<Book>
{
    private readonly List<Book> _books = new List<Book>();

    public void AppendBook(Book book)
    {
        _books.Add(book);
    }

    public IEnumerator<Book> GetEnumerator()
    {
        foreach (var book in _books)
        {
            yield return book;
        }
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

イテレーターブロックを利用したBookShelf その2

public class BookShelf
{
    private readonly List<Book> _books = new List<Book>();

    public IEnumerable<Book> GetEnumerable()
    {
        foreach (var book in _books)
        {
            yield return book;
        }
    }

    public void AppendBook(Book book)
    {
        _books.Add(book);
    }
}

こちらの場合、利用箇所も次のように変更する必要があります。

// foreach (var book in bookShelf) 変更前
foreach (var book in bookShelf.GetEnumerable())

yieldキーワード

ポイントはyield(譲るの意味)です。
これを利用することで、「イテレーターブロック」を生成する事ができ、イテレーターブロックの戻り値につぎの何れかを指定することで、コンパイラがその実装を自動生成してくれます。

  • System.Collections.IEnumerator
  • System.Collections.Generic.IEnumerator
  • System.Collections.IEnumerable
  • System.Collections.Generic.IEnumerable

極端な話、次のように記述する事も出来ます。

public IEnumerable<Book> Enumerable
{
    get
    {
        yield return new Book { Name = "Around the World in 80 Days" };
        yield return new Book { Name = "Bible" };
        yield return new Book { Name = "Cinderella" };
        yield return new Book { Name = "Daddy-Long-Legs" };
    }
}

詳細はつぎも参照する事をお勧めします。

イテレーター - C# によるプログラミング入門 | ++C++; // 未確認飛行 C


Iteratorパターンまとめ

  • Iteratorパターンとは集合を逐次処理するためのデザインパターン
  • Iteratorパターンの最も大切な点は、Iteratorの利用者側がIteratorの実装に依存せず逐次処理できること
  • 現在はJava・C#いずれもフレームワークの支援が存在する
  • IteratorパターンにおけるAggregate・IteratorはC#ではそれぞれIEnumeratable・IEnumeratorが該当する
  • IEnumeratable・IEnumeratorには非Generic版とGeneric版が存在する
  • イテレーターブロックを利用することでIEnumeratable・IEnumeratorを非常に容易に実装する事ができる
  • その際、yieldキーワードを利用する

サンプルコード

以下に公開しています。

https://github.com/nuitsjp/IteratorPattern

また第二回以降は以下にあります。

第二回:Adapterパターン
第三回:Template Methodパターン
第四回:Factory Methodパターン
第五回:Singletonパターン
第六回:Prototypeパターン
第七回:Builderパターン
第八回:Abstract Factoryパターン
第九回:Bridgeパターン
第十回:Strategyパターン
第十一回:Compositeパターン
第十二回:Decoratorパターン
第十三回:Visitorパターン
第十四回:Chain of Responsibilityパターン
第十五回:Facadeパターン
第十六回:Mediatorパターン
第十七回:Observerパターン
第十八回:Mementoパターン
第十九回:Stateパターン
第二十回:Flyweightパターン
第二十一回:Proxyパターン
第二十二回:Commandパターン
第二十三回:Interpreterパターン