Rubyにはdef_delegators
というメソッド委譲と呼ばれる処理を行うためのクラスメソッドが定義されています。ちなみに、RailsのActiveSupportにもdelegate
という同じようなメソッドがあります。
メソッド委譲を使うと、クラス外で定義されているメソッドを、あたかもインスタンスメソッドのように呼び出すことができるようになります。
class Person
attr_accessor :name
attr_accessor :age
end
class Staff
attr_accessor :person
def_delegators :person, :name, :age
end
p = Person.new
p.name = "Taro"
p.age = 20
s = Staff.new
s.person = p
pp s.name # => Taro
コードリーディング
まずは、delegate
メソッドは次のように定義されています。
forwardable.rb:198
alias delegate instance_delegate
instance_delegate
メソッドの処理を追いかけていくと、def_instance_delegator
と_delegator_method
に主要な処理が書かれていることが分かります。コードは長いのでAppendixに載せています。
_delegator_method
では、インスタンスメソッド定義を文字列として作って、def_instance_delegator
の中でmodule_eval
を呼び出すことでクラスにインスタンスメソッドを追加しています。
具体例
例えば、次のような委譲を定義することを考えます。
def_delegators @customer, :code, :name
すると、インスタンスメソッドとして、code
メソッドとname
メソッドが定義されます。
def code(*args, &block)
_ = @customer
_.__send__(:code, *args, &block)
end
def name(*args, &block)
_ = @customer
_.__send__(:name, *args, &block)
end
なので、code
とname
があたかもアクセサーとして定義されているかのように振る舞います。
Appendix
def_instance_delegator
def def_instance_delegator(accessor, method, ali = method)
gen = Forwardable._delegator_method(self, accessor, method, ali)
# If it's not a class or module, it's an instance
mod = Module === self ? self : singleton_class
ret = mod.module_eval(&gen)
mod.__send__(:ruby2_keywords, ali) if RUBY_VERSION >= '2.7'
ret
end
_delegator_method
forwardable.rb:203
def self._delegator_method(obj, accessor, method, ali)
accessor = accessor.to_s unless Symbol === accessor
if Module === obj ?
obj.method_defined?(accessor) || obj.private_method_defined?(accessor) :
obj.respond_to?(accessor, true)
accessor = "#{accessor}()"
end
method_call = ".__send__(:#{method}, *args, &block)"
if _valid_method?(method)
loc, = caller_locations(2,1)
pre = "_ ="
mesg = "#{Module === obj ? obj : obj.class}\##{ali} at #{loc.path}:#{loc.lineno} forwarding to private method "
method_call = "#{<<-"begin;"}\n#{<<-"end;".chomp}"
begin;
unless defined? _.#{method}
::Kernel.warn #{mesg.dump}"\#{_.class}"'##{method}', uplevel: 1
_#{method_call}
else
_.#{method}(*args, &block)
end
end;
end
_compile_method("#{<<-"begin;"}\n#{<<-"end;"}", __FILE__, __LINE__+1)
begin;
proc do
def #{ali}(*args, &block)
#{pre}
begin
#{accessor}
end#{method_call}
end
end
end;
end