Design Pattern: Ruby Companion
(ずばっと略)
Ruby版では,全てのクラス定義とサンプルを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
「サンプルその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
サンプルその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
継承の代わりに委譲を使ってみましょう.委譲を使えば,クラスを継承させることなく,他のクラスの機能を利用できます.
# 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
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
使い方が限定されている分には,外部イテレータよりも内部イテレータの方が便利でしょう.ただし,複雑な使い方をする場合には,内部イテレータで頑張るよりは外部イテレータを使い,自分で制御した方がシンプルになりそうです.
固めておきました.こちらからどうぞ.
このソースは,結城浩さんによる『Java言語で学ぶデザインパターン入門』を元に,たかはしが Ruby用に手を入れたものです.Rubyとして自然なソースにするようにしたため,あんまり原型を留めてません.
オリジナルのソースのライセンスは, <URL:http://www.hyuki.com/dp/index.html#download> にあります.このソースの扱いも上記と同様でお願いします.
文責: たかはし(maki@rubycolor.org)