きっかけ
Swiftでprotocolの実装を複数持てるのは便利そうだなぁと思ったことある。(foo as A).call だとA#call で、(foo as B).callだとB#callが呼べる的な #fukabori_rubykaigi_2022
— 神速 (@sinsoku_listy) 2022年10月5日
よく考えたら as
メソッドを定義するだけで実現できそうな気がしたので、実装してみた。
Swiftの話
Swiftを勉強してた頃に、プロトコル拡張で実装したメソッドをキャストした型によって呼び出し分けるのを試したことがある。
検証コード
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のように汎用的な名前は避けるが、実装を切り替えることが可能になれば使うことができる。