Rubyで1つのインスタンスに同名メソッドを複数保つ方法

きっかけ

よく考えたら as メソッドを定義するだけで実現できそうな気がしたので、実装してみた。

Swiftの話

Swiftを勉強してた頃に、プロトコル拡張で実装したメソッドをキャストした型によって呼び出し分けるのを試したことがある。

sinsoku.hatenablog.com

検証コード

module As
  class Proxy
    def initialize(this, mod)
      @this = this
      @mod = mod
    end

    def method_missing(name, *args)
      raise NoMethodError unless @mod.instance_methods.include?(name)
      @mod.instance_method(name).bind_call(@this, *args)
    end
  end

  def as(mod)
    raise NoMethodError unless self.class.ancestors.include?(mod)
    Proxy.new(self, mod)
  end
end

module A
  def call = puts "A"
end

module B
  def call = puts "B"
end

class Foo
  include As
  include A
  include B
end

foo = Foo.new

foo.call #=> B
foo.as(A).call #=> A
foo.as(B).call #=> B

コメントを読むと分かるように、普通は 後勝ちでBの実装 が優先される。

これを as メソッドを使うことで、任意のモジュールの実装を呼び出せるようになった。

メリット

モジュールに定義するメソッド名をシンプルに保つことができる。

例: データをAWSとDBに保存するメソッド

module AwsS3Record
  def save
    # S3にjsonを保存する処理
  end
end

class User < ActiveRecord::Base
  include AwsS3Record
end

user = User.new
user.as(ActiveRecord::Base).save
user.as(AwsS3Record).save

本来はsaveのように汎用的な名前は避けるが、実装を切り替えることが可能になれば使うことができる。