初めまして、ラクマでサーバサイドエンジニアをやっているtatsumiです。
普段は業務で Ruby on Railsを使っています。
業務で、サードパーティー製のgemのモンキーパッチを行ったのでいくつか方法を紹介しようと思います。
参考文献
この記事を作る上で、参考にした記事が以下の2つでこれらには書かれていない情報を記述しました。
環境
Ruby 2.5.7
Ruby on Rails 5.2.4.4
モンキーパッチとは
モンキーパッチの概要について説明したいと思います。
今日では何かシステムを作る時に、よく使う機能はすでに作成されていると思います。
それらをインストールして、仕様に合わせて足りない部分を自分で実装していく方法が一般的だと思います。
仮に、自分で実装した部分に不具合が発生した場合、その部分を修正すれば良いですが、
サードパーティー製のライブラリの方で不具合が発生したり、一部挙動を変えたい場合に修正を行うとそれを使っている他の全プロジェクトに影響してしまいます。
オリジナルのソースコードを変更することなく自分のプロジェクト内だけで、サードパーティー製のライブラリのコードを拡張したり、変更したりする方法のことをモンキーパッチと読んでいます。
事前準備
Railsのlibフォルダ配下に仮のsample_gemを想定してファイルを配置します。
sample_gemの機能の概要を説明すると、
sample_gemのpropertyモジュールはextendした先のクラスで、property :fooと書くと、
fooという名前のゲッターとセッターが作成されてfooという名前のインスタンス変数が格納できるようになります。
今回はパッチで、SampleGem::SampleKlassにproperty :foo3を追加したいと思います。
lib/sample_gem/property.rb
module SampleGem module Property def property(name) module_eval <<-END_OF_DEF def #{name} @#{name} end def #{name}=(value) @#{name} = value end END_OF_DEF end end end
lib/sample_gem/sample_klass.rb
module SampleGem class SampleKlass extend SampleGem::Property property :foo1 property :foo2 end end
[1] pry(main)> s = SampleGem::SampleKlass.new => #<SampleGem::SampleKlass:0x00007f9bac81a3b8> [2] pry(main)> s.foo1 = 'test1' => "test1" [3] pry(main)> s.foo2 = 'test2' => "test2" [4] pry(main)> s.foo1 => "test1" [5] pry(main)> s.foo2 => "test2"
モンキーパッチの方法紹介
1. 以前、ラクマで行ったモンキーパッチの方法
以前、ラクマでもモンキーパッチを行ったことがあります。
その時はこの記事で紹介する方法とは異なるのですが、
参考までに紹介したいと思います。
config/initializers/gem_name/file_name.rb
module Name # ここで、処理の上書きを行う def override end end
config/initializerにモジュールごと上書きをしていました。
ただし、この方法だと以下のようなデメリットがありました。
- モジュールの処理を丸々上書きしてしまうので、一部だけ変えたい時にも全部の処理を書く必要がある
- config/initializersにパッチファイルが散在し、第三者から見た時にどこでパッチしているかがわかりにくい
2. モジュールをincludeする方法
1.1で挙げた問題を解決するために、パッチファイルを1箇所にまとめるようにしました。
config/initializers/monkey_patches.rbを作成して、lib/monkey_patches配下の全てのパッチファイルをプロジェクト全体で読み込むようにしました。
これで、どこでパッチを行っているかがわかりやすくなりました。
config/initializers/monkey_patches.rb
Dir[Rails.root.join('lib', 'monkey_patches', '**', '*.rb')].sort.each do |file| require file end
lib/monkey_patches配下にsample_gem/sample_klass_extension.rbファイルを配置します。
今回は、property :foo3を追加したファイルを用意しました。
lib/monkey_patches/sample_gem/sample_klass_extension.rb
module SampleKlassExtension extend SampleGem::Property property :foo3 end SampleGem::SampleKlass.include(SampleKlassExtension)
最後の行で定義したモジュールを元のモジュールにincludeしています。
他の記事を見ると、includeの箇所をprependしている記事があるのですが、
これはメソッドをパッチする場合で、prependを使って先にパッチされたメソッドを呼ぶようにしています。
今回は、クラスに新しいpropertyを追加するパッチなので、prependでもincludeでも動きました。
これで、foo3がpropertyに追加されました。
修正前
[1] pry(main)> s = SampleGem::SampleKlass.new => #<SampleGem::SampleKlass:0x00007ffc16088bc0> [2] pry(main)> s.foo3 = 'test' NoMethodError: undefined method `foo3=' for #<SampleGem::SampleKlass:0x00007ffc16088bc0> Did you mean? foo1= foo2= foo2 foo1 from (pry):2:in `__pry__'
※ foo3 propertyが含まれていないことを確認
修正後
[1] pry(main)> s = SampleGem::SampleKlass.new => #<SampleGem::SampleKlass:0x00007fb96c823110> [2] pry(main)> s.foo3 = 'test' => "test" [3] pry(main)> s.foo3 => "test"
※ foo3 propertyが含まれていることを確認
3. itselfを使う方法
config/initializers/monkey_patches.rbを用意するのは同じでincludeする代わりに、itselfを使います。
lib/monkey_patches/sample_gem/sample_klass_extension.rb
# event_representerファイルに設定されているプロパティを展開 SampleGem::SampleKlass.itself # new_propertyを追加 module SampleGem class SampleKlass property :foo3 end end
itselfはレシーバ自身を返すメソッドで、パッチファイルに付けるとそこで定義されているプロパティを全て展開してくれます。
よって、2. で紹介したincludeの方法と異なり追加したいプロパティを記述するだけです。
修正前
[1] pry(main)> s = SampleGem::SampleKlass.new => #<SampleGem::SampleKlass:0x00007f9bbc486138> [2] pry(main)> s.foo3 = 'test' NoMethodError: undefined method `foo3=' for #<SampleGem::SampleKlass:0x00007f9bbc486138> Did you mean? foo2= foo1= foo1 foo2 from (pry):2:in `__pry__'
※ foo3 propertyが含まれていないことを確認
修正後
[1] pry(main)> s = SampleGem::SampleKlass.new => #<SampleGem::SampleKlass:0x00007fc65a25d5a0> [2] pry(main)> s.foo3 = 'test' => "test" [3] pry(main)> s.foo3 => "test"
※ foo3 propertyが含まれていることを確認
まとめ
Railsでモンキーパッチする方法をいくつか紹介しました。
メソッドをパッチする場合は2. で紹介したincludeを使う方法を採用すれば良く、
クラスなどのオブジェクト自体の設定をパッチする場合は3. で紹介したitselfを使う方法を採用すれば良いと思います。