クラス内で使いたい関数を明示的にincludeできるようにする
rubyで次のようなコードはわりとよくあると思います。
# some/utils.rb module Utils module_function def html_escape str # ... end def html_unescape str # ... end end # some/more_utils module MoreUtils module_function def some_func end end # some/foo.rb class Foo include Utils include MoreUtils def initialize html_escape 'html' end end
自分はこのコードが嫌な点が3つあって、
- html_escapeがどこで定義されているかsome/foo.rbを見ただけではわからない
- Fooクラスにはhtml_unescapeは不要なのにFooの中から呼べてしまい、しかもpublic_instance_methodになってしまう。
- Fooクラスにもescapeというメソッドが他にも存在しているので別名にしたい
というようなことが少し気にかかります。
これを気にするのであれば、明示的にモジュール名を書けばよいという話もありますが、あまりにも頻度が多いとそれはそれでコードが読みにくくなってしまうのがなんともといった感じです。
そこで、ここをより明示的に書けるようにするにはどうしたらいいんだろうということで書いてみました。
module FunctionExporter def export context, *args parent_mod = self if args.size == 1 && args.first.kind_of?(Hash) args.first.each do |original_name, want_name| context.module_eval %{ want_name = "#{want_name}" define_method want_name do |*params| #{parent_mod}.__send__ "#{original_name}", *params end if want_name =~ /^_/ private want_name end } end else args.each do |func| context.module_eval %{ func = "#{func}" define_method func do |*params| #{parent_mod}.__send__ func, *params end } end end end end module FunctionImporter def import_function mod, *funcs mod.export self, *funcs end end if $0 == __FILE__ # 対象となるようなモジュールはmodule_functionで定義してあるのが前提 module Utils extend FunctionExporter module_function def escape str str end def unescape str str end end # 一番単純な使い方。第2引数以降にFooの中で使いたいメソッドを並べる class Foo Utils.export self, :escape, :unescape def initialize p escape('hoge') end end # 上記の例と同様。selfを渡すのがちょっとカッコわるいのでシンタックスシュガーとしてFunctionImporterを使ってるだけ。 class Bar extend FunctionImporter import_function Utils, :escape, :unescape def initialize p escape('hoge') end end # Hashで渡すと、keyのメソッドをvalueの名前として使えるようになる # _で始まるメソッドはprivateメソッドになる class Baz Utils.export self, { :escape => :_escape, :unescape => :my_unescape } def initialize p _escape('hoge') end end Bar.new begin Baz.new._escape('hoge') rescue Exception => e warn e end Baz.new.my_unescape('hoge') end
ついmodule_evalでやってしまいましたが、こういうgemないかなぁと思って検索してみたらmodule-importというgemがみつかりました。
こちらは、
- importがKernel空間に直接定義されているということと、
別名で使えるようにするなどの機能がない( githubのtrunkなどをみると別名で書ける機能もあるっぽい )
といった点が個人的には不満ですが、元のモジュールをdupして不要なメソッドをundefしまくってからincludeする実装になっているので、自分の手法に比べ、
- 最終的にはincludeで入れているので元モジュールでmodule_functionしておく必要がない
- 元モジュールで定義されていないメソッドがあればimport時に例外を出してくれる
- Fooクラス内でのメソッド呼び出しのコストがないので実行時では速そう
といったあたりが優れているなぁと思いました。
探してみれば、過去に散々議論されてるネタなような気もします。
追記:
コードを一通り書き直してgemにしました。
gem install function_importerでインストールできます。
使い方などは特に変わってはいないですが、ドキュメントなどはこちらを見てください。 https://github.com/walf443/function_importer/tree