概要
原著者の許諾を得て翻訳・公開いたします。
- 英語記事: Using anonymous modules and prepend to work with generated code | Arkency Blog
- 原文公開日: 2016/02/29
- 著者: Robert Pankowecki
- サイト: Arkency Blog
Ruby: gemが生成するコードを無名モジュールとprependで動かす(翻訳)
前回のセッター記事で、セッターメソッドがgemによって作成されている場合はどうすればよいのかというコメントをいただきました。このような場合、セッターをどのように上書きできるでしょうか?
たとえばawesome
という名前のgemがあり、そのgemのAwesome
モジュールが自分のクラスでawesome
ゲッターとawesome=(val)
というセッターを導入し、そこに興味深いロジックが仕込まれているとしましょう。これを次のように使うとします。
class Foo
extend Awesome
attribute :awesome
end
f = Foo.new
f.awesome = "hello"
f.awesome
# => "Awesome hello"
Awesome
は、一部のgemがやっているのと同じようにメソッド生成にメタプログラミングを用いた、しょうもない実装です。
これは少々わざとらしいコード例なのでご了承ください。
module Awesome
def attribute(name)
define_method("#{name}=") do |val|
instance_variable_set("@#{name}", "Awesome #{val}")
end
attr_reader(name)
end
end
何の変哲もないコードですが、このAwesome
の作者が見落としていることがあります。たとえばval
のストリップと前後のホワイトスペースの削除が忘れられてます。gem作者はあなたのユースケースを知る由もないのですから、他に何が見落とされててもおかしくありません。
理想的には次のように普通に書きたいところです。
class Foo
extend Awesome
attribute :awesome
def awesome=(val)
super(val.strip)
end
end
しかし今回はこの手が使えません。というのも、gemがメタプログラミングに依存していて、こちらのクラスにセッターメソッドを直接追加しているからです。単に上書きすると次のようになってしまいます。
Foo.new.awesome = "bar"
# => NoMethodError: super: no superclass method `awesome=' for #<Foo:0x000000012ff0e8>
このgemがメタプログラミングに依存しておらず、シンプルな書き方を採用していれば次のように書けます。
module Awesome
def awesome=(val)
@awesome = "Awesome #{val}"
end
attr_reader :awesome
end
class Foo
include Awesome
def awesome=(val)
super(val.strip)
end
end
これならシンプルにできたでしょう。しかし、フィールド名がプログラマー側から提供される必要のあるgemではこの楽な書き方が通用しません。
gemユーザー側でのソリューション
gemの作者がこちらのクラスにメソッドを直接追加している場合の方法は次のとおりです。
class Foo
extend Awesome
attribute :awesome
prepend(Module.new do
def awesome=(val)
super(val.strip)
end
end)
end
prepend
で無名モジュールを追加することによって、モジュール内で定義されているawesome=
セッターの階層が上昇します。
Foo.ancestors
# => [#<Module:0x00000002d0d660>, Foo, Object, Kernel, BasicObject]
gem作者側でのソリューション
gemユーザーを楽にしてあげられる方法があります。メソッドをクラスに直接定義するのではなく、メソッドを無名モジュールにinclude
すればよいのです。これならプログラマー側でsuper
を使えるようになります。
module Awesome
def awesome_module
@awesome_module ||= Module.new().tap{|m| include(m) }
end
def attribute(name)
awesome_module.send(:define_method, "#{name}=") do |val|
instance_variable_set("@#{name}", "Awesome #{val}")
end
awesome_module.send(:attr_reader, name)
end
end
これによって、メソッドをメタプログラミングで生成するモジュールの階層がクラス自身より下がります。
Foo.ancestors
# => [Foo, #<Module:0x000000018062a8>, Object, Kernel, BasicObject]
gemのユーザーはお馴染みのsuper
を使えば済むようになります。
class Foo
extend Awesome
attribute :awesome
def awesome=(val)
super(val.strip)
end
end
ユーザーはprepend
という最後の手段を繰り出さずに済みます。
まとめ
以上で、ささやかなレッスンはすべて終了です。もっとお知りになりたい方は、元記事末尾のメーリングリストに加入いただくか、私どものFearless Refactoringの購入をぜひお願いします。