Refinementsの用法用量わからない日記
自分はわりとカジュアルにRefinementsを使うけど基準はどこにあるのだろう。と今日考えていた。
RefinementsSpecを読んでみる
はい
Monkey patching is a powerful feature of Ruby.However, it affects globally in a program. Therefore, a monkey patch might break code which doesn't expect the extended behavior, and multiple monkey patches for the same class might cause conflicts.To solve these problems, Refinements provide a way to extend classes locally. https://bugs.ruby-lang.org/projects/ruby-master/wiki/RefinementsSpec
モンキーパッチはRubyの強力な機能の1つ。しかしながら、モンキーパッチはプログラムにグローバルに影響を与える。したがって、モンキーパッチはモンキーパッチにより拡張された振る舞いを予期していないコードを壊す可能性があり、また一つのクラスに対して複数のモンキーパッチを当てるとコンフリクトを引き起こすかもしれない。これらの問題を解決するため、Refinementsはクラスを局所的に拡張する方法を提供する。
みたいな感じ?
局所的に、というのがキモっぽい
クラスの動作を拡張したいが局所的にしたい、というのが大事っぽい。 具体的にはどういうときなのか判断はわりとグラデーションがあると思う。
アプリケーションコードにおいて局所的に拡張する必要性
実はほとんどないのでは、と思う。
例えばアプリケーションのコードの動作を拡張したい場合
「アプリケーションコードの動作を拡張したい!」 「じゃあアプリケーションコードを書き換えようか (完)」
はい。
「局所的にアプリケーションの動作を拡張したい!」 「じゃあそういう設計でアプリケーションコードを書き換えようか (完)」
はい。
となりますよね。
現代のモンキーパッチはオープンクラスをせずにできる
モンキーパッチをどういうときに用いるかというと
- 組み込みライブラリの動作を変更したい
- gemで導入したライブラリの動作を変更したい
などアプリケーションコードとは違う自らが書き換えできないコードをダイナミックに書き換えるときだと思う。 じゃあそれモンキーパッチじゃないとできないの?というと現代だとそんなことはない。 例えば書き換えたgemをGitHubに置いてGemfileでgitリポジトリを指定すれば済む。
だがそれだともちろん局所性はない。
オープンクラスが必要な場面
やっぱり自分の手が届かないところのコードの動作を書き換えるときかなあ。(自分が書いてるのはだいたいそう)
局所性が必要な場面とは
例えば同じ名前のメソッドが被ると困るだろう。 でも現実問題アプリケーションコードのためにモンキーパッチで拡張したときに困るのって、既にあるメソッドの名前と同じ名前で書き換えたときぐらいしかなさそう。 別の名前のメソッドを追加して動作を拡張するのはまあ、名前がかぶれば変えればいいだけなので。
多分ライブラリを書いている場合は、ライブラリが使われる場面には手が届かないのでより局所性に気を使う必要がありそう、なのでRefinementsの使いどころはアプリケーションコードよりはありそう。
局所性を求めるとやっぱりRefinements、だが真に必要な場面は
局所性を求めるとRefinements、これは多分異論ないと思う。
だけど真にRefinementsでしか実現しないような使いどころとなるとかなり限られてしまいそう。
- アプリケーションコードではないライブラリの動作を拡張している
- アプリケーションコードの方の設計を変えられない
- ライブラリの方で定義されているメソッドの動作を変えるので局所性が必要
- 自分のコードがどこで使われるか分からないので自分のコード内だけで使う動作に局所性が必要
アプリケーションからは変更出来ないコードを変更したいが影響範囲を狭めたい、そのぐらいしかなさそうで。ほかはアプリケーションコード変えればいいだけだから。
アプリケーションのコードなのかライブラリのコードなのかの境界はどこにあるのか
例えばDeviseのモジュールを例にすると、Deviseのモジュールを使っているクラスにはDeviseのモジュール由来のメソッドがいくつか生える。
このときDeviseによって生えたメソッドを利用し、アプリケーション固有のユースケースを実現するコードはアプリケーションコードだと思える。 一方「この機能はDeviseの方でも持っていてほしいなあ、わりと特殊なユースケースかもしれないけど」と思うコードや「これはDeviseと組み合わせて使えるライブラリに出来るコードかもしれないなあ」というコードもあると思う。 それはアプリケーション固有の事情を表していないので純粋に単なるアプリケーションコードとしてしまうにはもったいない。
十分一般的な目的に抽象化されたアプリケーションコードはそれはもうライブラリなのでは、と思ってしまう。
Refinementsの使いどころにアプリケーションコードとライブラリの境目も含めてしまってよいのでは
そういう本来ならこのメソッドはライブラリに置かれててくれどうぞ、みたいな気持ちとRefinementsは相性いいと思うんですよね。
特定の場面でしか発生しないようなユースケースも十分抽象化されていたら実質ライブラリなのでは
例えば大クラス主義的にArray
に色々生えてて日常生活ではことたりるんだけど、たまーに「アプリケーション固有かもしれないけど each_with_いろは
がほしい」とかあると思うんですよ。
それもわりとアプリケーション固有なのかライブラリなのか微妙な立ち位置で、Refinementsがはまると思うんですよね。
関連レコードを引くとかscope使いたいとかそういうActiveRecordの機能によるのはactive_typeのようなgemを使ってやると思うけど、 単純にこの場面でだけだけメソッド生やしたいとかならRefinementsで生やすだけでも十分みたいなの結構あると思うんですよね。
Refinements使うのを気をつけたほうがよい場合
めちゃくちゃパフォーマンス気にする場合はRefinements使わない方がいいのかなあというふわっとした感想です。(要出典、ここはFUDになりそうなので書かないほうがよかったかもしれない)
動的にメソッドの動作を切り替えたい場合にもRefinementsは便利
無名クラスでもできるかもしれませんが #to_refinements
メソッドでRefinementsのモジュールを生成するようなコードを書くと using object.to_refinements
で動的に動作を切り替えられて便利そうです。
rakeタスク中の処理を読みやすくメソッドにわけたいときもRefinementsは便利
これに関してはモデルにうつしたほうがテストしやすいのでいい派が多分いると思います
これも特定のユースケースの場合のみ機能を拡張したい場合の1つかなあ 通常のアプリケーションコードの中では使われてほしくないけどrakeタスクの中だけではメソッドであってほしい、そういう気持ち。
何かを受け流すときにもRefinementsを使うと便利
例えばRuboCopで定数のfreezeを呼びたいけどどうしてもテストではfreezeされてると都合が悪いとき、とか。
以外と真に必要そうなケースあるな
用法用量所感
どうしてもRefinementsじゃないとダメ!となる前にアプリケーションコードの変更でどうにかなってしまう場合が多い。 なのでそういうどうしようもない場合にのみRefinementsの処方を限定するとRefinementsを使う機会がかなり減ってしまうと思う。
アプリケーションコードなのかライブラリなのか曖昧な、アプリケーション固有のユースケースのようなそうでないような、ライブラリに置かれててくれどうぞ、みたいな気持ちとRefinementsは相性がいいと思う。
まとめ
ハチャメチャにRefinementsキメましょう