動かざることバグの如し

近づきたいよ 君の理想に

Rubyでメソッドを色んな方法で上書きしてみる

環境

やりたいこと

使ってるGemの挙動を一部変えたくて、インスタンスメソッドを上書きしたい できれば黒魔術にならずにシンプルに変えたい

コード例ではCatクラスがあって、本来はhello()で「nyaa」を返すが「bowwow」に変更したい

うまくいかない例

  • 普通にmoduleを作成してincludeする
  • Rubyのクラス継承チェインの上位に上書きしたいmoduleが来てしまうため無理
# NOT WORKING
class Cat
  def hello
    "nyaa"
  end
end

module ExtendCat
  def hello
    "bowwow"
  end
end

class Cat
  include ExtendCat
end

cat = Cat.new
puts cat.hello

方法1 クラス継承

  • 1番手っ取り早い、クラスを継承してメソッドを上書き
  • 王道中の王道だが、rubyは多段継承を許してないので多用できない
class Cat
  def hello
    "nyaa"
  end
end

class MyCat < Cat
  def hello
    "#{super} > bowwow"
    # スーパークラスのメソッドはsuperで呼びさせるが、
    # 同じ引数が渡されることに注意 何も渡したくない場合はsuper()
  end
end

cat = MyCat.new
puts cat.hello

方法2 alias_method

  • 既存とは別の名前でメソッドを定義して、エイリアス機能を使ってメソッドをすり替えるモンキーパッチらしい方法
  • デメリットとして、エイリアスが被る可能性がある、新しいメソッドから古いメソッドを自動で呼べない
class Cat
  def hello
    "nyaa"
  end
end

module ExtendCat
  def new_hello
    "#{old_hello} > bowwow"
  end
end

class Cat
  include ExtendCat
  alias_method :old_hello, :hello
  alias_method :hello, :new_hello
end

cat = Cat.new
puts cat.hello

方法3 module#prepend

  • モジュール内に変更したいメソッドを再定義し、クラスを再オープンしてモジュールを prepend() する
  • うまくいかない例の include() とは違い、モジュールで定義した方が優先されるため可能
  • 方法2のやり方よりこっちのほうがよい
class Cat
  def hello
    "nyaa"
  end
end

module ExtendCat
  def hello
    "#{super} > bowwow"
  end
end

class Cat
  prepend ExtendCat
end

cat = Cat.new
puts cat.hello

方法4 委譲

  • DelegateClassを使うと、クラスを引数として受け取りそのクラスのオブジェクトにインスタンスメソッドを委譲する事が可能
  • DelegateClass(Cat)でCatオブジェクトにインスタンスメソッドを委譲するクラスが定義され、そのクラスを継承したMyCatクラスを定義することでメソッド実行を委譲している
class Cat
  def hello
    "nyaa"
  end
end

class Mycat < DelegateClass(Cat)
  def initialize(cat)
    super
  end

  def hello
    "#{super} > bowwow"
  end
end

cat = Mycat.new(Cat.new)
puts cat.hello

方法5 define_singleton_method

  • そもそも特異メソッドとは、インスタンス固有のメソッドのこと(instance specific method)
  • define_singleton_method() を使うと特異メソッドを定義できるためメソッドの挙動を書き換えることができる
  • 他の方法と違って挙動を戻すことが可能
class Cat
  def hello
    "nyaa"
  end
end

cat = Cat.new
puts cat.hello

orig = cat.method(:hello) # 元のメソッドを呼びたい時必要
cat.define_singleton_method(:hello) do
  "#{orig.call} > bowwow"
end
puts cat.hello

# 元に戻す
cat.singleton_class.send(:remove_method, :hello)
puts cat.hello

方法6 refinements

  • refinementsを使うと既存のメソッドの中身を一時的に変更できる
class Cat
  def hello
    "nyaa"
  end
end

module ExtendCat
  refine Cat do
    def hello
      "#{super} > bowwow"
    end
  end
end

using ExtendCat

puts Cat.new.hello

方法7 instance_eval

  • instance_evalを使うとメソッドやインスタンス変数を拡張できる。当然上書きもできるってわけ
class Cat
  def hello
    "nyaa"
  end
end

cat = Cat.new
cat.instance_eval do
  def hello
    "#{super} > bowwow"
  end
end
puts cat.hello

参考リンク