Edited at

Rubyによるデザインパターン【Iterator】-君の子供たちに伝えたいのだけど-

More than 5 years have passed since last update.


概要

Rubyによるデザインパターン第7章。

Iterator Pattern。

Rubyによるデザインパターン5原則に則って理解する。


どんなパターンか

集約オブジェクト(コンポジット)の全ての子オブジェクトを連続で走査し、また任意のロジックを適用する。

イテレータ用の外部クラスを用いて実現する外部イテレータと、

(例えばeachのように)オブジェクト自身がブロックを受け取り、子オブジェクトに適用する内部イテレータが存在する。


内部イテレータ

コードブロックの利用によって集約オブジェクトにロジックを伝える

(子オブジェクトたちそれぞれに対してコードブロックを呼ぶ)。

繰り返し処理の全てが集約オブジェクト内で起こるため、内部イテレータと呼ぶ。


配列を内部イテレータで走査

Arrayにはもともとイテレータメソッドeachが存在する。

array = ['red', 'green', 'blue']

array.each do |val|
p val
end
"red"
"green"
"blue"

手軽。


外部イテレータ

イテレータ用の外部クラスArrayIteratorを作成する。

class ArrayIterator

def initialize(array)
@array = array
@index = 0
end

def has_next?
@index < @array.length
end

def item
@array[@index]
end

def next_item
value = @array[@index]
@index += 1
value
end
end


配列を外部イテレータで走査

array = ['red', 'green', 'blue']

i = ArrayIterator.new(array)

while i.has_next?
puts("item: #{i.next_item}")
end
=>item: red
item: green
item: blue


文字列を外部イテレータで走査


  • lengthメソッドを持っていること

  • 整数でインデックスを付与できること

これらを満たした集約クラスなら、どんなものに対しても機能する。

→つまり、文字列にも適応可。

i = ArrayIterator.new('abc')

while i.has_next?
puts("item: #{i.next_item}")
end
=>item: a
item: b
item: c


内部イテレータ vs 外部イテレータ


内部イテレータ

外部イテレータでは、余分にイテレータオブジェクト(例ではArrayIterator)が必要であるのに対して

シンプル、分かりやすい。


外部イテレータ

外部機能として渡すため、複数コレクションから一つずつ要素を取り出して並行に処理するようなことも可能。

例として、配列を結合するmerge機能を追加してみる。

array1 = [10, 50, 100]

array2 = [20, 30, 150]

def merge(array1, array2)
merged = []

iterator1 = ArrayIterator.new(array1)
iterator2 = ArrayIterator.new(array2)

while(iterator1.has_next? and iterator2.has_next?)
if iterator1.item < iterator2.item
merged << iterator1.next_item
else
merged << iterator2.next_item
end
end

# array1から残りを取り出す
while(iterator1.has_next?)
merged << iterator1.next_item
end

# array2から残りを取り出す
while(iterator2.has_next?)
merged << iterator2.next_item
end

merged
end

merge(array1, array2)
=> [10, 20, 30, 50, 100, 150]

内部イテレータだとこのような並行処理は難しい。


Enumerable

集約クラスに対して内部イテレータを持たせた場合、そもそもEnumerableモジュールのMix-inを検討すべき。

module Enumerable

http://docs.ruby-lang.org/ja/2.0.0/class/Enumerable.html

複数のAccountを保持する集約クラスPortfolioにEnumerableをMix-inする例。

class Account

attr_accessor :name, :balance

def initialize(name, balance)
@name = name
@balance = balance
end
end

class Portfolio
include Enumerable

def initialize
@accounts = []
end

# Enumerable moduleをincludeしたクラスはeachメソッドを持っている必要がある
def each(&block)
@accounts.each(&block)
end

def add_account(account)
@accounts << account
end
end

my_portfolio = Portfolio.new
my_portfolio.add_account(Account.new('test', 1000))
my_portfolio.any? {|account| account.balance > 100 }
=> true


Iteratorパターン使用上の注意

集約オブジェクトを繰り返し走査しているタイミングで該当オブジェクトが変更されたらどうなるか?

→ ここまでのイテレータでは、不具合が発生する可能性がある。


内部イテレータ

走査済みの値が削除されると、インデックスがずれてしまう。

array = ['red', 'green', 'blue']

array.each do |val|
p val
if val=='red'
array.delete(val)
end
end
=>"red" # インデックスがずれてgreenが無視されてしまう
"blue"


対策

delete_ifを使って解決。

array = ['red', 'green', 'blue']

array.delete_if do |val|
p val
val=='red'
end
=>"green" # redを正常に削除
"blue"

deleteした後もインデックスを正しく保ってくれる。


外部イテレータ

以下のように集約のひとつひとつを走査するごとにそれに合わせてインデックスを追加しているため、

  def next_item

value = @array[@index]
@index += 1
value
end

もし走査済みの値が削除されるとインデックスがずれてしまう。


対策

イテレータのコンストラクタで配列のコピーを作成、値の削除に影響を受けないようにする。

class ChangeResistantArrayIterator

def initialize(array)
@array = Array.new(array)
@index = 0
end
end

※ この手法は単純に「イテレータで走査し始めたタイミングのコレクションに対するアウトプットを約束する」ものであり、

「走査中にコレクションに変更が加わった場合にも適応してくれる」delete_ifとは異なる。


Iteratorの実例

[Ruby] 便利な組み込みクラスのメソッド達(Enumerable編)

http://qiita.com/kidachi_/items/a00558cfb0a6a3e23f4b

[Ruby] 便利な組み込みクラスのメソッド達(Hash編)

http://qiita.com/kidachi_/items/651b5b5580be40ad047e


変態系


ObjectSpace * each_object

RubyインタプリタにロードされているすべてのRubyオブジェクトへアクセスできる。

# 実行注意

> ObjectSpace.each_object {|object| puts("Object: #{object}")}
Object: SyntaxError
Object: SyntaxError
Object: ScriptError
Object: ScriptError
Object: RangeError
Object: RangeError
Object: KeyError
Object: KeyError
Object: IndexError
Object: IndexError
Object: ArgumentError
Object: ArgumentError
Object: TypeError
Object: TypeError
Object: StandardError
Object: StandardError
Object: Interrupt
Object: Interrupt
Object: SignalException
Object: SignalException
Object: fatal
Object: fatal
Object: success?
Object: status
Object: SystemExit
・・・

Railsではこれを利用して、ObjectSpaceを使用して与えられたクラスの

サブクラスをすべて探し出すメソッドを用意している。

def subclasses_of(superclass)

subclasses = []
ObjectSpace.each_object(Class) do |k|
next if !k.ancestors.include?(superclass) ||
superclass ==k ||
k.to_s.include?('::') ||
subclasses.include?(k.to_s)
subclasses << k.to_s
end
subclasses
end

subclasses_of(Numeric)
=> ["Complex", "Rational", "Bignum", "Float", "Fixnum", "Integer"]

全オブジェクトを走査、該当するものだけ抜き出すとはなかなか。


まとめ


  • 集約コレクションの子オブジェクトにアクセスすることが可能。

  • 並行処理の必要がなければ基本は内部イテレータが手軽(Rubyの場合)。


以下に続く(予定)

【Command】パターン

WIP.