こんにちは、寺岡です。
近頃、巷を騒がせている偽装問題。
それにちなんで、Rubyで見かける偽装問題を見破ってみたいと思います。
偽装、もといProxyパターン
Proxyパターンと呼ばれるデザインパターンがあります。
噛み砕いて説明すると「対象のオブジェクトっぽい動きをするけどちょっとだけアレンジされてるよ」
みたいなオブジェクトを作成するときに使われます。
Rubyでは動的言語としての特徴を活かして、method_missingを使った以下のようなコードで実装されます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
# to_sが16進数になるInt class HexInt < BasicObject def initialize(int) @int = int.to_i end def to_s(base = 16) @int.to_s(base) end def inspect to_s end # メソッドが存在しない場合に呼ばれるメソッド # @intに処理を移譲する def method_missing(method, *args, &block) val = @int.__send__(method, *args, &block) # 戻り値がIntegerならHexIntに変換する val.kind_of?(::Integer) ? HexInt.new(val) : val end end p HexInt.new(255) => ff p HexInt.new(255) + 1 => 100 |
HexIntの親クラスになっているBasicObjectは、Ruby1.9から登場したObjectクラスの親のクラスです。
==やmethod_missinngなど、ObjectClassの基本的なメソッドのいくつかが定義されています。
BasicObjectを継承して、method_missingを継承することで、別のクラス(オブジェクト)になりすますためのクラスを作ることができるのです。
どんなときにハマるの?
Railsで特定の日時の1年後を表すDateTimeオブジェクトが欲しい場合、以下のコードで実現できます。
1 2 |
p DateTime.new(2013) + 1.year => Wed, 01 Jan 2014 00:00:00 +0000 |
この1.yearという数は、後で変更されるかもしれないので、設定ファイルに切り出すことにしました。
こんな時によく使うのがSettingsLogicというGemで、設定ファイルをYAML形式で扱えます。
1 2 3 |
# config/application.yml development: expired_after: <%= 1.year %> |
では切り出した設定値を使って、日付を計算してみましょう。
1 2 |
p DateTime.new(2013) + Settings.expired_after => Sat, 11 Oct 88414 00:00:00 +0000 |
あれ?おかしな日時になってしまいました。うーむ…。
1 2 3 4 5 6 |
p Settings.expired_after == 1.year # true p Settings.expired_after.class == 1.year.class # true p Settings.expired_after # 31557600.0 p 1.year # 31557600.0 p Settings.expired_after.class # Float p 1.year.class # Float |
値も同じ、クラスも同じ、どうして結果が違うのでしょうか。
もう少し詳しく調べてみます。
1 2 3 4 5 6 7 8 9 10 11 |
p DateTime.new(2013) + 1.year => Wed, 01 Jan 2014 00:00:00 +0000 p DateTime.new(2013) + Settings.expired_after => Sat, 11 Oct 88414 00:00:00 +0000 p DateTime.new(2013) + 31557600.0 => Sat, 11 Oct 88414 00:00:00 +0000 p DateTime.new(2013) + 1.year.to_f => Sat, 11 Oct 88414 00:00:00 +0000 |
1.yearだけ普通のFloatと結果が違うことが分かりました。
実体は
実はSettings.expired_afterと1.yearのクラスは同じではありません。
Settings.expired_afterはFloatですが、1.yearの実体はActiveSupport::Durationクラスのインスタンスなのです。
設定ファイルのYAMLにERBで記述した 「expired_after: <%= 1.year %>」 は「expired_after: 31557600.0」のように文字列として展開された後、
YAMLのパーサによりFloatに変換されるため、ActiveSupport::Durationクラスのインスタンスであったという情報は失われてしまいます。
ではなぜ、1.year.classがFloatになるのでしょうか。
これは、最初に紹介した BasicObject による Proxyパターンの実装に起因します。
先ほどのHexIntのインスタンスに.classメソッドを呼び出した場合も、結果はFixnumになります。
BasicObjectクラスにはclassメソッドが定義されていないため、method_missingへと処理が移譲されてしまうのです。
1 2 3 4 |
# HexIntで.classを実行してみる # 内部の処理の流れは HexInt.new(255).class -> HexInt.new(255).method_missing(:class) -> 255.class となる p HexInt.new(255).class => Fixnum |
クラス偽装を見破る
BasicObjectを用いたProxyクラスによる偽装を見破るために、以下のようなメソッドを定義してみます。
Ruby1.8と1.9以降でコードが違うのでご注意ください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
# ruby 1.8 までの場合 def expose_class(object) Object.instance_method(:class).bind(object).call end # ruby 1.9 以降 def expose_class(object) object.instance_eval{ class << self; self.superclass; end} rescue object.class end p expose_class(Settings.expired_after) => Float p expose_class(1.year) => ActiveSupport::Duration |
○○クラスの筈なのにおかしな動きをするなぁ、などという時は、このメソッドを利用して本当にそのクラスかどうか調べてみると良いかもしれません。