アルパカログ

Webエンジニア兼マネージャーがプログラミングやマネジメント、読んだ本のまとめを中心に書いてます。

Ruby 特異メソッドを定義してインスタンスメソッドを上書きする方法

f:id:otoyo0122:20200902125656p:plain:w300

一般に、継承やMix-inを使わずにメソッドを上書きするのは推奨されることではありません。

読み手にとって何が起こっているかわからないコードは、バグの温床となるからです。

しかし、上書きができないというわけではありません。そう、Rubyならね…。

このエントリでは、特異メソッドを定義してインスタンスメソッドを上書きする方法を紹介します。

特異メソッドとは?

「特異メソッド」という名前は、特異という言葉が難しく少しわかりづらいですが、英語を見るとわかりやすいです。

特異メソッドは、英語では「instance-specific method」と言います。

instance-specific、つまりインスタンス固有のメソッドと理解できます。

Rubyでは、クラスメソッドは特異メソッドと説明されることがあります。

このことも、特異メソッドはインスタンス(=オブジェクト)固有のメソッドであるという前提に立つと、すっきり理解することができます。

Rubyでは、例えばclass Postのように定義したクラスは、Classクラスのオブジェクトになります。

クラスメソッドはクラスオブジェクト固有のメソッドになることから、特異メソッドと言えるわけですね。

instance_execを使う方法

インスタンスメソッドを上書きするには、インスタンスに対して特異メソッドを定義すれば良いということがわかりました。

特異メソッドを定義する方法はいくつかありますが、まずはinstance_execを使う方法から紹介します。

下記のコードをご覧ください。

post1インスタンスに対してinstance_execを呼び出しています。

class Post                                         
  def rank                                         
    calc_likes                                     
  end                                              

  def calc_likes                                   
    # some heavy calc                              
    :raw                                           
  end                                              
end                                                

post1 = Post.new                                   
post2 = Post.new                                   

post1.instance_exec(:replacer) do |like_count|     
  @_like_count = like_count                        

  def self.calc_likes                              
    @_like_count                                   
  end                                              
end

puts post1.rank # => replacer
puts post2.rank # => raw

instance_execブロック内のself はレシーバのオブジェクトpost1を指すので、ブロック内のdefではpost1オブジェクト固有のメソッド(特異メソッド)を定義していることになります。

post1オブジェクト固有なので、post2オブジェクトには影響はありません。

post1calc_likesだけが、置き換えた値:replacerを返すようになります。

instance_execの引数は、ブロック引数としてブロック内に渡すことができます。

ここでは任意の値:replacerを渡し、ブロック引数like_countとして受け取っています。

いったんインスタンス変数@_like_countへ代入しているのは、self.calc_likesメソッドから参照できるようにするためです。

define_singleton_methodを使う方法

実は、特異メソッドの定義はdefine_singleton_methodを使うともっとシンプルに書くことができます。

下記のコードをご覧ください。

class Post
  def rank
    calc_likes
  end

  def calc_likes
    # some heavy calc
    :raw
  end
end

post1 = Post.new
post2 = Post.new

post1.define_singleton_method(:calc_likes) do
  :replacer
end

puts post1.rank # => replacer
puts post2.rank # => raw

とてもシンプルに書くことができました。

以上です。

このエントリでは、特異メソッドを定義してインスタンスメソッドを上書きする方法を紹介しました。

参考になった方は、ぜひ「はてブ」やSNSでシェアしていただけると嬉しいです。