イーテレータ | Ruby デザインパターン


GoFのデザインパターンのイーテレータ(Iterator)のRubyコードを使った紹介記事です。イーテレーターパターンは次のような場合に使います。

  • 要素の集まったオブジェクト(配列など)にアクセスする
  • 集合の要素に順にアクセスする必要がある

🐠 イーテレータとは?

GoFではイーテレータを「集約オブジェクトがもとにある内部表現を公開せずに、その要素に順にアクセスする方法を提供する」と定義しています。

言い換えると、要素の集まりをもつオブジェクトの各要素に、順番にアクセスする方法を提供するためのデザインパターンです。

🚌 内部イーテレータ

Rubyのeachと同じもの。コードブロックベースのイーテレータのこと。

🗽 外部イーテレータ

ここでは次のようなモデルで外部イーテレータを説明します。

  • Blogクラス(集約オブジェクト): 複数のArticleクラスをもつオブジェクト
  • Articleクラス(集約オブジェクト内の要素): Blogクラスの個別の要素
  • BlogIteratorクラス(外部イーテレータ): Blogの要素Articleにアクセスするためクラス

まず、記事を表すArticleクラスです。

# 記事を表す(集約オブジェクト中の要素)
class Article
def initialize(title)
@title = title
end
# 記事のタイトル
attr_reader :title
end

次は、記事を複数もつブログを表すBlogクラスです。

# ブログを表す(集約オブジェクト)
class Blog
def initialize
@articles = []
end
# 指定インデックスの要素を返す
def get_article_at(index)
@articles[index]
end
# 要素(Article)を追加する
def add_article(article)
@articles << article
end
# 要素(Article)の数を返す
def length
@articles.length
end
# イーテレータの生成
def iterator
BlogIterator.new(self)
end
end

最後に外部イーテレータのBlogIteratorクラスです。

# 外部イーテレータ
class BlogIterator
def initialize(blog)
@blog = blog
@index = 0
end
# 次のindexの要素が存在するかをtrue/falseで返す
def has_next?
@index < @blog.length
end
# indexを1つ進めて、次のArticleクラスを返す
def next_article
article = self.has_next? ? @blog.get_article_at(@index) : nil
@index = @index + 1
article
end
end

ソースコードは以上です。では、実際の動作を確認してみます。

blog = Blog.new
blog.add_article(Article.new("デザインパターン1"))
blog.add_article(Article.new("デザインパターン2"))
blog.add_article(Article.new("デザインパターン3"))
blog.add_article(Article.new("デザインパターン4"))
iterator = blog.iterator
while iterator.has_next?
article = iterator.next_article
puts article.title
end
#デザインパターン1
#デザインパターン2
#デザインパターン3
#デザインパターン4

blog.iteratorで生成した外部イーテレータによって、Blogクラスの要素Articleに順番にアクセスできていることがわかります。

🚜 Enumerableモジュール

唐突ですが、Rubyの便利モジュールEnumerableの紹介です。Enumerableモジュールをインクルードすると、集約オブジェクト向けの「all?any?include?」といった便利なメソッドを取り込むことができます。

以下は取り込んだ場合のサンプルソースです。

class Account
attr_accessor :name, :balance
def initialize(name, balance)
@name = name
@balance = balance
end
def <=>(other)
@balance <=> other.balance
end
end
class Portfolio
include Enumerable
def initialize
@accounts = []
end
def each(&block)
@accounts.each(&block)
end
def add_account(account)
@accounts << account
end
end
portfolio = Portfolio.new
portfolio.add_account(Account.new("account1", 1000))
portfolio.add_account(Account.new("account2", 2000))
portfolio.add_account(Account.new("account3", 3000))
portfolio.add_account(Account.new("account4", 4000))
portfolio.add_account(Account.new("account5", 5000))
# $3000より多く所有している口座があるか?
puts portfolio.any? { |account| account.balance > 3000 }
#=> true
# すべての口座が$2000以上あるか?
puts portfolio.all? { |account| account.balance >= 2000 }
#=> false

🤔 サンプルソース

🐡 参考リンク

📚 おすすめの書籍