Railsでgemに対しモンキーパッチを当てる方法
プログラミング #gem #rails #モンキーパッチ目次 / Index
はじめに
既存の組み込みクラスやライブラリ、外部ライブラリなどに対して動的に変更を加える「モンキーパッチ」という技術があります。
モンキーパッチを使うことで、既存の機能を拡張したり、バグを修正したりすることができます。
しかし、基本的にはモンキーパッチは使用する前提とせず、gemのアップデートや他の実装方法で回避できないか模索すべきです。
なぜ使用する前提としないほうが良いか、当て方などご紹介していきます。
なぜモンキーパッチを使用しない前提とすべきか?
モンキーパッチは、以下のように既存クラスにメソッドを追加/上書きします。(以下の例はRubyのオープンクラスを用いたモンキーパッチ)
p 'snake_case'.try(:to_camel_case) # => nil
class String
def to_camel_case
self.split(/_/).map{ |str| str[0] = str[0].upcase; str }.join
end
end
p "snake_case".to_camel_case # => "SnakeCase"
こちら見て頂くと分かる通り、既存クラスの定義元以外でメソッドが追加されています。
また、こちらはStringクラスのため、他のクラスでも使われる可能性が大いにあります。
この場合、もし他のgemで同じようにメソッド定義をしていた場合に、コンフリクトが起きてしまい、意図せぬ挙動となりバグが発生する原因となります。
これは大分極端な例ではありますが、こういった可能性があることを考慮しておく必要があります。
また、gemの一部のクラスのメソッドをモンキーパッチする時もコンフリクトを意識する必要があります。
モンキーパッチを当てた際、現在使用しているgemのバージョンに関しては問題なかったとしても、将来的にはgemのバージョンアップデートによる破壊的変更により、メソッドに期待する挙動が変わり、gemが意図せぬ挙動をする可能性があります。
これらを考慮し、なるべくはモンキーパッチを避ける方法を模索する方が、メンテナンスしやすいシステムになります。
しかし、使用しているgemになんらかのバグがありまだ修正されていないなど、モンキーパッチを当てる必要がある場面も出てくると思いますので、以降でモンキーパッチを当てる方法をご紹介します。
Railsでgemに対しモンキーパッチを当てるには?
事前準備
config/initializers/monkey_patches.rb
のファイルを作成し、以下の内容とします。
Dir[Rails.root.join('lib/monkey_patches/**/*.rb')].each do |file|
require file
end
こうすることで、lib/monkey_patches
配下にモンキーパッチに関するファイルを集約でき、確認がしやすくなります。
結果、Railsアップデートでgemが大量にアップデートされた時など、このディレクトリ配下のファイルのみを確認することでモンキーパッチの対応漏れを防ぐ事ができます。
モンキーパッチ用ファイルを作成
例としてpaper_trail-association_tracking
gemにモンキーパッチを当てる想定とします。
https://github.com/westonganger/paper_trail-association_tracking/blob/master/lib/paper_trail_association_tracking/record_trail.rbに当てたい場合はパスをlib/monkey_patches/paper_trail_association_tracking/record_trail.rb
のようにします。
見てお気づきかと思いますが、gemの/lib/paper_trail_association_tracking/record_trail.rb
と同じパス構造になるようにしております。
モンキーパッチに関しては、他のgemでも同じ形式にすることで、モンキーパッチを当てているオリジナルのパスが容易に推測でき、gemアップデート時など、モンキーパッチを適用したままで問題ないかなどの確認がしやすくなります。
ファイルの内容
以下はlib/monkey_patches/paper_trail_association_tracking/record_trail.rb
のモンキーパッチの例となります。
module PaperTrailAssociationTrackingRecordTrailMonkeyPatch
def data_for_create
# ...
end
end
PaperTrailAssociationTracking::RecordTrail.prepend PaperTrailAssociationTrackingRecordTrailMonkeyPatch
こちら以下のように対象モジュールをオープンクラスのように定義してモンキーパッチすることも可能です。(同じ名前のモジュールがあった場合、上書き/追加となる。参考)
module PaperTrailAssociationTracking
module RecordTrail
def data_for_create
# ...
end
end
end
なぜprepend
を使っているかですが、prepend
を使う場合、super
を使って元のメソッドを呼び出すことができます。
これにより、既存の機能を維持しながら、追加の処理を行うことができるため、モンキーパッチをする際はprepend
を使用したほうが何かと便利です。
最低限、モンキーパッチを当てた箇所のユニットテストを書く
モンキーパッチを当てたgemのバージョンのテストを書くと、gemのバージョンアップデート時の確認漏れを防ぐことができ、結果バグを未然に防ぐことができます。(コメントなどで「モンキーパッチを当てているため確認」しているなど背景がわかるようにすべき。)
また、モンキーパッチを当てた箇所だけでもユニットテストは作成するようにはしておきたいです。これにより、gemのバージョン更新時のデグレ防止に役立ち、メンテナンスが楽になります。
本来、モンキーパッチを当てた対象のgemを使用している箇所全体で、正常に動作していることを確認するリグレッションテストを行えるようにしたいですが、そこまで工数はかけられないのが現実だと思いますので、最低限、gemのバージョンの確認やモンキーパッチを当てた箇所のユニットテストだけでも書いておくと安心できます。
最後に
以上、私が考える「Railsでgemに対しモンキーパッチを当てる方法」でした。
モンキーパッチはなるべく使わず、もし使うにしてもメンテナンスしやすいよう工夫していきたいですね。