Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Ruby: gemが生成するコードを無名モジュールとprependで動かす(翻訳)

概要

原著者の許諾を得て翻訳・公開いたします。

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の購入をぜひお願いします。

関連記事

Ruby: ループには一時変数ではなくEnumerableを使おう(翻訳)

Ruby: 条件を切り出していい感じのメソッド名を付けよう(翻訳)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。