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
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 件のコメント:
コメントを投稿