普段仕事で使っている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なメソッドだけが委譲されます。
ソースコードを読んでみる
1. railsプロジェクトのactivesupportにある機能ですので、activesupportディレクトリのlib配下で def delegate_missing_to
を探してみます
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 に配列を与えると
このような集合を作成します
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
メタプログラミングについてはこちらの書籍がわかりやすかったので、もし読んでない方は読んでおくといいかもしれないです
- 作者: Paolo Perrotta,角征典
- 出版社/メーカー: オライリージャパン
- 発売日: 2015/10/10
- メディア: 大型本
- この商品を含むブログ (3件) を見る