Railsで開発していると使っているGemの挙動を一部変更したくなることが稀にあると思います。
gemに限らずですが、外部ライブラリの挙動を変えるために書く仕組みはモンキーパッチと呼ばれます。
モンキーパッチについては下記のクックパッドさんの記事がわかりやすかったのでご覧ください。
この記事でもこのやり方を参考にしています。
https://techlife.cookpad.com/entry/a-guide-to-monkey-patchers
モンキーパッチはある程度やり方が定型化されていますが、たまにしか実装しないのでやり方を忘れてしまいがちだと思います。
そこで備忘録としてよくありそうなモンキーパッチの当て方をこの記事に残しておこうと思います。
対象のgem
モンキーパッチを当てるsample_gemは下記の通り。
仕様
- incrementというクラス変数を持っている
- 渡した数値を+1して返却する
- 数値以外を受け取ると
SampleGem::NoIntegerError
- initializeではnumを渡してインスタンス変数に設定する
- 数値以外を受け取ると
SampleGem::NoIntegerError
- 数値以外を受け取ると
- incrementというインスタンス変数を持っている
- 実行するとインスタンス変数のnumを+1する
- 定数: SAMPLE_CONSTが定義されている
module SampleGem
class SampleKlass
SAMPLE_CONST='これはサンプルです'
attr_reader :num
class << self
def increment(num)
raise SampleGem::NoIntegerError unless num == num.to_i
num + 1
end
def sample_const
SAMPLE_CONST
end
end
def initialize(num)
raise SampleGem::NoIntegerError unless num == num.to_i
@num = num.to_i
end
def increment
@num += 1
end
end
end
詳細は下記のリポジトリ参照
https://github.com/ham0215/sample_gem
モンキーパッチを当てる
事前準備
適当なrailsアプリを作ってそこにsample_gemを追加します。
rubygemsには公開していないのでリポジトリを直接参照しています。
gem 'sample_gem', git: 'https://github.com/ham0215/sample_gem.git', branch: 'master'
冒頭で紹介したクックパッドさんの記事を参考にパッチファイルを作成
Dir[Rails.root.join('lib/monkey_patches/**/*.rb')].sort.each do |file|
require file
end
require 'sample_gem/version'
unless SampleGem::VERSION == '0.1.0'
raise 'Consider removing this patch'
end
### ここに実装していきます ###
インスタンスメソッドにパッチをあてる
まずはインスタンスメソッドにパッチをあててみます。
『#increment
を使わなくしたいが、ソースが膨大すぎて使用箇所を全て削除できたか不安。もし呼ばれたらdeprecated warningをログ出力するようにしてしばらく様子をみてみよう。』
というユースケースを考えてみます。
実装例
- パッチを実装するmoduleを定義(
SampleKlassMonkeyPatch
) - 上書きしたいメソッド(
increment
)を定義 - deprecated warningのログ出力する処理を書く。元々の処理はそのままで良いので
super
を呼び出す - 対象のclass(
SampleGem::SampleKlass
)にprependする
prependすることで既存クラスのインスタンスメソッドより先にパッチのメソッドが呼ばれるようになります。
module SampleKlassMonkeyPatch
def increment
Rails.logger.warn 'DEPRECATION WARNING: 使わないで!'
super
end
end
SampleGem::SampleKlass.prepend(SampleKlassMonkeyPatch)
コンソールで動作確認
> sample = SampleGem::SampleKlass.new 10
> sample.increment
DEPRECATION WARNING: 使わないで!
=> 11
> sample.increment
DEPRECATION WARNING: 使わないで!
=> 12
クラスメソッドにパッチをあてる
次にクラスメソッドにパッチをあててみます。
『Class.increment(num)
は数値以外が渡されるとSampleGem::NoIntegerError
をraiseする仕様になっているが、前バージョンまでは数値以外を渡しても問答無用で.to_i
して0として扱われていた。
使っている箇所を全て見直して数値以外を渡さないようにするのがよいが使用箇所が多くて大変だ。
取り急ぎ、数値以外を渡しても例外が発生しないようにパッチをあてよう。』
というユースケースを考えてみます。
実装例
- パッチを実装するmoduleを定義(
SampleKlassMonkeyPatch
) - 上書きしたいメソッド(
increment
)を定義 - 引数を
to_i
で数値に直して既存のメソッドを呼び出す - 対象のclass(
SampleGem::SampleKlass
)のsingleton_classにprependする
4の手順でsingleton_class
にprependするところがインスタンスメソッドとは違う点です。
module SampleKlassMonkeyPatch
def increment(num)
super(num.to_i)
end
end
SampleGem::SampleKlass.singleton_class.prepend(SampleKlassMonkeyPatch)
コンソールで動作確認
> SampleGem::SampleKlass.increment(2)
=> 3
> SampleGem::SampleKlass.increment('a')
=> 1
定数を上書きする
これまで紹介した2つに比べてあまりやることはないと思いますが定数を上書きしたい場合を考えてみます。
実装例
-
=
で代入する
これをパッチをあてると言っていいのか微妙ですがこれだけです。
SampleGem::SampleKlass::SAMPLE_CONST = 'hogehoge'.freeze
コンソールで動作確認
> SampleGem::SampleKlass::SAMPLE_CONST
=> "hogehoge"
> SampleGem::SampleKlass.sample_const
=> "hogehoge"
おまけ
パッチがあたっていることの確認方法
実際に動作確認するのも良いですが、#ancestors
を使うことでモジュールの順序を確認することができます。
# インスタンスメソッドの確認
# 自分のクラスより前にパッチが挿入されていたらOK
> SampleGem::SampleKlass.ancestors
=> [SampleKlassMonkeyPatch, SampleGem::SampleKlass, ActiveSupport::Dependencies::ZeitwerkIntegration::RequireDependency, ActiveSupport::ToJsonWithActiveSupportEncoder, Object, PP::ObjectMixin, JSON::Ext::Generator::GeneratorMethods::Object, ActiveSupport::Tryable, ActiveSupport::Dependencies::Loadable, Kernel, BasicObject]
# クラスメソッドの確認
# 自分のクラスより前にパッチが挿入されていたらOK
> SampleGem::SampleKlass.singleton_class.ancestors
=> [SampleKlassMonkeyPatch, #<Class:SampleGem::SampleKlass>, #<Class:Object>, #<Class:BasicObject>, Class, Module, Module::Concerning, ActiveSupport::Dependencies::ModuleConstMissing, ActiveSupport::Dependencies::ZeitwerkIntegration::RequireDependency, ActiveSupport::ToJsonWithActiveSupportEncoder, Object, PP::ObjectMixin, JSON::Ext::Generator::GeneratorMethods::Object, ActiveSupport::Tryable, ActiveSupport::Dependencies::Loadable, Kernel, BasicObject]