LoginSignup
45
42

More than 5 years have passed since last update.

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

Last updated at Posted at 2014-06-15

概要

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.

45
42
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
45
42