Railsのソースコード読んでみる | Active Support delegate_missing_to編

f:id:sktktk1230:20190921180106p:plain

普段仕事で使っているRuby on Railsですが、ソースコードを読む機会もなかなかないので、試しにやってみることにしました

読めるようにするまで

以前書いた記事で読めるようにするまでの設定を画像キャプチャ付きで解説しましたので、よろしければこちらをご参照下さい
shitake4.hatenablog.com

読んだ箇所

delegate_missing_to を今日は読んでみようと思います

どんな使い方だっけ?

読んでみる前にまずは使い方を調べてみます
RAILS GUIDESの日本語ドキュメントを見てみると

UserオブジェクトにないものをProfileにあるものにすべて委譲したいとしましょう。

delegate_missing_toマクロを使えばこれを簡単に実装できます。

class User < ApplicationRecord
  has_one :profile
 
  delegate_missing_to :profile
end

オブジェクト内にある呼び出し可能なもの(インスタンス変数、メソッド、定数など)なら何でも対象にできます。対象のうち、publicなメソッドだけが委譲されます。

引用:RAILS GUIDES:delegate_missing_to

ソースコードを読んでみる

1. railsプロジェクトのactivesupportにある機能ですので、activesupportディレクトリのlib配下で def delegate_missing_to を探してみます

f:id:sktktk1230:20181206152315p:plain

2. 該当箇所が1箇所あったので、みてみます

1. activesupport > lib > active_support > core_ext > module > delegation.rb
# frozen_string_literal: true

require "set"

class Module

省略

def delegate_missing_to(target)
    target = target.to_s
    target = "self.#{target}" if DELEGATION_RESERVED_METHOD_NAMES.include?(target)

    module_eval <<-RUBY, __FILE__, __LINE__ + 1
      def respond_to_missing?(name, include_private = false)
        # It may look like an oversight, but we deliberately do not pass
        # +include_private+, because they do not get delegated.

        #{target}.respond_to?(name) || super
      end

      def method_missing(method, *args, &block)
        if #{target}.respond_to?(method)
          #{target}.public_send(method, *args, &block)
        else
          begin
            super
          rescue NoMethodError
            if #{target}.nil?
              raise DelegationError, "\#{method} delegated to #{target}, but #{target} is nil"
            else
              raise
            end
          end
        end
      end
    RUBY
  end
end

まずはここまで見ていきます

def delegate_missing_to(target)
  target = target.to_s
  target = "self.#{target}" if DELEGATION_RESERVED_METHOD_NAMES.include?(target)

引数targetを文字列へ変換しています
そしてtargetは DELEGATION_RESERVED_METHOD_NAMES に含まれているのかチェックしています

定数 DELEGATION_RESERVED_METHOD_NAMES がどのようなものか調べる為、記述箇所付近を見てみます

class Module
  # Error generated by +delegate+ when a method is called on +nil+ and +allow_nil+
  # option is not used.
  class DelegationError < NoMethodError; end

  RUBY_RESERVED_KEYWORDS = %w(alias and BEGIN begin break case class def defined? do
  else elsif END end ensure false for if in module next nil not or redo rescue retry
  return self super then true undef unless until when while yield)
  DELEGATION_RESERVED_KEYWORDS = %w(_ arg args block)
  DELEGATION_RESERVED_METHOD_NAMES = Set.new(
    RUBY_RESERVED_KEYWORDS + DELEGATION_RESERVED_KEYWORDS
  ).freeze

定義箇所はこちらです

DELEGATION_RESERVED_METHOD_NAMES = Set.new(
  RUBY_RESERVED_KEYWORDS + DELEGATION_RESERVED_KEYWORDS
).freeze

class Set (Ruby 2.6.0)は集合を表すクラスになります
Set.new で引数のオブジェクトを要素として集合を作ります

Set.new に配列を与えると

Setの挙動
Setの挙動

このような集合を作成します

RUBY_RESERVED_KEYWORDS, DELEGATION_RESERVED_KEYWORDS で定義されている文字列を見てみると

RUBY_RESERVED_KEYWORDS = %w(alias and BEGIN begin break case class def defined? do
else elsif END end ensure false for if in module next nil not or redo rescue retry
return self super then true undef unless until when while yield)
DELEGATION_RESERVED_KEYWORDS = %w(_ arg args block)

こちらの文字列に含まれているかをチェックしているようです

ここまでを踏まえてもう一度、こちらを見てみると

target = target.to_s
target = "self.#{target}" if DELEGATION_RESERVED_METHOD_NAMES.include?(target)

targetが含まれている場合は、self.target で自身のメソッドを呼び出します

そして次を見ていきます

module_eval <<-RUBY, __FILE__, __LINE__ + 1
  def respond_to_missing?(name, include_private = false)
    # It may look like an oversight, but we deliberately do not pass
    # +include_private+, because they do not get delegated.

    #{target}.respond_to?(name) || super
  end

  def method_missing(method, *args, &block)
    if #{target}.respond_to?(method)
      #{target}.public_send(method, *args, &block)
    else
      begin
        super
      rescue NoMethodError
        if #{target}.nil?
          raise DelegationError, "\#{method} delegated to #{target}, but #{target} is nil"
        else
          raise
        end
      end
    end
  end
RUBY

instance method Module#class_eval (Ruby 2.6.0)を使用して動的にメソッドを定義しています
ヒアドキュメント <<-RUBY 〜 RUBY に記載されている FILE , LINE は疑似変数というものです

FILE
現在のソースファイル名

フルパスとは限らないため、フルパスが必要な場合は File.expand_path(FILE) とする必要があります。

LINE
現在のソースファイル中の行番号
引用:Ruby 2.5.0 リファレンスマニュアル#疑似変数

動的に定義するメソッドを1つずつ見ていきます

def respond_to_missing?(name, include_private = false)
  # It may look like an oversight, but we deliberately do not pass
  # +include_private+, because they do not get delegated.

  #{target}.respond_to?(name) || super
end

実際に動作する部分はこちらで

#{target}.respond_to?(name) || super

targetにnameメソッドが存在するか、もしなければ、継承チェーン内の親の instance method Object#respond_to_missing? (Ruby 2.6.0)を実行します

次にもうひとつ定義されるメソッドを見てみます

def method_missing(method, *args, &block)
  if #{target}.respond_to?(method)
    #{target}.public_send(method, *args, &block)
  else
    begin
      super
    rescue NoMethodError
      if #{target}.nil?
        raise DelegationError, "\#{method} delegated to #{target}, but #{target} is nil"
      else
        raise
      end
    end
  end
end

instance method BasicObject#method_missing (Ruby 2.6.0)はメタプログラミングで利用されるテクニックで、オーバーライドすることで、存在しないメソッドを呼び出したときに、エラーとなる前に特定の処理を実行することができます

targetのpublicなmethodに存在しているか確認しています instance method Object#public_send (Ruby 2.6.0)
存在している場合は、そのメソッドを呼び出すという処理です

if #{target}.respond_to?(method)
  #{target}.public_send(method, *args, &block)

else句を見てみると、 NoMethodError が発生した場合、 targetがnilであれば、DelegationError とするようです

else
  begin
    super
  rescue NoMethodError
    if #{target}.nil?
      raise DelegationError, "\#{method} delegated to #{target}, but #{target} is nil"
    else
      raise
    end
  end
end

メタプログラミングについてはこちらの書籍がわかりやすかったので、もし読んでない方は読んでおくといいかもしれないです

メタプログラミングRuby 第2版

メタプログラミングRuby 第2版