Design Pattern: Ruby Companion

4 Iterator パターン

4.1 Iteratorパターンの意図

(ずばっと略)

4.2 Iteratorパターンの実装

Ruby版では,全てのクラス定義とサンプルを1つのソースにまとめています.

4.2.1 サンプルその1: 普通の実装

素朴にRubyで実装したものです.メソッドなどはRubyっぽく変えたところもあります.

# Iterator Pattern
# sample1

class Book
  def initialize(name)
    @name = name
  end

  attr_reader :name
end

class BookShelf
  def initialize()
    @books = Array.new()
  end
  def [](index)
    return @books[index]
  end
  def append_book(book)
    @books << book
  end
  def length
    return @books.length
  end
  def iterator
    return BookShelfIterator.new(self)
  end
end

class BookShelfIterator
  def initialize(bookshelf)
    @bookShelf = bookshelf
    @index = 0
  end

  def has_next?()
    @index < @bookShelf.length
  end

  def next()
    book = @bookShelf[@index]
    @index += 1
    return book
  end
end

### main

if __FILE__ == $0
  bookShelf = BookShelf.new()
  bookShelf.append_book(Book.new("Around the World in 80 Days"))
  bookShelf.append_book(Book.new("Bible"))
  bookShelf.append_book(Book.new("Cinderella"))
  bookShelf.append_book(Book.new("Daddy-Long-Legs"))

  it = bookShelf.iterator()

  while it.has_next?
    book = it.next()
    print book.name, "\n"
  end
end

4.2.2 サンプルその2: 内部イテレータ

「サンプルその1」は,オブジェクトを格納するコンテナと,コンテナをスキャンするイテレータとが別々のオブジェクトになっていて,イテレータを前に進ませるにはその都度 next メソッドを実行させる必要がありました.

ところで,Rubyで「イテレータ」というと,ふつうはサンプルその1のようなものではなく,コンテナとイテレータとが同じオブジェクトになっているものを指します.そして,イテレータを進ませるためのメソッドは必要ありません.イテレータに勝手に進ませるのです.このようなイテレータを,「内部イテレータ」と言います.これに対して,サンプル1のようなイテレータを「外部イテレータ」とも言います.

Rubyの内部イテレータを使って実装すると,以下のようになります.イテレータはeachメソッドによって実装されています.

# Iterator Pattern
# sample2: using Enumerable

class Book
  def initialize(name)
    @name = name
  end

  attr_reader :name
end

class BookShelf
  include Enumerable

  def initialize()
    @books = Array.new()
  end

  def [](index)
    @books[index]
  end

  def <<(book)
    @books << book
  end

  def length()
    @books.length
  end

  def each()
    @books.each{|book|
      yield(book)
    }
  end
end

## main

if __FILE__ == $0
  bookShelf = BookShelf.new()
  bookShelf << Book.new("Around the World in 80 Days")
  bookShelf << Book.new("Bible")
  bookShelf << Book.new("Cinderella")
  bookShelf << Book.new("Daddy-Long-Legs")

  bookShelf.each{|book|
    print book.name, "\n"
  }

  bookstr =  bookShelf.collect{|book|
    '"'+book.name+'"'
  }.join(":")
  print bookstr, "\n"
end

4.2.3 サンプルその3: StructとArrayの継承

サンプルその2のBookShelfクラスの定義の通り,BookShelfクラスは,結局のところほとんどArrayクラスと同じ機能を持つクラスになります.そのため,いっそのことArrayをそのまま使っても構わないのですが,そうすると今度はBookShelf独自のメソッドを定義できなくなります.そこで,Arrayを継承させたクラスにしてみましょう.

また,単なる属性を持ったオブジェクトのクラスがほしい場合には, Structクラスを使います.これは,単なる属性と,その属性へのアクセスメソッドのみのクラスを作るためのクラスです.Struct.newの結果を定数に代入するのがミソで,その定数が新しいクラスになります.

# Iterator Pattern
# sample3: using Struct and inheretance of Array class

## Book class
Book = Struct.new("Book", :name)

## BookShelf class
class BookShelf < Array; end

## main
if __FILE__ == $0
  bookShelf = BookShelf.new()
  bookShelf << Book.new("Around the World in 80 Days")
  bookShelf << Book.new("Bible")
  bookShelf << Book.new("Cinderella")
  bookShelf << Book.new("Daddy-Long-Legs")

  bookShelf.each{|book|
    print book.name, "\n"
  }

  bookstr =  bookShelf.collect{|book|
    '"'+book.name+'"'
  }.join(":")
  print bookstr, "\n"
end

4.2.4 サンプルその4: Arrayへの委譲(delegate)

継承の代わりに委譲を使ってみましょう.委譲を使えば,クラスを継承させることなく,他のクラスの機能を利用できます.

# Iterator Pattern
# sample4: using delegate 

require 'delegate'

## Book class
Book = Struct.new("Book", :name)

## BookShelf class
class BookShelf < DelegateClass(Array)
  def initialize()
    super([])
  end
end


## main
if __FILE__ == $0
  bookShelf = BookShelf.new()
  bookShelf << Book.new("Around the World in 80 Days")
  bookShelf << Book.new("Bible")
  bookShelf << Book.new("Cinderella")
  bookShelf << Book.new("Daddy-Long-Legs")

  bookShelf.each{|book|
    print book.name, "\n"
  }

  bookstr =  bookShelf.collect{|book|
    '"'+book.name+'"'
  }.join(":")
  print bookstr, "\n"
end

4.2.5 サンプルその5: Arrayへの委譲その2・(forwarding)

Deletagorを使った場合,元のクラスの全てのメソッドについて委譲されてしまうようになります.しかし,そうではなく,限られたメソッドについてのみ委譲したいことも多々あります.

そのような場合には,forwardableライブラリを使います.こちらは,委譲したいメソッドを指定しておけば,そのメソッドのみ委譲されるようになります.

# Iterator Pattern
# sample5: using forwarding

require 'forwardable'

## Book class
Book = Struct.new("Book", :name)

## BookShelf class
class BookShelf
  extend Forwardable
  def_delegators("@books", "<<", "[]", "length", "each")

  include Enumerable

  def initialize()
    @books = Array.new()
  end
end


## main
if __FILE__ == $0
  bookShelf = BookShelf.new()
  bookShelf << Book.new("Around the World in 80 Days")
  bookShelf << Book.new("Bible")
  bookShelf << Book.new("Cinderella")
  bookShelf << Book.new("Daddy-Long-Legs")

  bookShelf.each{|book|
    print book.name, "\n"
  }

  bookstr =  bookShelf.collect{|book|
    '"'+book.name+'"'
  }.join(":")
  print bookstr, "\n"
end

4.3 おまけ

使い方が限定されている分には,外部イテレータよりも内部イテレータの方が便利でしょう.ただし,複雑な使い方をする場合には,内部イテレータで頑張るよりは外部イテレータを使い,自分で制御した方がシンプルになりそうです.

4.4 ソース

固めておきました.こちらからどうぞ.

<URL:src-iterator.tar.gz>

4.4.1 ソースのライセンス

このソースは,結城浩さんによる『Java言語で学ぶデザインパターン入門』を元に,たかはしが Ruby用に手を入れたものです.Rubyとして自然なソースにするようにしたため,あんまり原型を留めてません.

オリジナルのソースのライセンスは, <URL:http://www.hyuki.com/dp/index.html#download> にあります.このソースの扱いも上記と同様でお願いします.

文責: たかはし(maki@rubycolor.org)