0
0

[Ruby] 委譲(def_delegators)を理解する

Posted at

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

なので、codenameがあたかもアクセサーとして定義されているかのように振る舞います。

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
0
0
0

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
0
0