odeの開発メモ日記

プログラマーやってます。今までの道のり ポケコンbasic→C(DirectX5 ネットやろうぜ)→Perl(ちょろっと)→Java→C#→Ruby→Android(Java)

classをカスタマイズするために再定義じゃなくてaliasしないとだめな場合がある話

当たり前といえば当たり前なaliasの挙動の話です。
わかってたはずなのに、とある状況だと気づかなかったのでメモります。


今回はまった部分は以下の部分の修正
railsでdb保存した場合に自動でcreated_at列に現在の時間が入る機能。
これををレガシーDBの列名がdt_creationなのでそれに対応したいと思いました。
(列名に型のprefix名がついちゃってるとか本当かんべんしてー)


オリジナルのコード

module ActiveRecord
  module Timestamp
    def self.included(base) #:nodoc:
      base.alias_method_chain :create, :timestamps
--- 省略 ---
    end
    
--- 省略 ---

    private
      def create_with_timestamps #:nodoc:
        if record_timestamps
          current_time = current_time_from_proper_timezone

          write_attribute('created_at', current_time) if respond_to?(:created_at) && created_at.nil?
          write_attribute('created_on', current_time) if respond_to?(:created_on) && created_on.nil?
--- 省略 ---
        end

        create_without_timestamps
      end
--- 省略 ---
  end
end


これを下記のようにcreate_with_timestampsメソッドを再定義しても動きません。


再定義ver(NG)

module ActiveRecord
  module Timestamp
    def create_with_timestamps #:nodoc:
      if record_timestamps
        current_time = current_time_from_proper_timezone
        write_attribute('dt_creation', current_time) if respond_to?(:dt_creation) && dt_creation.nil?
      end
    end
  end
end


aliasで元のcreateを上書きすればOK


alias ver(OK)

module ActiveRecord
  class Base
    def create_with_timestamps_regacy #:nodoc:
       current_time = current_time_from_proper_timezone
      write_attribute('dt_creation', current_time) if respond_to?(:dt_creation) && dt_creation.nil?
      create_without_timestamps_regacy
    end
    alias_method_chain :create, :timestamps_regacy
  end
end


これはaliasした場合、古いメソッド名のほうを再定義しても新しいメソッド名には反映されないからです。


簡単なtestコード

def test
  "test"
end

def test_new
  "test_new"
end

alias test test_new

def test_new
  "test_new_override"
end

p test # test_newと表示(test_newの再定義は反映されない)


よくよく考えるといつもよくやる下記の動作ができてるってことはそりゃそうだと思った。
http://www.ruby-lang.org/ja/man/html/_A5AFA5E9A5B9A1BFA5E1A5BDA5C3A5C9A4CEC4EAB5C1.html#alias

# メソッド foo を定義
def foo
  "foo"
end

# 別名を設定(メソッド定義の待避)
alias :_orig_foo :foo

# foo を再定義(元の定義を利用)
def foo
  _orig_foo * 2
end

p foo  # => "foofoo"

もし、古いメソッド名のほうを再定義して新しいメソッドにも影響あったら上記のやりたいことできないもんね。


aliasの内部的な仕組みを想像するとメソッド定義のコピーであってメソッドのポインタをコピーしてるわけではなさそう。
もしくは再定義するとオリジナルのメソッドポインタとは違う、別のメソッドポインタで作られているのかもしれない。

まとめ

カスタマイズするには再定義ではだめな場合がある(aliasされて実装されてる機能の場合)
目には目を、aliasにはaliasをw

実行環境

ruby (1.8.6)
rails (2.3.5)