Rails の黒魔術: 自己再定義メソッド
Ruby on Rails の意味不明な黒魔術を問題視するシリーズ。
問題: 次のメソッド定義において、(*1) や (*2) や (*3) は何をしているんでしょう?
module ActiveRecord module ConnectionAdapters class PostgreSQLColumn < Column private ... # Escapes binary strings for bytea input to the database. def self.string_to_binary(value) if PGconn.respond_to?(:escape_bytea) self.class.module_eval do # (*1) define_method(:string_to_binary) do |value| PGconn.escape_bytea(value) if value end end else self.class.module_eval do # (*2) define_method(:string_to_binary) do |value| if value result = '' value.each_byte { |c| result << sprintf('\\\\%03o', c) } result end end end end self.class.string_to_binary(value) # (*3) end ... end end end
(activerecord-2.1.0/lib/active_record/connection_adapters/postgresql_adapter.rb より抜粋)
答えは、自己再定義メソッドの定義。
ActiveRecord::ConnectionAdapters::PostgreSQLColumn.string_to_binary() メソッドは、最初に実行されたとき、自分自身を再定義する *1。
class Foo def hello(name) puts "*** 1回目の呼び出し..." self.class.module_eval do # 自分自身を再定義 define_method(:hello) do |name| puts "Hello #{name}!" end end self.hello(name) # 再定義したメソッドを呼び出す puts "*** 終了" end end foo = Foo.new foo.hello('world') # 1回目の呼び出し foo.hello('world') # 2回目の呼び出し foo.hello('world') # 3回目の呼び出し
実行結果:
*** 1回目の呼び出し... Hello world! *** 終了 Hello world! Hello world!
よくこんなこと思いつくよな。最初に考えたやつは頭いい。
でも、これが本当に必要なのかは疑問だ。こう書けば済む話だと思う。
module ActiveRecord module ConnectionAdapters class PostgreSQLColumn < Column private ... if PGconn.respond_to?(:escape_bytea) def self.string_to_binary(value) PGconn.escape_bytea(value) if value end else # Escapes binary strings for bytea input to the database. def self.string_to_binary(value) if value result = '' value.each_byte { |c| result << sprintf('\\\\%03o', c) } result end end end ... end end end
どう考えてもこっちのほうがわかりやすい。わざわざ黒魔術を使う意味がわからない。
なんか妥当な理由を思いついた人がいたら教えてください。