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: 条件を切り出していい感じのメソッド名を付けよう(翻訳)

デザインも頼めるシステム開発会社をお探しならBPS株式会社までどうぞ 開発エンジニア積極採用中です! Ruby on Rails の開発なら実績豊富なBPS

この記事の著者

hachi8833

Twitter: @hachi8833、GitHub: @hachi8833 コボラー、ITコンサル、ローカライズ業界、Rails開発を経てTechRachoの編集・記事作成を担当。 これまでにRuby on Rails チュートリアル第2版の半分ほど、Railsガイドの初期翻訳ではほぼすべてを翻訳。その後も折に触れてそれぞれ一部を翻訳。 かと思うと、正規表現の粋を尽くした日本語エラーチェックサービス enno.jpを運営。 実は最近Go言語が好き。 仕事に関係ないすっとこブログ「あけてくれ」は2000年頃から多少の中断をはさんで継続、現在はnote.muに移転。

hachi8833の書いた記事

BPSアドベントカレンダー

週刊Railsウォッチ

インフラ

ActiveSupport探訪シリーズ