Ruby で upcasting 的なことがしたくて upcastable という gem を作った

Ruby の良さを殺してると言われそうですが、upcasting っぽいことができる gem を作りました。初めて公開した gem なので至らないところもありますが…
https://github.com/abicky/upcastable

次のコマンドでインストールできます。

gem install upcastable

サンプルコード

次のような Animal module と Cat class があるとします。

module Animal
  def talk; end
end

class Cat
  include Animal

  def talk
    'Meow!'
  end

  def run
    'Running...'
  end
end

次のように upcast_to で Animal に upcasting することで、Animal に定義されているメソッドしか使えなくすることができます。

cat = Cat.new
animal = cat.upcast_to(Animal)
animal.class #=> Cat
animal.talk  #=> "Meow!"
animal.run   #=> NoMethodError: `run' is not defined in Animal

詳細は README を見てください。

仕組み

upcast_to の返り値は Upcastable::UpcastedObject なんですが、そいつが次のように upcast した class または module にインスタンスメソッドが定義されているかチェックして、存在すれば delegate しているだけです。

    def method_missing(m, *args, &block)
      unless @ancestor.method_defined?(m)
        raise NoMethodError, "`#{m}' is not defined in #{@ancestor}"
      end
      @object.send(m, *args, &block)
    end

メソッドが存在しない場合だけでなく、==, eql? 等 Object に定義されているインスタンスメソッドも delegate しているので、現在の実装だと a == a.upcast_to(SomeClass) と a.upcast_to(SomeClass) == a で結果が変わるという鬼畜なことになっています。どちらも false を返すようにするかもです。

メソッドの存在チェックと delegate をしているので当然遅くなりますが、https://github.com/abicky/upcastable/blob/v0.1.0/benchmark/upcasting.rb を実行してみる感じだと 1 μs/call 程度の差なのかなと思います。

何故作ったか?

例えば class A, class B, class C どのインスタンスでも同じように処理しているコードがあったとして、class A のインスタンスでしかテストしていなかったとします。ところが、そのコードで class A にしか定義されていないメソッドを呼んでいると、テストは通るのに class B や class C のインスタンスを処理する場合にエラーになってしまいます。
class A, class B, class C が共通のメソッドを持つことはテストで簡単に保証することができますが、ある一連の処理の中で共通のメソッドしか呼ばれないことを保証するのは少し大変です。

共通のメソッドを super class や module に定義した上で upcasting すれば、class A, class B, class C 全てのケースでテストしなくても、いずれかのクラスでテストが通れば他のクラスでもテストが通ることになり、より本質的なロジックのテストに集中できます。