2012年7月23日月曜日

RubyのDelegatorを使ってみる話

例えば、Arrayクラスにhelloというメソッドを追加したい場合、次のようなやり方ができる。

class Array
  def hello
    puts "Hello world !!"
  end
end

しかし、もともとあるArrayクラスに手を加えたくない場合もある。いくつかやり方があるが、Arrayクラスを継承した別のクラスを定義するという方法がある。

class HelloArray < Array
  def hello
    puts "Hello world !!"
  end
end

しかし、HelloArrayクラスのオブジェクトにmapなどのメソッドを使うと、Arrayクラスに戻ってしまう。

obj1 = HelloArray.new([1, 2, 3])
obj2 = obj1.map{|n| n + 1}
obj2.class # => Array

そこで、obj1に何か仕事をさせて、戻り値がArrayであれば自動的にHelloArrayにするという処理を実現したいとする。これはdelegateという標準ライブラリを使って次のように実現することができる。

require 'delegate'
class HelloArray < SimpleDelegator
  def method_missing(name, *args, &block)
    obj = super(name, *args, &block)
    obj = HelloArray.new(obj) if obj.class == Array
    return obj
  end
end

obj1 = HelloArray.new([1, 2, 3])
obj2 = obj1.map{|n| n + 1}
obj2.class # => HelloArray

SimpleDelegatorはmethod_missingを利用してオブジェクトへの委譲を行う。そこで、SimpleDelegatorを継承したHelloArrayクラスを定義して、method_missingを書き換えることによって「ほとんど全てのメソッドを対象としたフック処理」を行うことができる。これにより、メソッドの戻り値がArrayかどうかを監視して、ArrayであればHelloArrayにするという処理を実現することができる。

これで、obj1によって生成されるArrayオブジェクトは全て自動的にHelloArrayオブジェクトになるようになったはずである。ところが、次のような処理はうまく行かない。

obj1 = HelloArray.new([[1,2,3],[4,5,6],[7,8,9]])
obj1.each{|item| item.hello}

itemには[1,2,3]や[4,5,6]や[7,8,9]が順番に代入されるが、これらはobj1のメソッドの結果として生成されるわけではないので、HelloArrayの監視対象ではない。そこでHelloArrayのeachメソッドを上書きして、これらの要素も監視するようにしよう。

class HelloArray
  def each
    super do |item|
      item = HelloArray.new(item) if item.class == Array
      yield item
    end
  end
end

これで、obj1.each{|item| ... }のitemも、自動的にArrayからHelloArrayに変換されるようになる。当然、eachメソッドを参照するEnumerable系のメソッド(mapとか)にも自動的に適用される・・・と思いきや、実はうまく行かない。

obj1.map{|item| item.hello} # => Error!

HelloArrayクラスにはmapは定義されていないので、method_missingを経由してArrayに委譲されることになる。ところが、Array#mapはあくまでArray#eachを参照するのであって、HelloArray#eachを再定義しても無視されるのではないかと思われる。ならばどうするのかというと、HelloArrayクラスにEnumerableをインクルードしてやる。

class HelloArray
  include Enumerable
end

これで、HelloArray#mapその他が定義され、これらはHelloArray#eachを参照するので、mapも期待通り動作するようになる。が、今度はEnumerableがインクルードされてHelloArray#mapが定義されたことにより、method_missing経由でArray#mapが呼び出されなくなるので、method_missingで行なっていたmapの結果をHelloArrayに変換する処理が行われなくなってしまう。これを解決するには、デリゲーターを二重構造にするという方法がある。

class HelloArray < SimpleDelegator
  def hello
    puts "Hello World !!"
  end
  def method_missing(name, *args, &block)
    obj = super(name, *args, &block)
    obj = HelloArray.new(obj) if obj.class == Array
    return obj
  end
end

class HelloArrayInner < SimpleDelegator
  include Enumerable
  def each
    super do |item|
      item = HelloArray.new(item) if item.class == Array
      yield item
    end
  end
end

obj1 = HelloArray.new([])
obj2 = HelloArrayInner.new(obj1)

これで、obj2#mapのブロック引数はHelloArrayになるし、戻り値もHelloArrayになる。最後に、obj1、obj2と二段階に分けてデリゲーターを生成するのは手間なので、一回ですむようにHelloArray#initializeを修正することにしよう。

class HelloArray
  def initialize(obj)
    super
    self.__setobj__ HelloArrayInner.new(obj)
  end
end

Delegator#__setobj__は委譲先オブジェクトを変更するメソッドである。ここではobjからHelloArrayInnner.new(obj)へと委譲先を差し替えることで、HelloArrayとobjの間にHelloArrayInnerをはさみこんでいる。これにより、次のような書き方で二重構造のデリゲーターを生成することができるようになる。

obj = HelloArray.new([])

0 件のコメント:

コメントを投稿