読者です 読者をやめる 読者になる 読者になる

Murayama blog.

プログラミングと、その次の話

Iterator

Rubyによるデザインパターン生活、何日目とかもういいや。
本日はIteratorパターンを取り上げます。


Rubyによるデザインパターン

Rubyによるデザインパターン


Iteratorパターンは、
「集約オブジェクトがもとにある内部表現を公開せずに、その要素に順にアクセスする方法を提供する」とGoFは定義しています。
Javaでよく出てくるIteratorインタフェースがまさにそれです。
Iteratorインタフェースを利用することでListオブジェクトなど内部を意識せず、順番に要素へアクセスすることができます。


RubyにおけるIteratorパターンでは、外部イテレータと内部イテレータが存在します。
外部イテレータってのは、JavaIteratorインタフェースのようなクラスを自分で作ってしまう方法で、
内部イテレータってのは、Rubyでよく見るeach{|element| #do something}のようなメソッドを意味します。
*1


で、まずは外部イテレータを試してみます。
配列オブジェクト用のイテレータ(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"]
it = ArrayIterator.new(array)
while it.has_next? 
  puts "item: #{it.next_item}"
end


そんで実行結果。

item: red
item: green
item: blue


ちなみに名前はArrayIteratorだけど、配列オブジェクトにしか使えないわけではなくて、文字列オブジェクトの操作にも使えます。

it = ArrayIterator.new("test")
while it.has_next?
  puts "item:#{it.next_item.chr}"
end


文字列操作の場合は文字コードが返されるので、chrメソッドで文字データに変換しています。
実行結果は以下のとおり。

item:t
item:e
item:s
item:t


続いて、内部イテレータの話です。
Arrayオブジェクトに定義されているeachメソッドのようなものを独自で実装すると以下のようになります。

def for_each_element(array)
  i = 0
  while i < array.length
    yield(array[i])
    i += 1
  end
end

引数で受け取ったArrayオブジェクトに対して、yieldメソッドでコードブロックを順に実行するかんじです。


呼び出し側のプログラムは以下のようになります。

a = [10, 20, 30]
for_each_element(a){ |element| puts element }


実行結果です。

10
20
30


外部イテレータより、内部イテレータの方がRubyっぽいというか便利なように思います。
ただし、2つのArrayオブジェクトをマージするような場合は、
外部イテレータを使う方が楽なようです。

def merge(array1, array2)
  merged = []
  
  it1 = ArrayIterator.new(array1)
  it2 = ArrayIterator.new(array2)
  
  while it1.has_next? && it2.has_next?
    if it1.item < it2.item
      merged << it1.next_item
    else
      merged << it2.next_item
    end
  end
  
  while it1.has_next?
      merged << it1.next_item
  end

  while it2.has_next?
      merged << it2.next_item
  end
  merged
end

a = [10, 20, 30]
b = [11, 12, 21, 22]
merge(a, b).each { |e|  
  puts e
}


マージの実行結果です。

10
11
12
20
21
22
30


Iteratorパターンのお話はこんなところなんだけど、
この章では、Enumerableモジュールの使い方を紹介しています。


Enumerableモジュールをインクルードすると、集約オブジェクトに対して便利なメソッドを取り込むことができます。
たとえば、all?とかany?とかinclude?とか。

Enumerableモジュールをインクルードするには、以下の点を考慮します。

  • 内部イテレータメソッドにeachという名前をつけること。
  • <=>比較演算子の適切な実装をもつこと。
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

account1 = Account.new("account1", 1000)
account2 = Account.new("account2", 5000)
account3 = Account.new("account3", 3000)
account4 = Account.new("account4", 4000)
portfolio = Portfolio.new
portfolio.add_account(account1)
portfolio.add_account(account2)
portfolio.add_account(account3)
portfolio.add_account(account4)

puts portfolio.any? { |account| account.balance > 2000 }
puts portfolio.all? { |account| account.balance > 2000 }


実行すると、trueが表示されます。

true
false


あと、この章の最後のところで、ObjectSpaceモジュールも紹介されています。

ObjectSpaceは、Rubyインタープリタの中に存在する完全なオブジェクト空間へのアクセスを提供します。

初めて知ったけど、これけっこう面白い。どこで使えばよいのかはわからないけど。
さっきのプログラムのつづきに以下のコードを追加すると、

ObjectSpace.each_object(Account){ |object| puts object.name }


こんなふうにメモリ上に存在するインスタンスを取得することができます。

account4
account3
account2
account1

以上。

*1:こういう話は言われてみたら、そうだなーって思うけど、言われなかったら意識しないままだと思う。