イーテレータ | 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

😸 サンプルソース

🤔 参考リンク

🖥 VULTRおすすめ

VULTR」はVPSサーバのサービスです。日本にリージョンがあり、最安は512MBで2.5ドル/月($0.004/時間)で借りることができます。4GBメモリでも月20ドルです。 最近はVULTRのヘビーユーザーになので、「ここ」から会員登録してもらえるとサービス開発が捗ります!

📚 おすすめの書籍