スパイダープラス Tech Blog

建設SaaS「スパイダープラス」のエンジニアとデザイナーのブログ

Rubyのクラスメソッドの仕組みを理解する

はじめに

初めまして、スパイダープラスでWebエンジニアをしているizkです。
普段は、S+Reportというプロダクトでバックエンドを中心にRubyPHPなどを書いています。

さて、Rubyでコーディングしていると、モジュールをクラスにincludeしてメソッドを呼び出す場面はよくあると思います。

ある日、同じようにモジュールをincludeしてメソッドを呼び出そうとしたところ、undefined methodエラーが発生しました。
原因を調べてみると、メソッドを呼び出そうとした場所がクラスメソッド内だったためでした。includeextendに書き換えると、無事にメソッドを呼び出せるようになりました。

この時の私は「インスタンスメソッドにはinclude、クラスメソッドにはextend」くらいの認識でコードを書いていたため、なぜそうなるのか仕組みを調べてみることにしました。

本記事の対象読者

本記事のゴール

Rubyのオブジェクトモデル(クラス、モジュール、特異クラス)を整理しつつ、クラスメソッドの呼び出しの仕組みを理解すること

0. Rubyはすべてがオブジェクト

前提

Rubyにおけるオブジェクトとは、状態、振る舞い、クラスへの参照をひとまとめにしたものです。
インスタンスという言葉は、クラスから生成されたオブジェクトという意味で使っています。

はじめに

Rubyは「純粋オブジェクト指向言語」と呼ばれていて、すべてがオブジェクトとして扱われます。
オブジェクトはクラスの参照を持っているため、何らかのクラスのインスタンスになります。

サンプルコード:

# すべてがオブジェクトである証拠
1.class          # => Integer
"hello".class    # => String
nil.class        # => NilClass
true.class       # => TrueClass
[1, 2, 3].class  # => Array

# クラス自体もオブジェクト(Classクラスのインスタンス)
Integer.class    # => Class
String.class     # => Class
Array.class      # => Class
Class.class      # => Class

最後の行を見ると、ClassクラスもClassのインスタンスだということが分かります。

1. クラス

はじめに

Rubyにおけるクラスは、インスタンスの設計図であり、同時にオブジェクトでもあります。
クラスからインスタンスを生成でき、クラス自体もClassクラスのインスタンスとして振る舞います。
また、クラスはスーパークラス(親クラス)を持ち、継承によって振る舞いを引き継ぐことができます。

1.1 クラスとインスタンス

Userクラスを定義して、そのインスタンスを生成してみます。
以下にサンプルコードとオブジェクトモデル図を記載します。

サンプルコード:

class User; end
user = User.new

1行目でクラスを定義して、2行目でクラスのnewメソッドを使ってインスタンスを生成しています。

オブジェクトモデル図

オブジェクトモデル図は、四角はオブジェクトを、矢印は参照を表現しています。
オブジェクトを右へ移動するとクラスが見えて、上へ移動するとスーパークラスが見えるようになっています。
このスーパークラスをたどる経路を「継承チェーン」と呼びます。この考え方は、後述するメソッド探索でも重要になります。

1.2 状態と振る舞い

先ほどのUserクラスに状態(インスタンス変数)と、振る舞い(メソッド)を追加してみます。
このメソッドはインスタンスメソッドと呼ばれ、インスタンスをレシーバとして呼び出されます。

サンプルコード:

class User
  def initialize(name)
    @name = name  # 状態(インスタンス変数)
  end

  def greet       # 振る舞い(メソッド)
    "Hi, I'm #{@name}!"
  end
end

モデル図

インスタンス変数(@name)はインスタンスが持つため、オブジェクト毎に固有の状態を保持できます。
メソッド(greet)はクラスが持つため、すべてのインスタンスで同じ振る舞いを共有できる仕組みになっています。

1.3 メソッド探索

インスタンスからメソッドを呼び出すとき、Rubyは特定の順序でメソッドを探索します。
この探索ルールは「右へ一歩、そして上へ」と表現されます。

  1. 右へ一歩: インスタンスからクラスへ移動(classの参照)
  2. そして上へ: クラスからスーパークラスへ移動(superclassの参照)を繰り返す

サンプルコード:

class Animal
  def breathe
    "スーハー"
  end
end

class Human < Animal; end

human = Human.new
human.breathe  # => "スーハー"

Humanクラスにはbreatheメソッドがありませんが、スーパークラスのAnimalにあるため呼び出せます。

モデル図

この「右へ一歩、そして上へ」というルールは、後述するモジュールのincludeや特異クラスを理解する上で非常に重要になります。

1.4 クラスのクラス

最初に見たとおり、クラスも何かしらのクラスのインスタンスです。
Userクラスのモデル図に追加して、確認してみます。

モデル図

自己参照という仕組みを使うため、ClassのクラスはClass自身になります。

2. モジュール

はじめに

本章では、クラスに似たオブジェクトで、継承チェーン上に存在可能な"モジュール"について説明していきます。

2.1 モジュールとクラス

実は、モジュール(Module)とはクラス(Class)のスーパークラスです。
つまり、全てのクラスはモジュールを継承しているため、クラスはモジュールであると言えます。

サンプルコード:

Class.superclass  # => Module

さらに、ModuleのスーパークラスはObjectになります。

サンプルコード:

Module.superclass # => Object

モデル図

モデル図にすると、参照が循環しているように見えますがそんなことは無く、UserもそのクラスであるClassもObjectのサブクラスというだけです。
classとsuperclassの参照を区別しながら考える必要があります。

2.2 Kernelモジュール

Userクラスの継承チェーンを確認すると、Kernelというものが見えてきます。
これは、Objectクラスにincludeされたモジュールになります。
Kernelモジュールには、putsrequireloopraiseなど基本的なメソッドが定義されています。
つまり、クラス内でputsなどのメソッドを呼び出せるのは、Objectを継承しており、ObjectがKernelモジュールをincludeしているためです。

サンプルコード:

class User; end
User.ancestors # => [User, Object, Kernel, BasicObject]

2.3 モジュールのinclude

モジュールを作成すると、includeメソッドでクラスに追加できます。
includeしたモジュールは、継承チェーンのinclude先のクラスとそのスーパークラスの間に挿入されます。

サンプルコード:

module Greetable
  def greet
    "Hello!"
  end
end

class User
  include Greetable
end

User.ancestors # => [User, Greetable, Object, Kernel, BasicObject]

user = User.new
user.greet # => "Hello!"

モデル図

3. 特異クラス(シングルトンクラス)

はじめに

本章では、特定のオブジェクトが1つだけ持てる隠れたクラス「特異クラス」について説明していきます。
特異クラスを理解することで、本記事のゴールであるクラスメソッドの呼び出しについて深く理解することができます。

3.1 クラスメソッド

本記事のテーマであるクラスメソッドについて見ていきます。
クラスメソッドとは、クラスをレシーバとして呼び出せるメソッドのことで、メソッドの先頭にself.を付けることで定義できます。

サンプルコード:

class User
  def self.count
    100
  end
end

User.count  # => 100

alice = User.new("Alice")
alice.count  # => NoMethodError(インスタンスからは呼び出せない)

では、クラスメソッドはどこに存在するのでしょうか?
それを確認するために、クラスメソッドに関わりの深い特異メソッドを見ていきます。

3.2 特異メソッド

特異メソッドとは、特定のインスタンスだけに定義されたメソッドで、同じクラスの他のインスタンスにも影響しないメソッドです。

サンプルコード:

alice = User.new("Alice")

# aliceオブジェクトだけに特異メソッドを定義
def alice.admin?
  true
end

alice.admin?  # => true

bob = User.new("Bob")
bob.admin?    # => NoMethodError(bobには定義されていない)

では、特異メソッドはどこに存在しているのでしょうか?
答えは、特異クラスです。

3.3 特異クラス(シングルトンクラス)

特異クラスは、各オブジェクトが1つだけ持てる隠れたクラスで、1つだけなので、シングルトンクラスとも呼ばれます。
特異メソッドを定義すると、Rubyは必要に応じて特異クラスを生成し、その中に特異メソッドを格納します。
aliceに特異クラスと特異メソッドが存在していることをサンプルコードで確認してみましょう。

サンプルコード:

# 特異クラスの存在を確認
alice.singleton_class           # => #<Class:#<User:0x00007f...>>
alice.singleton_class.superclass # => User

# 特異メソッドが特異クラスに定義されていることを確認
alice.singleton_class.instance_methods(false)  # => [:admin?]

特異クラスを含めたモデル図:

aliceのクラスが特異クラスとなっています。
これは、メソッド探索が「右へ一歩、そして上へ」の順で行われるため、特異メソッドを最初に探索するにはこの位置に入る必要があるためです。
つまり、aliceの概念的なクラスはUserですが、実際のクラスは特異クラス(#<Class:alice>)になります。

3.4 クラスメソッドと特異クラスの関係

もうお気づきかもしれません。クラスメソッドはクラスの特異メソッドになります。
そのため、クラスメソッドが定義されると、クラスに特異クラスが生成されて、その中にクラスメソッドが格納されます。

サンプルコード:

class User
  def self.count
    100
  end
end

特異クラスを含めたモデル図:

インスタンスの特異クラス同様にスーパークラスのクラスメソッドを探索する必要があるため、上記のような継承関係になります。
つまり、Userクラスの概念的なクラスはClassで、実際のクラスは特異クラス(#<Class:User>)になります。

3.5 モジュールのextend

ここまでの内容を踏まえて、extendの仕組みを見ていきましょう。
extendは、モジュールのメソッドをクラスメソッドとして追加します。
includeインスタンスメソッドを追加するのに対し、extendはクラス自体にメソッドを追加します。

サンプルコード:

module Greetable
  def greet
    "Hello!"
  end
end

class User
  extend Greetable
end

User.greet       # => "Hello!"(クラスメソッドとして呼び出せる)

user = User.new
user.greet       # => NoMethodError(インスタンスメソッドとしては呼び出せない)

extendを使うと、モジュールはクラスの特異クラスの継承チェーンに挿入されます。
3.4で見たように、クラスメソッドは特異クラスに住んでいるため、extendしたモジュールのメソッドもクラスメソッドとして呼び出せるようになります。
最後にモジュールをincludeとextend両方行った場合をモデル図で確認していきます。

サンプルコード:

class User
  include Greetable  # インスタンスメソッドとして追加
  extend Greetable   # クラスメソッドとして追加
end

モデル図:

このように、includeは左側の継承チェーン(インスタンスメソッド用)に、extendは右側の継承チェーン(クラスメソッド用)にモジュールを挿入します。

まとめ

本記事のゴールである「クラスメソッドの呼び出しの仕組み」をまとめると、以下のようになります。

  • クラスメソッドは、クラスの特異クラスに存在している
  • クラスメソッドを呼び出すと、クラスの特異クラスの継承チェーンを「右へ一歩、そして上へ」と探索する
  • includeでモジュールを追加すると、左側の継承チェーン(インスタンスメソッド用)に挿入されるため、インスタンスメソッドとして呼び出せる
  • extendでモジュールを追加すると、右側の継承チェーン(クラスメソッド用)に挿入されるため、クラスメソッドとして呼び出せる

つまり、インスタンスをレシーバとしてメソッドを呼び出したい場合はinclude、クラスをレシーバとして呼び出したい場合はextendを使います。

感想

インスタンスとクラスの間に特異クラスが作られるのは、実物と設計図の間に他の要素が入る感じがして違和感がありましたが、これによりインスタンスメソッドとクラスメソッド両方にメソッド探索のルールが適用できる仕組みになっていると思いました。
実際、特異クラスは隠れたクラスという扱いなので、普段は意識せずコーディングできるようになっています。

冒頭で紹介したundefined methodエラーは、includeしたモジュールが左側の継承チェーン(インスタンスメソッド用)に入ったため、クラスメソッドとして呼び出せなかったと理解できました。

おわりに

ここまでお読みいただき、ありがとうございました!

本記事は、社内で開催された技術LT大会での発表内容をもとに執筆しました。スパイダープラスでは、こうした技術共有の場を定期的に設けており、エンジニア同士が学び合う文化を大切にしています。

スパイダープラスでは、建設DXを推進する仲間を募集中です。Rubyをはじめ様々な技術を使って開発しています。興味のある方は、ぜひお気軽にご連絡ください!

参考文献

メタプログラミングRuby www.oreilly.co.jp