switch_point Active Recordにメソッドを追加・変更する部分を勉強してみた

昨日DB関連の資料を調べていく中で興味があった『eagletmt/switch_point』がどんなふうに実装されているのかが、興味あって、ソースコードを読んでみました。特に興味があったのは、Active Record::Baseなどにメソッドを追加したり、既存のメソッドに手を加える部分です。まだわかっていない部分も多いのでメモ書きレベルですが、一応せっかくなのでアウトプットしておきます!


🏈 既存のActiveRecod::Baseにメソッドを生やす

eagletmt/switch_point - GitHub』のswitch_point/lib/switch_point.rbで既存のActiveRecod::Baseにメソッドを生やす目的で次のようなコードが書かれていました。

require 'active_support/lazy_load_hooks'
# ...(省略)...
# ActiveRecordが読み込まれたら以下のコードを実行する
# 第一引数(active_record)がフックのキー
# 第二引数のブロックが、ブロックを実行するためのコンテキスト
ActiveSupport.on_load(:active_record) do
require 'switch_point/model'
require 'switch_point/connection'
# SwitchPoint::ModelのメソッドをActiveRecord::Baseにinclude
ActiveRecord::Base.send(:include, SwitchPoint::Model)
# class_evalで動的にクラス・メソッドを定義
ActiveRecord::ConnectionAdapters::AbstractAdapter.class_eval do
include SwitchPoint::Connection
# insert, update, deleteメソッドのメソッドを上書き
# 中ではwritableなDBにつなぎに行っているかのチェックとクエリー・キャッシュをクリアしてからメソッドを実行
SwitchPoint::Connection::DESTRUCTIVE_METHODS.each do |method_name|
alias_method_chain method_name, :switch_point
end
end
end

🗽 遅延読み込みフックlazy_load_hooks#on_loadについて

on_loadは、Active SupportのLazyLoadの機能のひとつで、ライブラリの読み込み後に実行したいコードを登録するための機能だそうです。サンプルとしてはこんなかんじです。

require 'active_support/lazy_load_hooks'
# run_load_hooksが実行されていないので呼ばれない
ActiveSupport.on_load :fuga do
puts 'start 1st on load'
fuga
puts 'end 1st on load'
end
class Fuga
def self.fuga
puts 'fugafuga'
end
end
puts 'before run_load_hooks'
# run_load_hooksを実行すると、4行目の ActiveSupport.on_load が呼ばれる
ActiveSupport.run_load_hooks :fuga, Fuga
puts 'after run_load_hooks'
# run_load_hooks が実行されているのですぐに呼ばれる
ActiveSupport.on_load :fuga do
puts 'start 2st on load'
fuga
puts 'end 2st on load'
end
# 実行結果
# before run_load_hooks
# start 1st on load
# fugafuga
# end 1st on load
# after run_load_hooks
# start 2st on load
# fugafuga
# end 2st on load

コードにちょこちょこ書いていますが、要はActive Support.run_load_hooksが実行されないと呼ばれないし、
Active Support.run_load_hooksが呼ばれれば一緒に実行してくれるということっぽいです。

ここいらは、@eielさんのRails Docの記事『RailsDoc - Lazy Load Hooks』を参考にさせていただきました。最近Rails Docさんにはお世話になりっぱなしです!

🚕 alias_method_chainについて

alias_method_chainは、既存のメソッドを置き換えをしてくれます。

# active_supportのメソッドなのでrequireが必要
require 'active_support/all'
module LogHelper
def log(message)
puts message
end
def log_with_timestamp(message)
log_without_timestamp("[#{Time.now}] #{message}")
end
# 以下と同義
# alias_method :log, :log_with_timestamp
# alias_method :log_without_timestamp, :log
alias_method_chain :log, :timestamp
end
include LogHelper
log('Hello') #=> [2015-01-02 11:45:01 +0900] Hello

このメソッドには次のルールがあるそうです。

 (A) 前提
   alias_method_chain :xxx, :yyy

 (B) ルール
   (1) 上書きして呼び出すメソッドは、 xxx_with_yyyとする
   (2) 元となるメソッドは、 xxx_without_yyyとする

ちなみにActive Support(v4.2.0)のalias_method_chainのソースコードはこんなかんじです。
(わかりやすい^^)

def alias_method_chain(target, feature)
# Strip out punctuation on predicates, bang or writer methods since
# e.g. target?_without_feature is not a valid method name.
aliased_target, punctuation = target.to_s.sub(/([?!=])$/, ''), $1
yield(aliased_target, punctuation) if block_given?
with_method = "#{aliased_target}_with_#{feature}#{punctuation}"
without_method = "#{aliased_target}_without_#{feature}#{punctuation}"
alias_method without_method, target
alias_method target, with_method
case
when public_method_defined?(without_method)
public target
when protected_method_defined?(without_method)
protected target
when private_method_defined?(without_method)
private target
end
end

シナジーマーケティングさんの、» Rails: alias_method_chain: 既存の処理を修正する常套手段 TECHSCORE BLOG
がすごくわかりやすい説明を書いていただけていたので参考にさせていただきました。ありがたうございます!

🎃 モジュール#prependについて

モジュール#prepend自体はswitch_pointには出てきませんが、調べていく中でRuby 2.0で導入されたメソッドのモジュール#prependalias_method_chainを置き換えることができるのを知りました。使い方としてはこんな感じ。

module LoggingWithTimestamp
def log(message)
# 呼び出し元のメソッドを呼び出す
super("[#{Time.now}] #{message}")
end
end
class Logging
# LoggingWithTimestampのlogが、Loggingのlogメソッドよりも優先して呼ばれる
prepend LoggingWithTimestamp
def log(message)
puts message
end
end
Logging.new.log('Hello') #=> [2015-01-02 12:23:15 +0900] Hello

includeprependとの使い分けは次のとおり。

* include: 新しい機能を追加するために使う
  * モジュール側のメソッドはクラスのメソッドを上書きできない
  * クラス側のメソッドはモジュールのメソッドを上書きできる

* prepend: 既存の機能の変更をするために使う
  * モジュール側のメソッドはクラス側のメソッドを上書きでる
  * クラス側のメソッドはモジュール側のメソッドを上書きできない

こちらもシナジーマーケティングさんのブログ記事『» Ruby2.0のModule#prependは如何にしてalias_method_chainを撲滅するのか!?』が図解も含めてすごくわかりやすい解説をしてくれています!

🤔 あとがき

今まで何の気なしに使ってきたライブラリですが、よく読んでみるといろいろと勉強になる部分が多くあります。
自分がライブラリ書くときに使えそうなTipsがあってすごい勉強になります! 今度はテストとかも読んでみるつもりです!

🎉 参考リンク

Rubyのdefine_method、class_evalで動的に定義されたメソッドの呼出コストを調べてみた - Qiita

📚 おすすめの書籍

💩 欲しいものリスト公開しました